From 8ad887f7bbc2cf891dad763821771e63f3a10ec7 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 10 Dec 2025 10:54:17 -0800 Subject: [PATCH 01/77] New translations enus.json (French) --- src/locales/strings/fr.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/locales/strings/fr.json b/src/locales/strings/fr.json index e6c6813ef7a..642e95d4f17 100644 --- a/src/locales/strings/fr.json +++ b/src/locales/strings/fr.json @@ -82,7 +82,6 @@ "warning_scam_message": "Has anyone helped you set up this account?", "warning_scam_message_no_1s": "Great, if you ever have any questions, please reach out to our support team at %1$s.", "warning_scam_message_yes_1s": "Please proceed with caution! Assistance with account creation has the potential for fraud. Users should never share passwords or private keys. Social media and chat platforms have been involved in attacks. Do not send cryptocurrency to strangers. If you believe you're being taken advantage of, please contact our support team at %1$s.", - "warning_token_code_override_2s": "The entered contract address differs from the contract address of built-in token %1$s. Please proceed with caution and verify the contract is legitimate as use of this token can result in loss of funds. If you have questions about this feature or contract please contact %2$s.", "warning_token_exists_1s": "The entered token already exists as a built-in token %1$s", "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", From 4df57b857637f0efcdea059571b137a826039f97 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 10 Dec 2025 10:54:18 -0800 Subject: [PATCH 02/77] New translations enus.json (Spanish) --- src/locales/strings/es.json | 39 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/locales/strings/es.json b/src/locales/strings/es.json index 26cebd5d31a..4b4802e607e 100644 --- a/src/locales/strings/es.json +++ b/src/locales/strings/es.json @@ -82,7 +82,6 @@ "warning_scam_message": "¿Alguien te ayudó a configurar esta cuenta?", "warning_scam_message_no_1s": "Perfecto, si alguna vez tienes alguna pregunta, por favor comunícate con nuestro equipo de soporte en %1$s.", "warning_scam_message_yes_1s": "¡Procede con precaución! La asistencia en la creación de cuentas puede dar lugar a fraude. Los usuarios nunca deben compartir contraseñas ni claves privadas. Las redes sociales y las plataformas de chat han estado involucradas en ataques. No envíes criptomonedas a desconocidos. Si crees que se están aprovechando de ti, por favor comunícate con nuestro equipo de soporte en %1$s.", - "warning_token_code_override_2s": "La dirección de contrato ingresada difiere de la dirección de contrato del token integrado %1$s. Por favor procede con precaución y verifica que el contrato sea legítimo, ya que el uso de este token puede resultar en la pérdida de fondos. Si tienes preguntas sobre esta función o sobre este contrato, por favor comunícate con %2$s.", "warning_token_exists_1s": "El token ingresado ya existe como token integrado %1$s", "warning_battery_saver": "¡Se detectó el ahorro de batería! Es posible que los saldos no se actualicen. Para una mejor experiencia, por favor desactiva el modo de ahorro de batería.", "warning_sending_pix_to_email_title": "Enviando pago PIX a la dirección de correo electrónico", @@ -139,8 +138,8 @@ "request_xrp_minimum_notification_alert_body_1xrp": "Esta billetera siempre requerirá un saldo mínimo de 1 XRP", "request_xlm_minimum_notification_body": "Las carteras de Stellar requieren un saldo mínimo de 1 XLM. Debes depositar al menos 1 XLM a esta dirección antes de que esta cartera muestre saldo o transacciones. 1 XLM de esta cartera no podrán ser gastados de por vida.", "request_xlm_minimum_notification_alert_body": "Esta cartera siempre requerirá un mínimo de 1 XRP", - "request_dot_minimum_notification_body_1": "Polkadot (DOT) wallets require a 0.01 DOT minimum balance. You must deposit at least 0.01 DOT to this address before this wallet will be activated.", - "request_dot_minimum_notification_alert_body_1": "This wallet will always require a 0.01 DOT minimum", + "request_dot_minimum_notification_body_1": "Las billeteras de Polkadot (DOT) requieren un saldo mínimo de 0.01 DOT. Debes depositar al menos 0.01 DOT en esta dirección antes de que esta billetera se active.", + "request_dot_minimum_notification_alert_body_1": "Esta billetera siempre requerirá un saldo mínimo de 0.01 DOT", "request_lld_minimum_notification_body": "Las billeteras de Liberland (LLD) requieren un saldo mínimo de 1 LLD. Debes depositar al menos 1 LLD en esta dirección antes de que esta billetera muestre un saldo o transacciones. Ese 1 LLD no se podrá gastar durante toda la vida de esta dirección de billetera.", "request_lld_minimum_notification_alert_body": "Esta billetera siempre requerirá un saldo mínimo de 1 LLD", "fragment_send_address": "Dirección", @@ -204,28 +203,28 @@ "wallet_list_add_token": "Añadir token", "wallet_list_referral_link_currency_invalid": "La moneda a crear no es válida", "wallet_list_referral_link_currency_loading": "Un momento. Creando la cartera necesaria para esta promoción", - "wallet_list_referral_link_ask_wallet_creation_1s": "You need a %1$s wallet for this promotion. Would you like to create one?", + "wallet_list_referral_link_ask_wallet_creation_1s": "Necesitas una billetera de %1$s para esta promoción. ¿Te gustaría crear una?", "wallet_list_wallet_search": "Buscar carteras", "compromised_key_label": "Lave comprometida", - "create_new_account": "Create New Account", + "create_new_account": "Crear nueva cuenta", "create_wallet_choice_new_button": "Crear nueva cartera", "create_wallet_choice_new_button_fragment": "Nuevo monedero", - "create_wallet_select_wallet_for_assets": "Please select the wallet you would like to add the following assets: %s", + "create_wallet_select_wallet_for_assets": "Por favor, selecciona la billetera a la que te gustaría añadir los siguientes activos: %s", "create_wallet_import_title": "Importar cartera", - "create_wallet_import_options_title": "Import Options", - "create_wallet_import_options_birthday_height": "Wallet Birthday Height", - "create_wallet_import_options_birthday_height_description": "The birthday height is the network block height that your wallet will start synchronizing from.", - "create_wallet_import_options_passphrase": "Seed passphrase", - "create_wallet_import_options_passphrase_description": "A passphrase is an optional extra word or phrase you add to your recovery seed.", + "create_wallet_import_options_title": "Opciones de importación", + "create_wallet_import_options_birthday_height": "Altura de inicio de la billetera", + "create_wallet_import_options_birthday_height_description": "La altura de inicio es la altura de bloque de la red desde la que tu billetera comenzará a sincronizarse.", + "create_wallet_import_options_passphrase": "Frase de contraseña de la semilla", + "create_wallet_import_options_passphrase_description": "Una frase de contraseña es una palabra o frase adicional opcional que añades a tu semilla de recuperación.", "create_wallet_imports_title": "Importar monedero", - "create_wallet_import_all_instructions": "Enter your private seed, private key, or active key to verify and restore the associated wallet", + "create_wallet_import_all_instructions": "Ingresa tu semilla privada, clave privada o clave activa para verificar y restaurar la billetera asociada", "create_wallet_import_instructions": "Introduce tu semilla privada para verificar y restaurar la cartera asociada", "create_wallet_import_input_prompt": "Semilla privada", "create_wallet_import_key_instructions": "Introduce tu semilla privada para verificar y restaurar la cartera asociada", "create_wallet_import_input_key_prompt": "Clave privada", "create_wallet_import_input_key_or_seed_instructions": "Introduce tu semilla privada para verificar y restaurar la cartera asociada", "create_wallet_import_input_key_or_seed_prompt": "Clave privada o semilla privada", - "create_wallet_import_polkadot_input_key_or_seed_instructions": "Enter your private seed or private key to verify and restore the associated ed25519 wallet", + "create_wallet_import_polkadot_input_key_or_seed_instructions": "Ingresa tu semilla privada o clave privada para verificar y restaurar la billetera ed25519 asociada", "create_wallet_import_active_key_input_prompt": "Clave Privada Activa", "create_wallet_import_active_key_instructions": "Ingresa tu semilla privada para verificar y restaurar la cartera asociada:", "create_wallet_edit": "Editar", @@ -237,16 +236,16 @@ "create_wallet_fiat_type_label": "Moneda fiat de la cartera:", "create_wallet_failed_import_header": "Fallo en la importación de la clave", "create_wallet_all_failed": "Por favor edite la clave y vuelva a intentarlo.", - "create_wallet_some_failed": "The following assets cannot be imported with the provided seed: %s. Would you like to continue importing all other assets?", - "create_wallet_all_disabled_import": "No selected assets can be imported. You can create new wallets or go back and select different assets.", - "create_wallet_some_disabled_import": "The following assets cannot be imported: %s. \nWould you like to continue importing all other assets?", + "create_wallet_some_failed": "Los siguientes activos no se pueden importar con la semilla proporcionada: %s. ¿Te gustaría continuar importando todos los demás activos?", + "create_wallet_all_disabled_import": "Ninguno de los activos seleccionados se puede importar. Puedes crear nuevas billeteras o volver atrás y seleccionar otros activos.", + "create_wallet_some_disabled_import": "Los siguientes activos no se pueden importar: %s.\n¿Te gustaría continuar importando todos los demás activos?", "create_wallet_no_assets_selected": "No hay activos seleccionados", "create_wallet_failed_message": "La creación de la cartera falló por exceder el tiempo de espera. Por favor comprueba tu conexión a internet e vuevla a inténtarlo más tarde.", "create_wallet_create_account": "Crear Cuenta", "create_wallet_account_activate": "Activar Cuenta", "create_wallet_account_handle": "Alias de Cuenta", - "create_wallet_account_select_instructions_with_cost_4s": "All new %1$s wallets require a one time payment to activate the account and name. This payment is required by the %2$s network and not a requirement of %3$s.\n\nThe current cost is equivalent to %4$s but may fluctuate in the future.\n\nPlease select a wallet to pay from:", - "create_wallet_account_make_payment_2s": "You are about to make the following payment to activate your %1$s account for your %2$s wallet:", + "create_wallet_account_select_instructions_with_cost_4s": "Todas las nuevas billeteras de %1$s requieren un pago único para activar la cuenta y el nombre. Este pago es un requisito de la red %2$s y no es un requisito de %3$s.\n\nEl costo actual es equivalente a %4$s, pero puede fluctuar en el futuro.\n\nPor favor, selecciona la billetera desde la que deseas pagar:", + "create_wallet_account_make_payment_2s": "Estás a punto de realizar el siguiente pago para activar tu cuenta de %1$s para tu billetera de %2$s:", "create_wallet_account_select_wallet": "Seleccionar Cartera", "create_wallet_account_review_instructions": "Crear un alias de cuenta único, éste también será el nombre de tu cartera de %s:", "create_wallet_account_requirements_eos": "• Debe tener exactamente 12 caracteres\n• Debe incluir sólo letras minúsculas a-z o números 1-5\n", @@ -257,7 +256,7 @@ "create_wallet_account_amount_due": "Importe:", "create_wallet_account_error_sending_transaction": "Error enviando la transacción", "create_wallet_account_payment_sent_title": "Pago Enviado", - "create_wallet_account_payment_sent_message": "Activation payment sent. Please wait for a confirmation on your transaction before using your new wallet.", + "create_wallet_account_payment_sent_message": "Pago de activación enviado. Por favor espera la confirmación de tu transacción antes de usar tu nueva billetera.", "create_wallet_account_handle_unavailable_modal_title": "Alias de cuenta no disponible", "create_wallet_account_handle_unavailable_modal_message": "Tu alias de cuenta elegido, %s, no esta disponible en este momento. Por favor utiliza uno distinto para continuar.", "create_wallet_account_metadata_name": "Red %s", @@ -265,7 +264,7 @@ "create_wallet_account_metadata_notes": "Esta transacción pagó la activación de tu cartera %s. Por favor espera a tener una confirmación de esta transacción antes de utilizar tu nueva cartera %s. Para problemas relacionados con la activación de la cartera por favor escribe %s", "create_wallet_account_unfinished_activation_title": "Cartera no activada", "create_wallet_account_unfinished_activation_message": "Para completar la activación de esta cartera %s, por favor elige un nombre de cuenta único y completa el pago de activación. Si ya has hecho un pago de activación, por favor espera a que ese pago se haya confirmado antes de intentar utilizar esta cartera.", - "cryptocurrency": "Cryptocurrency", + "cryptocurrency": "Criptomoneda", "activate_wallet_token_transaction_name_category_generic": "Activación del token", "activate_wallet_token_transaction_notes_generic": "Activar transacción de token", "activate_wallet_token_transaction_name_xrp": "Libro contable de XRP", From 2f470873baa17aaa63afdceb1f69354b7ded5591 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 10 Dec 2025 10:54:19 -0800 Subject: [PATCH 03/77] New translations enus.json (German) --- src/locales/strings/de.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/locales/strings/de.json b/src/locales/strings/de.json index 25d62f58a22..bc5c9e4c51d 100644 --- a/src/locales/strings/de.json +++ b/src/locales/strings/de.json @@ -82,7 +82,6 @@ "warning_scam_message": "Has anyone helped you set up this account?", "warning_scam_message_no_1s": "Great, if you ever have any questions, please reach out to our support team at %1$s.", "warning_scam_message_yes_1s": "Please proceed with caution! Assistance with account creation has the potential for fraud. Users should never share passwords or private keys. Social media and chat platforms have been involved in attacks. Do not send cryptocurrency to strangers. If you believe you're being taken advantage of, please contact our support team at %1$s.", - "warning_token_code_override_2s": "The entered contract address differs from the contract address of built-in token %1$s. Please proceed with caution and verify the contract is legitimate as use of this token can result in loss of funds. If you have questions about this feature or contract please contact %2$s.", "warning_token_exists_1s": "The entered token already exists as a built-in token %1$s", "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", From 592e635647c720096c5298a809d1ca3b73f6c62e Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 10 Dec 2025 10:54:20 -0800 Subject: [PATCH 04/77] New translations enus.json (Italian) --- src/locales/strings/it.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/locales/strings/it.json b/src/locales/strings/it.json index 24bac7beb21..f7310e513ff 100644 --- a/src/locales/strings/it.json +++ b/src/locales/strings/it.json @@ -82,7 +82,6 @@ "warning_scam_message": "Qualcuno ti ha aiutato a configurare questo account?", "warning_scam_message_no_1s": "Grandioso! Se mai dovessi avere domande, contatta il nostro team di supporto qui %1$s.", "warning_scam_message_yes_1s": "Per favore, procedi con cautela! Ricevere assistenza durante la creazione dell'account potrebbe portare a delle truffe. Gli utenti non dovrebbero mai condividere la password o le chiavi private. I social media e le piattaforme di chat sono mezzi utilizzati spesso dai truffatori. Non inviare criptovalute agli sconosciuti. Se hai anche solo il minimo sospetto di essere vittima di un tentativo di truffa, contatta il nostro team di supporto qui %1$s.", - "warning_token_code_override_2s": "L'indirizzo del contratto inserito differisce dall'indirizzo del contratto del token integrato %1$s. Si prega di procedere con cautela e verificare che il contratto sia legittimo in quanto l'uso di questo token può comportare la perdita di fondi. Se hai domande su questa funzione o sul contratto contatta %2$s.", "warning_token_exists_1s": "Il token inserito esiste già come token integrato %1$s", "warning_battery_saver": "Risparmio batteria rilevato! I bilanci potrebbero non essere aggiornati. Per una migliore esperienza, si prega di disattivare la modalità risparmio batteria.", "warning_sending_pix_to_email_title": "Invio pagamento PIX all'indirizzo email", From 4f169417ae7ccf4e08966a275bc2396ebd44a487 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 10 Dec 2025 10:54:22 -0800 Subject: [PATCH 05/77] New translations enus.json (Japanese) --- src/locales/strings/ja.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/locales/strings/ja.json b/src/locales/strings/ja.json index 4c1be53a207..edb968b9dc3 100644 --- a/src/locales/strings/ja.json +++ b/src/locales/strings/ja.json @@ -82,7 +82,6 @@ "warning_scam_message": "Has anyone helped you set up this account?", "warning_scam_message_no_1s": "Great, if you ever have any questions, please reach out to our support team at %1$s.", "warning_scam_message_yes_1s": "Please proceed with caution! Assistance with account creation has the potential for fraud. Users should never share passwords or private keys. Social media and chat platforms have been involved in attacks. Do not send cryptocurrency to strangers. If you believe you're being taken advantage of, please contact our support team at %1$s.", - "warning_token_code_override_2s": "The entered contract address differs from the contract address of built-in token %1$s. Please proceed with caution and verify the contract is legitimate as use of this token can result in loss of funds. If you have questions about this feature or contract please contact %2$s.", "warning_token_exists_1s": "The entered token already exists as a built-in token %1$s", "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", From 861333b9a44bc1fd537de6b99dfda2949d0d7988 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 10 Dec 2025 10:54:23 -0800 Subject: [PATCH 06/77] New translations enus.json (Korean) --- src/locales/strings/ko.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/locales/strings/ko.json b/src/locales/strings/ko.json index 16588b20120..f750bd7ea9d 100644 --- a/src/locales/strings/ko.json +++ b/src/locales/strings/ko.json @@ -82,7 +82,6 @@ "warning_scam_message": "Has anyone helped you set up this account?", "warning_scam_message_no_1s": "Great, if you ever have any questions, please reach out to our support team at %1$s.", "warning_scam_message_yes_1s": "Please proceed with caution! Assistance with account creation has the potential for fraud. Users should never share passwords or private keys. Social media and chat platforms have been involved in attacks. Do not send cryptocurrency to strangers. If you believe you're being taken advantage of, please contact our support team at %1$s.", - "warning_token_code_override_2s": "The entered contract address differs from the contract address of built-in token %1$s. Please proceed with caution and verify the contract is legitimate as use of this token can result in loss of funds. If you have questions about this feature or contract please contact %2$s.", "warning_token_exists_1s": "The entered token already exists as a built-in token %1$s", "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", From 2ca415b45f03a1ab83bfe78a328e970010343af3 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 10 Dec 2025 10:54:24 -0800 Subject: [PATCH 07/77] New translations enus.json (Portuguese) --- src/locales/strings/pt.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/locales/strings/pt.json b/src/locales/strings/pt.json index 7e2d9b4242b..081eb2c853d 100644 --- a/src/locales/strings/pt.json +++ b/src/locales/strings/pt.json @@ -82,7 +82,6 @@ "warning_scam_message": "Has anyone helped you set up this account?", "warning_scam_message_no_1s": "Great, if you ever have any questions, please reach out to our support team at %1$s.", "warning_scam_message_yes_1s": "Please proceed with caution! Assistance with account creation has the potential for fraud. Users should never share passwords or private keys. Social media and chat platforms have been involved in attacks. Do not send cryptocurrency to strangers. If you believe you're being taken advantage of, please contact our support team at %1$s.", - "warning_token_code_override_2s": "O endereço do contrato informado difere do endereço do contrato do token %1$sincorporado. Proceda com cuidado e verifique se o contrato é legítimo, já que o uso desse token pode resultar em perda de fundos. Se você tiver dúvidas sobre este recurso ou contrato, entre em contato com %2$s.", "warning_token_exists_1s": "O token informado já existe como um token interno %1$s", "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", From f83d58b8941f4b411baff7ad1f7432bbecd70a48 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 10 Dec 2025 10:54:26 -0800 Subject: [PATCH 08/77] New translations enus.json (Russian) --- src/locales/strings/ru.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/locales/strings/ru.json b/src/locales/strings/ru.json index 45906e0399e..9f3a8d57dd1 100644 --- a/src/locales/strings/ru.json +++ b/src/locales/strings/ru.json @@ -82,7 +82,6 @@ "warning_scam_message": "Has anyone helped you set up this account?", "warning_scam_message_no_1s": "Great, if you ever have any questions, please reach out to our support team at %1$s.", "warning_scam_message_yes_1s": "Please proceed with caution! Assistance with account creation has the potential for fraud. Users should never share passwords or private keys. Social media and chat platforms have been involved in attacks. Do not send cryptocurrency to strangers. If you believe you're being taken advantage of, please contact our support team at %1$s.", - "warning_token_code_override_2s": "The entered contract address differs from the contract address of built-in token %1$s. Please proceed with caution and verify the contract is legitimate as use of this token can result in loss of funds. If you have questions about this feature or contract please contact %2$s.", "warning_token_exists_1s": "The entered token already exists as a built-in token %1$s", "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", From b852beecb8e3040e89ac728b35ba73db7d36198a Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 10 Dec 2025 10:54:27 -0800 Subject: [PATCH 09/77] New translations enus.json (Chinese Simplified) --- src/locales/strings/zh.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/locales/strings/zh.json b/src/locales/strings/zh.json index 3f607fba5a9..ae0270975d2 100644 --- a/src/locales/strings/zh.json +++ b/src/locales/strings/zh.json @@ -82,7 +82,6 @@ "warning_scam_message": "Has anyone helped you set up this account?", "warning_scam_message_no_1s": "Great, if you ever have any questions, please reach out to our support team at %1$s.", "warning_scam_message_yes_1s": "Please proceed with caution! Assistance with account creation has the potential for fraud. Users should never share passwords or private keys. Social media and chat platforms have been involved in attacks. Do not send cryptocurrency to strangers. If you believe you're being taken advantage of, please contact our support team at %1$s.", - "warning_token_code_override_2s": "The entered contract address differs from the contract address of built-in token %1$s. Please proceed with caution and verify the contract is legitimate as use of this token can result in loss of funds. If you have questions about this feature or contract please contact %2$s.", "warning_token_exists_1s": "The entered token already exists as a built-in token %1$s", "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", From b75c0d368c661fb7b6e2db9bd531782705133082 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 10 Dec 2025 10:54:28 -0800 Subject: [PATCH 10/77] New translations enus.json (Vietnamese) --- src/locales/strings/vi.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/locales/strings/vi.json b/src/locales/strings/vi.json index ea1a999f870..cd0e1b193cc 100644 --- a/src/locales/strings/vi.json +++ b/src/locales/strings/vi.json @@ -82,7 +82,6 @@ "warning_scam_message": "Has anyone helped you set up this account?", "warning_scam_message_no_1s": "Great, if you ever have any questions, please reach out to our support team at %1$s.", "warning_scam_message_yes_1s": "Please proceed with caution! Assistance with account creation has the potential for fraud. Users should never share passwords or private keys. Social media and chat platforms have been involved in attacks. Do not send cryptocurrency to strangers. If you believe you're being taken advantage of, please contact our support team at %1$s.", - "warning_token_code_override_2s": "The entered contract address differs from the contract address of built-in token %1$s. Please proceed with caution and verify the contract is legitimate as use of this token can result in loss of funds. If you have questions about this feature or contract please contact %2$s.", "warning_token_exists_1s": "The entered token already exists as a built-in token %1$s", "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", From f2ab779a4e3fa75564c4dc83237e37c465c0817e Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 10 Dec 2025 10:54:30 -0800 Subject: [PATCH 11/77] New translations enus.json (Spanish, Mexico) --- src/locales/strings/esMX.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/locales/strings/esMX.json b/src/locales/strings/esMX.json index 28db19fc22e..00501eda9b6 100644 --- a/src/locales/strings/esMX.json +++ b/src/locales/strings/esMX.json @@ -82,7 +82,6 @@ "warning_scam_message": "¿Alguien te ha ayudado a configurar esta cuenta?", "warning_scam_message_no_1s": "Genial, si alguna vez tiene alguna pregunta, comuníquese con nuestro equipo de soporte al %1$s.", "warning_scam_message_yes_1s": "¡Procede con precaución! La asistencia en la creación de cuentas puede dar lugar a fraude. Los usuarios nunca deben compartir contraseñas ni claves privadas. Las redes sociales y las plataformas de chat han estado involucradas en ataques. No envíes criptomonedas a desconocidos. Si crees que se están aprovechando de ti, por favor comunícate con nuestro equipo de soporte en %1$s.", - "warning_token_code_override_2s": "La dirección del contrato ingresada difiere de la dirección del contrato del token integrado %1$s. Por favor. Proceda con precaución y verifique que el contrato sea legítimo, ya que el uso de este token puede provocar la pérdida de fondos. Si tiene preguntas sobre esta función o contrato, comuníquese con %2$s.", "warning_token_exists_1s": "El token ingresado ya existe como token integrado %1$s", "warning_battery_saver": "¡Se detectó ahorro de batería! Es posible que los saldos no se actualicen. Para disfrutar de la mejor experiencia, desactive el modo de ahorro de batería.", "warning_sending_pix_to_email_title": "Envío pago de PIX a dirección de correo electrónico", @@ -139,8 +138,8 @@ "request_xrp_minimum_notification_alert_body_1xrp": "Esta billetera siempre requerirá un mínimo de 1 XRP", "request_xlm_minimum_notification_body": "Las billeteras Stellar (XLM) requieren un saldo mínimo de 1 XLM. Debes depositar al menos 1 XLM a esta dirección antes de que esta billetera muestre un saldo o transacciones. 1 XLM será intransferible durante toda la vida útil de esta dirección de billetera.", "request_xlm_minimum_notification_alert_body": "Esta billetera siempre requerirá un mínimo de 1 XRP", - "request_dot_minimum_notification_body_1": "Polkadot (DOT) wallets require a 0.01 DOT minimum balance. You must deposit at least 0.01 DOT to this address before this wallet will be activated.", - "request_dot_minimum_notification_alert_body_1": "This wallet will always require a 0.01 DOT minimum", + "request_dot_minimum_notification_body_1": "Las billeteras de Polkadot (DOT) requieren un saldo mínimo de 0.01 DOT. Debes depositar al menos 0.01 DOT en esta dirección antes de que esta billetera se active.", + "request_dot_minimum_notification_alert_body_1": "Esta billetera siempre requerirá un saldo mínimo de 0.01 DOT", "request_lld_minimum_notification_body": "Para que las billeteras Liberland (LLD) funcionen requieren un saldo mínimo de 1 LLD. Debe depositar al menos 1 LLD en esta dirección antes de que esta billetera muestre un saldo o transacciones. 1 LLD serán inutilizables de por vida en la dirección de esta billetera.", "request_lld_minimum_notification_alert_body": "Esta billetera siempre requerirá un mínimo de 1 LLD", "fragment_send_address": "Dirección", @@ -207,7 +206,7 @@ "wallet_list_referral_link_ask_wallet_creation_1s": "Necesitas una billetera %1$s para esta promoción. ¿Te gustaría crear una?", "wallet_list_wallet_search": "Buscar billeteras", "compromised_key_label": "Lave comprometida", - "create_new_account": "Create New Account", + "create_new_account": "Crear nueva cuenta", "create_wallet_choice_new_button": "Crear nueva billetera", "create_wallet_choice_new_button_fragment": "Nuevo monedero", "create_wallet_select_wallet_for_assets": "Seleccione la billetera a la que le gustaría agregar los siguientes activos: %s", @@ -215,8 +214,8 @@ "create_wallet_import_options_title": "Opciones de importación", "create_wallet_import_options_birthday_height": "Altura de inicio de la billetera", "create_wallet_import_options_birthday_height_description": "La altura del aniversario es la altura del bloque de red desde la que su billetera comenzará a sincronizarse.", - "create_wallet_import_options_passphrase": "Seed passphrase", - "create_wallet_import_options_passphrase_description": "A passphrase is an optional extra word or phrase you add to your recovery seed.", + "create_wallet_import_options_passphrase": "Frase de contraseña de la semilla", + "create_wallet_import_options_passphrase_description": "Una frase de contraseña es una palabra o frase adicional opcional que añades a tu semilla de recuperación.", "create_wallet_imports_title": "Importar monedero", "create_wallet_import_all_instructions": "Ingrese su frase semilla privada, clave privada o clave activa para verificar y restaurar la billetera asociada", "create_wallet_import_instructions": "Introduce tu semilla privada para verificar y restaurar la billetera asociada", From 33cc146b01a948c52cf7a6b9ef298499d45d98c9 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 10 Dec 2025 10:54:31 -0800 Subject: [PATCH 12/77] New translations enus.json (Karakalpak) --- src/locales/strings/kaa.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/locales/strings/kaa.json b/src/locales/strings/kaa.json index 79e62c28997..a58593e0eca 100644 --- a/src/locales/strings/kaa.json +++ b/src/locales/strings/kaa.json @@ -82,7 +82,6 @@ "warning_scam_message": "Has anyone helped you set up this account?", "warning_scam_message_no_1s": "Great, if you ever have any questions, please reach out to our support team at %1$s.", "warning_scam_message_yes_1s": "Please proceed with caution! Assistance with account creation has the potential for fraud. Users should never share passwords or private keys. Social media and chat platforms have been involved in attacks. Do not send cryptocurrency to strangers. If you believe you're being taken advantage of, please contact our support team at %1$s.", - "warning_token_code_override_2s": "The entered contract address differs from the contract address of built-in token %1$s. Please proceed with caution and verify the contract is legitimate as use of this token can result in loss of funds. If you have questions about this feature or contract please contact %2$s.", "warning_token_exists_1s": "The entered token already exists as a built-in token %1$s", "warning_battery_saver": "Battery Saver Detected! Balances may not update. For the best experience, please turn off battery saver mode.", "warning_sending_pix_to_email_title": "Sending PIX payment to email address", From 9f400fb620398ea8caaa2d207cfe288212f5a900 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 10 Dec 2025 15:06:46 -0800 Subject: [PATCH 13/77] Upgrade edge-exchange-plugins@^2.40.2 --- ios/Podfile.lock | 4 ++-- package.json | 2 +- yarn.lock | 54 +++++++----------------------------------------- 3 files changed, 11 insertions(+), 49 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ce646ad0e81..c8762b2dd4b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -21,7 +21,7 @@ PODS: - React-Core - edge-currency-plugins (3.8.9): - React-Core - - edge-exchange-plugins (2.40.1): + - edge-exchange-plugins (2.40.2): - React-Core - edge-login-ui-rn (3.34.6): - React-Core @@ -3336,7 +3336,7 @@ SPEC CHECKSUMS: edge-core-js: 248f7d28942a5ea6c9835eca6f9f16969c89476c edge-currency-accountbased: 993920e46f000e04df92d0a49eabb57973096d1c edge-currency-plugins: 0d8a1a8da63672342cbc9bd5055feb4b397544e7 - edge-exchange-plugins: b92baace286dd8ed8a7a6672e1d0172f04a91357 + edge-exchange-plugins: f35930ddcd5a4551a6e45334cb3f4c0295c23acd edge-login-ui-rn: c9648a772533c092f4526a189cd4da9d6f729639 EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8 Expo: 43d9e0c3108cc3a1c2739743e9b51086144ee4b0 diff --git a/package.json b/package.json index c02dbfe3a9f..14a951bb1c6 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "edge-currency-accountbased": "^4.67.0", "edge-currency-monero": "^2.0.1", "edge-currency-plugins": "^3.8.9", - "edge-exchange-plugins": "^2.40.1", + "edge-exchange-plugins": "^2.40.2", "edge-info-server": "^3.10.0", "edge-login-ui-rn": "^3.34.6", "ethers": "^5.7.2", diff --git a/yarn.lock b/yarn.lock index f6633e01b76..15d7e5dcbb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1063,20 +1063,7 @@ "@babel/parser" "^7.27.2" "@babel/types" "^7.27.1" -"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.0.tgz#518aa113359b062042379e333db18380b537e34b" - integrity sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg== - dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.0" - "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.0" - "@babel/template" "^7.27.2" - "@babel/types" "^7.28.0" - debug "^4.3.1" - -"@babel/traverse@^7.25.3", "@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3", "@babel/traverse@^7.28.0", "@babel/traverse@^7.7.0": +"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3", "@babel/traverse@^7.25.3", "@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3", "@babel/traverse@^7.28.0", "@babel/traverse@^7.7.0": version "7.28.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.0.tgz#518aa113359b062042379e333db18380b537e34b" integrity sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg== @@ -9563,10 +9550,10 @@ edge-currency-plugins@^3.8.9: wifgrs "^2.0.6" ws "^7.4.6" -edge-exchange-plugins@^2.40.1: - version "2.40.1" - resolved "https://registry.yarnpkg.com/edge-exchange-plugins/-/edge-exchange-plugins-2.40.1.tgz#8ea938e06f65a7b9c2b01a59d6f741e7ee7c03d8" - integrity sha512-yx0Ehg9NUxPB7dyoAGmf6MWX7DSit9YcVWv28GX2dsgAb+RDBWkEw1+4a59mN/px+wFv2xtO+6sT2b/Ru32ndg== +edge-exchange-plugins@^2.40.2: + version "2.40.2" + resolved "https://registry.yarnpkg.com/edge-exchange-plugins/-/edge-exchange-plugins-2.40.2.tgz#2725781ac522cf7676da7f5f208c0eb7c022200f" + integrity sha512-bMF4I68nKVNVw46pTy2Ul9uOrP3Bb6wJRqUAoVt3yiEzIQA1SLX2FNVgj5lEXDXHXjLQnLJGgo879z4AQfIv/Q== dependencies: "@cosmjs/encoding" "^0.32.2" "@scure/base" "^1.2.6" @@ -17621,16 +17608,7 @@ string-length@^4.0.2: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -17730,7 +17708,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -17744,13 +17722,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.0, strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -19459,7 +19430,7 @@ wordwrapjs@^4.0.0: reduce-flatten "^2.0.0" typical "^5.2.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -19477,15 +19448,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From b3705402495f96c1fee1d466673175cd3665815e Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 4 Dec 2025 15:48:55 -0800 Subject: [PATCH 14/77] Fix fading to avoid white empty white square --- src/components/themed/QrCode.tsx | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/components/themed/QrCode.tsx b/src/components/themed/QrCode.tsx index b8b13f4ff69..ea6df21c965 100644 --- a/src/components/themed/QrCode.tsx +++ b/src/components/themed/QrCode.tsx @@ -9,7 +9,7 @@ import { } from 'react-native' import Animated, { useAnimatedStyle, - useDerivedValue, + useSharedValue, withTiming } from 'react-native-reanimated' import Svg, { Path } from 'react-native-svg' @@ -42,6 +42,8 @@ export const QrCode: React.FC = props => { // Scale the surface to match the container's size: const [size, setSize] = React.useState(0) + const layoutPending = size <= 0 || data == null // loading state includes layout timing to avoid flicker + const handleLayout = (event: LayoutChangeEvent): void => { setSize(event.nativeEvent.layout.height) } @@ -54,9 +56,14 @@ export const QrCode: React.FC = props => { const path = svg.replace(/.*d="([^"]*)".*/, '$1') // Handle animation: - const derivedData = useDerivedValue(() => data) + const opacity = useSharedValue(0) + + React.useEffect(() => { + opacity.value = withTiming(data != null ? 1 : 0) + }, [data, opacity]) + const fadeStyle = useAnimatedStyle(() => ({ - opacity: withTiming(derivedData.value != null ? 1 : 0) + opacity: opacity.value })) // Create a drawing transform to scale QR cells to device pixels: @@ -93,17 +100,18 @@ export const QrCode: React.FC = props => { return ( - - - {size <= 0 ? null : ( + {layoutPending ? ( + + ) : ( + - )} - {icon} - + {icon} + + )} ) From bdddef0df158ab04fba45de4645bad8c96e523c9 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Thu, 4 Dec 2025 12:15:56 -0800 Subject: [PATCH 15/77] Fix Paybis sell for US users --- src/plugins/ramps/paybis/paybisRampPlugin.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/plugins/ramps/paybis/paybisRampPlugin.ts b/src/plugins/ramps/paybis/paybisRampPlugin.ts index 562d3d007ff..102409b3d89 100644 --- a/src/plugins/ramps/paybis/paybisRampPlugin.ts +++ b/src/plugins/ramps/paybis/paybisRampPlugin.ts @@ -342,7 +342,7 @@ const PAYMENT_METHOD_MAP: Record = { 'method-id-trustly': 'ach', 'method-id-credit-card': 'credit', 'method-id-credit-card-out': 'credit', - 'method-id-mass-pay-out': 'ach', + 'method-id-mass-pay-out': 'credit', // US version of credit card payout 'method-id_bridgerpay_revolutpay': 'revolut', 'fake-id-googlepay': 'googlepay', 'fake-id-applepay': 'applepay', @@ -354,6 +354,11 @@ const PAYMENT_METHOD_MAP: Record = { 'method-id_bridgerpay_directa24_pix_payout': 'pix' } +// TODO: Deprecate REVERSE_PAYMENT_METHOD_MAP and +// SELL_REVERSE_PAYMENT_METHOD_MAP. Instead, dynamically generate the reverse +// map(s) from PAYMENT_METHOD_MAP within fetchQuotes. This is necessary due +// to the one-to-many relationship between edge payment types and Paybis +// payment methods. Mapping is based on the user's IP/region parameters. const REVERSE_PAYMENT_METHOD_MAP: Partial< Record > = { @@ -366,11 +371,9 @@ const REVERSE_PAYMENT_METHOD_MAP: Partial< revolut: 'method-id_bridgerpay_revolutpay', spei: 'method-id_bridgerpay_directa24_spei' } - const SELL_REVERSE_PAYMENT_METHOD_MAP: Partial< Record > = { - ach: 'method-id-mass-pay-out', credit: 'method-id-credit-card-out', colombiabank: 'method-id_bridgerpay_directa24_colombia_payout', mexicobank: 'method-id_bridgerpay_directa24_mexico_payout', @@ -472,6 +475,9 @@ export const paybisRampPlugin: RampPluginFactory = ( let state: PaybisPluginState | undefined const paybisPairs: PaybisPairs = { buy: undefined, sell: undefined } let userIdHasTransactions: boolean | undefined + // Store actual payout method IDs from API response (varies by user's IP/region) + const sellPayoutMethodIds: Partial> = + {} const allowedCurrencyCodes: Record< FiatDirection, Partial> @@ -561,6 +567,8 @@ export const paybisRampPlugin: RampPluginFactory = ( if (name == null) continue const edgePaymentType = PAYMENT_METHOD_MAP[name] if (edgePaymentType == null) continue + // Store the actual method ID from API (varies by region/IP) + sellPayoutMethodIds[edgePaymentType] = name for (const pair of pairs) { const { fromAssetId, to } = pair @@ -833,10 +841,13 @@ export const paybisRampPlugin: RampPluginFactory = ( if (!constraintOk) continue try { + // For sell, prefer the method ID from API response (varies by region IP) + // Fallback to hardcoded map for backwards compatibility const paymentMethod = direction === 'buy' ? REVERSE_PAYMENT_METHOD_MAP[paymentType] - : SELL_REVERSE_PAYMENT_METHOD_MAP[paymentType] + : sellPayoutMethodIds[paymentType] ?? + SELL_REVERSE_PAYMENT_METHOD_MAP[paymentType] if (paymentMethod == null) continue // Skip unsupported payment types From c92ea7a2cfff7176f7f11cd1cca202bc476e706c Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Fri, 5 Dec 2025 11:28:59 -0800 Subject: [PATCH 16/77] Only include `oneTimeToken` on first webview open Per Paybis team feedback, this prevents an invalid session that triggers a new email verification check. --- src/plugins/ramps/paybis/paybisRampPlugin.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/plugins/ramps/paybis/paybisRampPlugin.ts b/src/plugins/ramps/paybis/paybisRampPlugin.ts index 102409b3d89..743dc966860 100644 --- a/src/plugins/ramps/paybis/paybisRampPlugin.ts +++ b/src/plugins/ramps/paybis/paybisRampPlugin.ts @@ -1226,11 +1226,19 @@ export const paybisRampPlugin: RampPluginFactory = ( const successReturnURL = encodeURIComponent(RETURN_URL_SUCCESS) const failureReturnURL = encodeURIComponent(RETURN_URL_FAIL) - const webviewUrl = `${widgetUrl}?requestId=${requestId}&successReturnURL=${successReturnURL}&failureReturnURL=${failureReturnURL}${ott}${promoCodeParam}` - console.log(`webviewUrl: ${webviewUrl}`) + const baseWebviewUrl = `${widgetUrl}?requestId=${requestId}&successReturnURL=${successReturnURL}&failureReturnURL=${failureReturnURL}${promoCodeParam}` + console.log(`baseWebviewUrl: ${baseWebviewUrl}`) let inPayment = false + let isFirstOpen = true const openWebView = async (): Promise => { + // Only include oneTimeToken on the first open. Subsequent opens + // with the same requestId must omit it per Paybis API requirements. + const webviewUrl = isFirstOpen + ? `${baseWebviewUrl}${ott}` + : baseWebviewUrl + isFirstOpen = false + navigation.navigate('guiPluginWebView', { url: webviewUrl, // No pending promise to resolve From ffd9294fcd4b092de911f2f595247fb278a8bd7f Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 11 Dec 2025 14:05:09 -0800 Subject: [PATCH 17/77] Re-enable UFO coin --- CHANGELOG.md | 1 + src/constants/WalletAndCurrencyConstants.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 308691258ec..ca32240e515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## 4.41.0 (staging) - changed: Replace `currencyCode` usage with `EdgeTokenId` throughout the app +- changed: (UFO) Re-enable UFO - changed: Update translations ## 4.40.0 (staging) diff --git a/src/constants/WalletAndCurrencyConstants.ts b/src/constants/WalletAndCurrencyConstants.ts index 9c5d37badf2..73764ca0044 100644 --- a/src/constants/WalletAndCurrencyConstants.ts +++ b/src/constants/WalletAndCurrencyConstants.ts @@ -716,8 +716,7 @@ export const SPECIAL_CURRENCY_INFO: Record = { ufo: { maxSpendTargets: UTXO_MAX_SPEND_TARGETS, initWalletName: lstrings.string_first_ufo_wallet_name, - isImportKeySupported: true, - keysOnlyMode: true + isImportKeySupported: true }, fantom: { initWalletName: lstrings.string_first_fantom_wallet_name, From 557721134c382f7abfdb67af7930563a24752e5b Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Tue, 2 Dec 2025 16:23:05 -0800 Subject: [PATCH 18/77] Add multi-word search and chainDisplayName to wallet search Split search queries by spaces so users can search multiple terms like 'base eth' to find Ethereum on Base network. All terms must match at least one searchable field (AND logic). Add chainDisplayName as a searchable field in searchWalletList to enable searching by network/chain name. --- src/components/services/SortedWalletList.ts | 28 +++++++--- src/selectors/getCreateWalletList.ts | 58 +++++++++++++-------- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/components/services/SortedWalletList.ts b/src/components/services/SortedWalletList.ts index 331bb1b073f..d0e9cfdd165 100644 --- a/src/components/services/SortedWalletList.ts +++ b/src/components/services/SortedWalletList.ts @@ -35,7 +35,7 @@ type EnabledTokenIds = Record * so we make that as fast as possible by using good data structures * and tight code. */ -export function SortedWalletList(props: Props) { +export const SortedWalletList: React.FC = (props: Props) => { const { account } = props // Subscribe to everything that affects the list ordering: @@ -263,6 +263,7 @@ function matchWalletList(a: WalletListItem[], b: WalletListItem[]): boolean { /** * Filters a wallet list using a search string. + * Supports multi-word search where each word must match at least one field. */ export function searchWalletList( list: WalletListItem[], @@ -270,7 +271,13 @@ export function searchWalletList( ): WalletListItem[] { if (searchText === '') return list - const target = normalizeForSearch(searchText) + // Split search text into individual terms (space-delimited), then normalize + const searchTerms = searchText + .split(/\s+/) + .filter(term => term.length > 0) + + if (searchTerms.length === 0) return list + return list.filter(item => { // Eliminate loading wallets in search mode: if (item.type !== 'asset') return false @@ -278,15 +285,20 @@ export function searchWalletList( // Grab wallet and token information: const { currencyCode, displayName } = token ?? wallet.currencyInfo + const { chainDisplayName } = wallet.currencyInfo const name = getWalletName(wallet) const contractAddress = token?.networkLocation?.contractAddress ?? '' - return ( - normalizeForSearch(currencyCode).includes(target) || - normalizeForSearch(displayName).includes(target) || - normalizeForSearch(name).includes(target) || - normalizeForSearch(contractAddress).includes(target) - ) + // All search terms must match at least one field (AND logic) + return searchTerms.every(term => { + return ( + normalizeForSearch(currencyCode).includes(term) || + normalizeForSearch(displayName).includes(term) || + normalizeForSearch(chainDisplayName).includes(term) || + normalizeForSearch(name).includes(term) || + normalizeForSearch(contractAddress).includes(term) + ) + }) }) } diff --git a/src/selectors/getCreateWalletList.ts b/src/selectors/getCreateWalletList.ts index a441776030d..fbde8a642cb 100644 --- a/src/selectors/getCreateWalletList.ts +++ b/src/selectors/getCreateWalletList.ts @@ -215,12 +215,23 @@ export const getCreateWalletList = ( return walletList } +/** + * Filters a wallet create item list using a search string. + * Supports multi-word search where each word must match at least one field. + */ export const filterWalletCreateItemListBySearchText = ( createWalletList: WalletCreateItem[], searchText: string ): WalletCreateItem[] => { + // Split search text into individual terms (space-delimited), then normalize + const searchTerms = searchText + .split(/\s+/) + .map(term => normalizeForSearch(term)) + .filter(term => term.length > 0) + + if (searchTerms.length === 0) return createWalletList + const out: WalletCreateItem[] = [] - const searchTarget = normalizeForSearch(searchText) for (const item of createWalletList) { const { currencyCode, @@ -229,30 +240,33 @@ export const filterWalletCreateItemListBySearchText = ( pluginId, walletType } = item - if ( - normalizeForSearch(currencyCode).includes(searchTarget) || - normalizeForSearch(displayName).includes(searchTarget) - ) { - out.push(item) - continue - } - // Do an additional search for pluginId for mainnet create items - if ( - walletType != null && - normalizeForSearch(pluginId).includes(searchTarget) - ) { - out.push(item) - continue - } - // See if the search term can be found in the networkLocation object ie. contractAddress - for (const value of Object.values(networkLocation)) { + + // Check if all search terms match at least one field (AND logic) + const allTermsMatch = searchTerms.every(term => { if ( - typeof value === 'string' && - normalizeForSearch(value).includes(searchTarget) + normalizeForSearch(currencyCode).includes(term) || + normalizeForSearch(displayName).includes(term) ) { - out.push(item) - break + return true + } + // Search pluginId for mainnet create items + if (walletType != null && normalizeForSearch(pluginId).includes(term)) { + return true + } + // Search networkLocation values (ie. contractAddress) + for (const value of Object.values(networkLocation)) { + if ( + typeof value === 'string' && + normalizeForSearch(value).includes(term) + ) { + return true + } } + return false + }) + + if (allTermsMatch) { + out.push(item) } } return out From c5cfb08808d0e5b08469db71169d6f004e3707ff Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Tue, 2 Dec 2025 16:25:38 -0800 Subject: [PATCH 19/77] Add assetDisplayName as searchable field in wallet search Include assetDisplayName from currencyInfo as a searchable field for wallet and create-wallet searches. This allows users to search by the asset's display name (e.g., 'Ethereum') in addition to other existing searchable fields. --- .../CreateWalletSelectCryptoScene.test.tsx.snap | 1 + src/components/services/SortedWalletList.ts | 3 ++- src/selectors/getCreateWalletList.ts | 15 ++++++++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap index 071b593a98c..4002fdea69b 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap @@ -824,6 +824,7 @@ exports[`CreateWalletSelectCrypto should render with loading props 1`] = ` data={ [ { + "assetDisplayName": "Ethereum", "currencyCode": "ETH", "displayName": "Ethereum", "key": "create-wallet:ethereum-ethereum", diff --git a/src/components/services/SortedWalletList.ts b/src/components/services/SortedWalletList.ts index d0e9cfdd165..1967fb8b1fc 100644 --- a/src/components/services/SortedWalletList.ts +++ b/src/components/services/SortedWalletList.ts @@ -285,7 +285,7 @@ export function searchWalletList( // Grab wallet and token information: const { currencyCode, displayName } = token ?? wallet.currencyInfo - const { chainDisplayName } = wallet.currencyInfo + const { assetDisplayName, chainDisplayName } = wallet.currencyInfo const name = getWalletName(wallet) const contractAddress = token?.networkLocation?.contractAddress ?? '' @@ -295,6 +295,7 @@ export function searchWalletList( return ( normalizeForSearch(currencyCode).includes(term) || normalizeForSearch(displayName).includes(term) || + normalizeForSearch(assetDisplayName).includes(term) || normalizeForSearch(chainDisplayName).includes(term) || normalizeForSearch(name).includes(term) || normalizeForSearch(contractAddress).includes(term) diff --git a/src/selectors/getCreateWalletList.ts b/src/selectors/getCreateWalletList.ts index fbde8a642cb..b462110000a 100644 --- a/src/selectors/getCreateWalletList.ts +++ b/src/selectors/getCreateWalletList.ts @@ -25,6 +25,7 @@ export interface WalletCreateItem { walletType?: string // Used for filtering + assetDisplayName?: string networkLocation?: JsonObject } @@ -127,7 +128,7 @@ export const getCreateWalletList = ( if (filterActivation && requiresActivation(pluginId)) continue const currencyConfig = account.currencyConfig[pluginId] - const { currencyCode, displayName, walletType } = + const { assetDisplayName, currencyCode, displayName, walletType } = currencyConfig.currencyInfo if (isAllowed(pluginId, null)) @@ -135,6 +136,7 @@ export const getCreateWalletList = ( newWallets.push({ type: 'create', key: `create-${walletType}-bip49-${pluginId}`, + assetDisplayName, currencyCode, displayName: `${displayName} (Segwit)`, keyOptions: { format: 'bip49' }, @@ -145,6 +147,7 @@ export const getCreateWalletList = ( newWallets.push({ type: 'create', key: `create-${walletType}-bip44-${pluginId}`, + assetDisplayName, currencyCode, displayName: `${displayName} (no Segwit)`, keyOptions: { format: 'bip44' }, @@ -156,6 +159,7 @@ export const getCreateWalletList = ( newWallets.push({ type: 'create', key: `create-${walletType}-${pluginId}`, + assetDisplayName, currencyCode, displayName, keyOptions: {}, @@ -234,6 +238,7 @@ export const filterWalletCreateItemListBySearchText = ( const out: WalletCreateItem[] = [] for (const item of createWalletList) { const { + assetDisplayName, currencyCode, displayName, networkLocation = {}, @@ -249,6 +254,14 @@ export const filterWalletCreateItemListBySearchText = ( ) { return true } + // Search assetDisplayName for mainnet create items + if ( + walletType != null && + assetDisplayName != null && + normalizeForSearch(assetDisplayName).includes(term) + ) { + return true + } // Search pluginId for mainnet create items if (walletType != null && normalizeForSearch(pluginId).includes(term)) { return true From f32e2f4de3f2e1252af933837840a4126f9669b5 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Tue, 2 Dec 2025 16:27:09 -0800 Subject: [PATCH 20/77] Use startsWith matching for asset identification fields Change search matching for currencyCode, displayName, and assetDisplayName from includes() to startsWith(). This prevents partial substring matches that crowd search results. For example, searching 'eth' now shows Ethereum assets but not Tether (which contains 'eth' in the middle). Context fields like chainDisplayName, wallet name, and contractAddress still use includes() for broader discovery. --- src/components/services/SortedWalletList.ts | 42 +++++++++++++++------ src/selectors/getCreateWalletList.ts | 14 ++++--- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/components/services/SortedWalletList.ts b/src/components/services/SortedWalletList.ts index 1967fb8b1fc..e0a49639d62 100644 --- a/src/components/services/SortedWalletList.ts +++ b/src/components/services/SortedWalletList.ts @@ -274,6 +274,7 @@ export function searchWalletList( // Split search text into individual terms (space-delimited), then normalize const searchTerms = searchText .split(/\s+/) + .map(term => normalizeForSearch(term)) .filter(term => term.length > 0) if (searchTerms.length === 0) return list @@ -286,20 +287,37 @@ export function searchWalletList( // Grab wallet and token information: const { currencyCode, displayName } = token ?? wallet.currencyInfo const { assetDisplayName, chainDisplayName } = wallet.currencyInfo - const name = getWalletName(wallet) - const contractAddress = token?.networkLocation?.contractAddress ?? '' + // Normalize all fields (once per item, not per search term) + const normalCurrencyCode = normalizeForSearch(currencyCode) + const normalDisplayName = normalizeForSearch(displayName) + const normalName = normalizeForSearch(getWalletName(wallet)) + const normalContractAddress = normalizeForSearch( + token?.networkLocation?.contractAddress ?? '' + ) + + // Only match for mainnet assets (not tokens): + const normalAssetDisplayName = + token == null && assetDisplayName != null + ? normalizeForSearch(assetDisplayName) + : undefined + const normalChainDisplayName = + token == null && chainDisplayName != null + ? normalizeForSearch(chainDisplayName) + : undefined // All search terms must match at least one field (AND logic) - return searchTerms.every(term => { - return ( - normalizeForSearch(currencyCode).includes(term) || - normalizeForSearch(displayName).includes(term) || - normalizeForSearch(assetDisplayName).includes(term) || - normalizeForSearch(chainDisplayName).includes(term) || - normalizeForSearch(name).includes(term) || - normalizeForSearch(contractAddress).includes(term) - ) - }) + return searchTerms.every( + term => + // Asset identification fields use startsWith to avoid partial matches + // (e.g., "eth" matches "Ethereum" but not "Tether") + normalCurrencyCode.startsWith(term) || + normalDisplayName.startsWith(term) || + (normalAssetDisplayName?.startsWith(term) ?? false) || + // Context/discovery fields use includes for broader matching + normalName.includes(term) || + normalContractAddress.includes(term) || + (normalChainDisplayName?.includes(term) ?? false) + ) }) } diff --git a/src/selectors/getCreateWalletList.ts b/src/selectors/getCreateWalletList.ts index b462110000a..59a60a967ec 100644 --- a/src/selectors/getCreateWalletList.ts +++ b/src/selectors/getCreateWalletList.ts @@ -248,25 +248,27 @@ export const filterWalletCreateItemListBySearchText = ( // Check if all search terms match at least one field (AND logic) const allTermsMatch = searchTerms.every(term => { + // Asset identification fields use startsWith to avoid partial matches + // (e.g., "eth" matches "Ethereum" but not "Tether") if ( - normalizeForSearch(currencyCode).includes(term) || - normalizeForSearch(displayName).includes(term) + normalizeForSearch(currencyCode).startsWith(term) || + normalizeForSearch(displayName).startsWith(term) ) { return true } - // Search assetDisplayName for mainnet create items + // Search assetDisplayName for mainnet create items (also uses startsWith) if ( walletType != null && assetDisplayName != null && - normalizeForSearch(assetDisplayName).includes(term) + normalizeForSearch(assetDisplayName).startsWith(term) ) { return true } - // Search pluginId for mainnet create items + // Search pluginId for mainnet create items (uses includes for discovery) if (walletType != null && normalizeForSearch(pluginId).includes(term)) { return true } - // Search networkLocation values (ie. contractAddress) + // Search networkLocation values ie. contractAddress (uses includes) for (const value of Object.values(networkLocation)) { if ( typeof value === 'string' && From 536ffd0ef231cff7539273984c2041132b7daca3 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Fri, 5 Dec 2025 13:52:13 -0800 Subject: [PATCH 21/77] Add unit tests for wallet search functionality Test coverage for: - Multi-word search with space delimiter (AND logic) - startsWith matching for currencyCode, displayName, assetDisplayName - includes matching for chainDisplayName, wallet name, contractAddress - assetDisplayName and chainDisplayName only searched for mainnet assets - Regression tests for original issues (#1: 'base eth', #3: 'eth' vs Tether) --- src/__tests__/walletSearch.test.ts | 505 ++++++++++++++++++++++++++++ src/util/fake/fakeSearchTestData.ts | 91 +++++ 2 files changed, 596 insertions(+) create mode 100644 src/__tests__/walletSearch.test.ts create mode 100644 src/util/fake/fakeSearchTestData.ts diff --git a/src/__tests__/walletSearch.test.ts b/src/__tests__/walletSearch.test.ts new file mode 100644 index 00000000000..9df5582cd4c --- /dev/null +++ b/src/__tests__/walletSearch.test.ts @@ -0,0 +1,505 @@ +import { describe, expect, test } from '@jest/globals' +import type { EdgeToken } from 'edge-core-js' + +import { searchWalletList } from '../components/services/SortedWalletList' +import { filterWalletCreateItemListBySearchText } from '../selectors/getCreateWalletList' +import type { WalletListItem } from '../types/types' +import { btcCurrencyInfo } from '../util/fake/fakeBtcInfo' +import { ethCurrencyInfo } from '../util/fake/fakeEthInfo' +import { + makeTestCreateWalletItem, + makeTestCurrencyInfo, + makeTestWallet, + makeTestWalletListItem, + testTetherToken, + testWstethToken +} from '../util/fake/fakeSearchTestData' + +// ----------------------------------------------------------------------------- +// searchWalletList Tests +// ----------------------------------------------------------------------------- + +describe('searchWalletList', () => { + // Use existing fake currency infos where possible + const ethereumWallet = makeTestWallet(ethCurrencyInfo, 'My Ethereum') + const bitcoinWallet = makeTestWallet(btcCurrencyInfo, 'BTC Savings') + + // Create custom chain configurations for L2s + const baseInfo = makeTestCurrencyInfo({ + pluginId: 'base', + currencyCode: 'ETH', + displayName: 'Ethereum', + assetDisplayName: 'Ethereum', + chainDisplayName: 'Base' + }) + const baseWallet = makeTestWallet(baseInfo, 'Base L2') + + const testWalletList: WalletListItem[] = [ + makeTestWalletListItem(ethereumWallet), + makeTestWalletListItem(baseWallet), + makeTestWalletListItem(bitcoinWallet), + makeTestWalletListItem(ethereumWallet, testTetherToken), + makeTestWalletListItem(ethereumWallet, testWstethToken) + ] + + describe('empty search', () => { + test('returns all items when search is empty', () => { + const result = searchWalletList(testWalletList, '') + expect(result).toHaveLength(5) + }) + + test('returns all items when search is only whitespace', () => { + const result = searchWalletList(testWalletList, ' ') + expect(result).toHaveLength(5) + }) + }) + + describe('single word search', () => { + test('matches currencyCode from beginning (startsWith)', () => { + const result = searchWalletList(testWalletList, 'btc') + // Should match only Bitcoin (currencyCode starts with "btc") + expect(result).toHaveLength(1) + expect( + result[0].type === 'asset' && + result[0].wallet.currencyInfo.currencyCode === 'BTC' + ).toBe(true) + }) + + test('matches displayName from beginning (startsWith)', () => { + const result = searchWalletList(testWalletList, 'bit') + expect(result).toHaveLength(1) + expect( + result[0].type === 'asset' && + result[0].wallet.currencyInfo.currencyCode === 'BTC' + ).toBe(true) + }) + + test('does NOT match displayName in middle (startsWith behavior)', () => { + // "steth" is in "WSTETH" but not at start of displayName "Wrapped stETH" + const result = searchWalletList(testWalletList, 'steth') + expect(result).toHaveLength(0) + }) + + test('matches chainDisplayName anywhere (includes behavior)', () => { + const result = searchWalletList(testWalletList, 'base') + expect(result).toHaveLength(1) + expect( + result[0].type === 'asset' && + result[0].wallet.currencyInfo.pluginId === 'base' + ).toBe(true) + }) + + test('matches wallet name anywhere (includes behavior)', () => { + // Searching wallet name returns the wallet AND all its tokens + const result = searchWalletList(testWalletList, 'savings') + expect(result).toHaveLength(1) // Just BTC wallet (no tokens) + expect( + result[0].type === 'asset' && result[0].wallet.name === 'BTC Savings' + ).toBe(true) + }) + + test('wallet name search includes tokens on that wallet', () => { + // "ethereum" in wallet name "My Ethereum" matches wallet + its tokens + const result = searchWalletList(testWalletList, 'my ethereum') + expect(result).toHaveLength(3) // Mainnet + USDT + WSTETH + expect( + result.every(r => r.type === 'asset' && r.wallet.name === 'My Ethereum') + ).toBe(true) + }) + + test('matches contract address anywhere (includes behavior)', () => { + // Search for partial contract address + const result = searchWalletList(testWalletList, 'dac17f') + expect(result).toHaveLength(1) + expect( + result[0].type === 'asset' && result[0].token?.currencyCode === 'USDT' + ).toBe(true) + }) + }) + + describe('multi-word search (AND logic)', () => { + test('all words must match (base eth)', () => { + const result = searchWalletList(testWalletList, 'base eth') + // "base" matches chainDisplayName, "eth" matches currencyCode + expect(result).toHaveLength(1) + expect( + result[0].type === 'asset' && + result[0].wallet.currencyInfo.pluginId === 'base' + ).toBe(true) + }) + + test('order does not matter (eth base)', () => { + const result = searchWalletList(testWalletList, 'eth base') + expect(result).toHaveLength(1) + expect( + result[0].type === 'asset' && + result[0].wallet.currencyInfo.pluginId === 'base' + ).toBe(true) + }) + + test('returns nothing if one word does not match', () => { + const result = searchWalletList(testWalletList, 'base btc') + expect(result).toHaveLength(0) + }) + + test('multiple terms can match different fields', () => { + // "btc" matches currencyCode, "savings" matches wallet name + const result = searchWalletList(testWalletList, 'btc savings') + expect(result).toHaveLength(1) + expect( + result[0].type === 'asset' && + result[0].wallet.currencyInfo.currencyCode === 'BTC' + ).toBe(true) + }) + + test('handles multiple spaces between words', () => { + const result = searchWalletList(testWalletList, 'base eth') + expect(result).toHaveLength(1) + }) + }) + + describe('case insensitivity', () => { + test('matches regardless of case', () => { + const resultLower = searchWalletList(testWalletList, 'eth') + const resultUpper = searchWalletList(testWalletList, 'ETH') + const resultMixed = searchWalletList(testWalletList, 'EtH') + expect(resultLower).toHaveLength(resultUpper.length) + expect(resultLower).toHaveLength(resultMixed.length) + }) + }) + + describe('assetDisplayName matching', () => { + test('matches assetDisplayName from beginning for mainnet assets', () => { + // Use a wallet with a non-Ethereum name to isolate assetDisplayName matching + const isolatedInfo = makeTestCurrencyInfo({ + pluginId: 'arbitrum', + currencyCode: 'ETH', + displayName: 'Ethereum', + assetDisplayName: 'Ethereum', + chainDisplayName: 'Arbitrum' + }) + const isolatedWallet = makeTestWallet(isolatedInfo, 'Arbitrum Wallet') + const isolatedList: WalletListItem[] = [ + makeTestWalletListItem(isolatedWallet) + ] + + const result = searchWalletList(isolatedList, 'ethereum') + expect(result).toHaveLength(1) + expect(result[0].type === 'asset' && result[0].token == null).toBe(true) + }) + + test('tokens do not match via parent assetDisplayName', () => { + // Create a wallet with non-matching name to test assetDisplayName isolation + const isolatedWallet = makeTestWallet(ethCurrencyInfo, 'Savings') + const token: EdgeToken = { + currencyCode: 'USDC', + displayName: 'USD Coin', + denominations: [{ name: 'USDC', multiplier: '1000000' }], + networkLocation: { contractAddress: '0xa0b8...' } + } + const isolatedList: WalletListItem[] = [ + makeTestWalletListItem(isolatedWallet, token) + ] + + // "ethereum" should NOT match USDC token (assetDisplayName is only for mainnet) + const result = searchWalletList(isolatedList, 'ethereum') + expect(result).toHaveLength(0) + }) + }) + + describe('loading wallets', () => { + test('filters out loading wallets', () => { + const listWithLoading: WalletListItem[] = [ + ...testWalletList, + { type: 'loading', key: 'loading-1', walletId: 'wallet-loading' } + ] + const result = searchWalletList(listWithLoading, 'eth') + expect(result.every(r => r.type === 'asset')).toBe(true) + }) + }) + + describe('edge cases', () => { + test('handles special characters in search', () => { + const result = searchWalletList(testWalletList, '0x') + // Should match contract addresses + expect(result.length).toBeGreaterThan(0) + }) + + test('returns empty array when no matches', () => { + const result = searchWalletList(testWalletList, 'xyz123notfound') + expect(result).toHaveLength(0) + }) + + test('handles empty wallet list', () => { + const result = searchWalletList([], 'eth') + expect(result).toHaveLength(0) + }) + }) +}) + +// ----------------------------------------------------------------------------- +// filterWalletCreateItemListBySearchText Tests +// ----------------------------------------------------------------------------- + +describe('filterWalletCreateItemListBySearchText', () => { + const testCreateList = [ + makeTestCreateWalletItem({ + key: 'create-ethereum', + currencyCode: 'ETH', + displayName: 'Ethereum', + assetDisplayName: 'Ethereum', + pluginId: 'ethereum', + walletType: 'wallet:ethereum' + }), + makeTestCreateWalletItem({ + key: 'create-base', + currencyCode: 'ETH', + displayName: 'Base', + assetDisplayName: 'Ethereum', + pluginId: 'base', + walletType: 'wallet:base' + }), + makeTestCreateWalletItem({ + key: 'create-bitcoin', + currencyCode: 'BTC', + displayName: 'Bitcoin', + assetDisplayName: 'Bitcoin', + pluginId: 'bitcoin', + walletType: 'wallet:bitcoin' + }), + makeTestCreateWalletItem({ + key: 'create-usdt', + currencyCode: 'USDT', + displayName: 'Tether', + pluginId: 'ethereum', + tokenId: '0xdac17f958d2ee523a2206206994597c13d831ec7', + networkLocation: { + contractAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7' + } + }), + makeTestCreateWalletItem({ + key: 'create-wsteth', + currencyCode: 'WSTETH', + displayName: 'Wrapped stETH', + pluginId: 'ethereum', + tokenId: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', + networkLocation: { + contractAddress: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0' + } + }) + ] + + describe('empty search', () => { + test('returns all items when search is empty', () => { + const result = filterWalletCreateItemListBySearchText(testCreateList, '') + expect(result).toHaveLength(5) + }) + + test('returns all items when search is only whitespace', () => { + const result = filterWalletCreateItemListBySearchText( + testCreateList, + ' ' + ) + expect(result).toHaveLength(5) + }) + }) + + describe('single word search with startsWith', () => { + test('matches currencyCode from beginning', () => { + const result = filterWalletCreateItemListBySearchText( + testCreateList, + 'eth' + ) + // ETH matches from start, but USDT (Tether) should not match "eth" + const codes = result.map(r => r.currencyCode) + expect(codes).toContain('ETH') + expect(codes).not.toContain('USDT') // "eth" is in "tether" but not at start + }) + + test('matches displayName from beginning', () => { + const result = filterWalletCreateItemListBySearchText( + testCreateList, + 'bit' + ) + expect(result).toHaveLength(1) + expect(result[0].currencyCode).toBe('BTC') + }) + + test('does NOT match in middle (startsWith for currencyCode)', () => { + const result = filterWalletCreateItemListBySearchText( + testCreateList, + 'steth' + ) + // "steth" should not match "WSTETH" as currencyCode doesn't start with it + // nor "Wrapped stETH" as displayName doesn't start with it + expect(result).toHaveLength(0) + }) + + test('matches assetDisplayName from beginning', () => { + const result = filterWalletCreateItemListBySearchText( + testCreateList, + 'ethereum' + ) + // Ethereum mainnet and Base both have assetDisplayName "Ethereum" + expect(result).toHaveLength(2) + }) + }) + + describe('pluginId search (includes, mainnet only)', () => { + test('matches pluginId for mainnet items', () => { + const result = filterWalletCreateItemListBySearchText( + testCreateList, + 'base' + ) + expect(result).toHaveLength(1) + expect(result[0].pluginId).toBe('base') + }) + + test('does NOT match pluginId for token items', () => { + // "ethereum" as pluginId should only match mainnet items + const result = filterWalletCreateItemListBySearchText( + testCreateList, + 'ethereum' + ) + // Should match Ethereum mainnet (pluginId and displayName), Base (assetDisplayName) + // But NOT tokens even though they have pluginId: 'ethereum' + expect(result.every(r => r.walletType != null)).toBe(true) + }) + }) + + describe('networkLocation search (includes)', () => { + test('matches contract address anywhere', () => { + const result = filterWalletCreateItemListBySearchText( + testCreateList, + 'dac17f' + ) + expect(result).toHaveLength(1) + expect(result[0].currencyCode).toBe('USDT') + }) + }) + + describe('multi-word search (AND logic)', () => { + test('all words must match', () => { + const result = filterWalletCreateItemListBySearchText( + testCreateList, + 'base eth' + ) + // "base" matches pluginId/displayName, "eth" matches currencyCode + expect(result).toHaveLength(1) + expect(result[0].pluginId).toBe('base') + }) + + test('returns nothing if one word does not match', () => { + const result = filterWalletCreateItemListBySearchText( + testCreateList, + 'base btc' + ) + expect(result).toHaveLength(0) + }) + + test('handles multiple spaces', () => { + const result = filterWalletCreateItemListBySearchText( + testCreateList, + 'base eth' + ) + expect(result).toHaveLength(1) + }) + }) + + describe('case insensitivity', () => { + test('matches regardless of case', () => { + const resultLower = filterWalletCreateItemListBySearchText( + testCreateList, + 'btc' + ) + const resultUpper = filterWalletCreateItemListBySearchText( + testCreateList, + 'BTC' + ) + expect(resultLower).toHaveLength(resultUpper.length) + }) + }) + + describe('edge cases', () => { + test('handles empty create list', () => { + const result = filterWalletCreateItemListBySearchText([], 'eth') + expect(result).toHaveLength(0) + }) + + test('returns empty when no matches', () => { + const result = filterWalletCreateItemListBySearchText( + testCreateList, + 'xyz123notfound' + ) + expect(result).toHaveLength(0) + }) + }) +}) + +// ----------------------------------------------------------------------------- +// Regression Tests for Original Issues +// ----------------------------------------------------------------------------- + +describe('Regression: Original search issues', () => { + describe('Issue #1: Multi-word search "base eth"', () => { + const baseEthInfo = makeTestCurrencyInfo({ + pluginId: 'base', + currencyCode: 'ETH', + displayName: 'Ethereum', + assetDisplayName: 'Ethereum', + chainDisplayName: 'Base' + }) + const baseWallet = makeTestWallet(baseEthInfo) + const walletList: WalletListItem[] = [makeTestWalletListItem(baseWallet)] + + test('finds Ethereum wallet on Base network with "base eth"', () => { + const result = searchWalletList(walletList, 'base eth') + expect(result).toHaveLength(1) + expect( + result[0].type === 'asset' && + result[0].wallet.currencyInfo.pluginId === 'base' && + result[0].wallet.currencyInfo.currencyCode === 'ETH' + ).toBe(true) + }) + }) + + describe('Issue #3: "eth" showing Tether', () => { + const ethereumWallet = makeTestWallet(ethCurrencyInfo) + const tetherToken: EdgeToken = { + currencyCode: 'USDT', + displayName: 'Tether', + denominations: [{ name: 'USDT', multiplier: '1000000' }], + networkLocation: { contractAddress: '0xdac17f...' } + } + + const walletList: WalletListItem[] = [ + makeTestWalletListItem(ethereumWallet), + makeTestWalletListItem(ethereumWallet, tetherToken) + ] + + test('"eth" shows Ethereum but NOT Tether', () => { + const result = searchWalletList(walletList, 'eth') + expect(result).toHaveLength(1) + expect( + result[0].type === 'asset' && + result[0].wallet.currencyInfo.currencyCode === 'ETH' + ).toBe(true) + }) + + test('"teth" shows Tether (starts with "teth")', () => { + const result = searchWalletList(walletList, 'teth') + expect(result).toHaveLength(1) + expect( + result[0].type === 'asset' && result[0].token?.currencyCode === 'USDT' + ).toBe(true) + }) + + test('"usdt" shows Tether', () => { + const result = searchWalletList(walletList, 'usdt') + expect(result).toHaveLength(1) + expect( + result[0].type === 'asset' && result[0].token?.currencyCode === 'USDT' + ).toBe(true) + }) + }) +}) diff --git a/src/util/fake/fakeSearchTestData.ts b/src/util/fake/fakeSearchTestData.ts new file mode 100644 index 00000000000..b4910ec444f --- /dev/null +++ b/src/util/fake/fakeSearchTestData.ts @@ -0,0 +1,91 @@ +import type { + EdgeCurrencyInfo, + EdgeCurrencyWallet, + EdgeToken +} from 'edge-core-js' + +import type { WalletCreateItem } from '../../selectors/getCreateWalletList' +import type { WalletListItem } from '../../types/types' + +/** + * Creates a minimal EdgeCurrencyInfo for testing. + * Use this to create custom chain configurations (e.g., Base, Arbitrum). + * For standard chains, prefer importing from fakeBtcInfo.ts or fakeEthInfo.ts. + */ +export const makeTestCurrencyInfo = ( + overrides: Partial = {} +): EdgeCurrencyInfo => ({ + pluginId: 'ethereum', + currencyCode: 'ETH', + displayName: 'Ethereum', + assetDisplayName: 'Ethereum', + chainDisplayName: 'Ethereum', + walletType: 'wallet:ethereum', + addressExplorer: '', + transactionExplorer: '', + denominations: [{ name: 'ETH', multiplier: '1000000000000000000' }], + ...overrides +}) + +/** + * Creates a minimal EdgeCurrencyWallet mock for testing. + */ +export const makeTestWallet = ( + currencyInfo: EdgeCurrencyInfo, + name: string = 'My Wallet' +): EdgeCurrencyWallet => + ({ + id: `wallet-${currencyInfo.pluginId}`, + currencyInfo, + name, + balanceMap: new Map(), + enabledTokenIds: [] + } as unknown as EdgeCurrencyWallet) + +/** + * Creates a WalletListItem for testing. + */ +export const makeTestWalletListItem = ( + wallet: EdgeCurrencyWallet, + token?: EdgeToken +): WalletListItem => ({ + type: 'asset', + key: token != null ? `${wallet.id}-${token.currencyCode}` : wallet.id, + wallet, + token, + tokenId: token?.networkLocation?.contractAddress ?? null +}) + +/** + * Creates a WalletCreateItem for testing. + */ +export const makeTestCreateWalletItem = ( + overrides: Partial +): WalletCreateItem => ({ + type: 'create', + key: 'create-wallet', + currencyCode: 'ETH', + displayName: 'Ethereum', + pluginId: 'ethereum', + tokenId: null, + ...overrides +}) + +// Pre-configured test tokens +export const testTetherToken: EdgeToken = { + currencyCode: 'USDT', + displayName: 'Tether', + denominations: [{ name: 'USDT', multiplier: '1000000' }], + networkLocation: { + contractAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7' + } +} + +export const testWstethToken: EdgeToken = { + currencyCode: 'WSTETH', + displayName: 'Wrapped stETH', + denominations: [{ name: 'WSTETH', multiplier: '1000000000000000000' }], + networkLocation: { + contractAddress: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0' + } +} From 6e7c4765372d54349928a9b1aa98cf85202ceea2 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Mon, 15 Dec 2025 16:45:03 -0800 Subject: [PATCH 22/77] Use dockProp for next button --- eslint.config.mjs | 2 +- ...reateWalletSelectCryptoScene.test.tsx.snap | 3713 +++++++++-------- .../scenes/CreateWalletSelectCryptoScene.tsx | 57 +- 3 files changed, 1909 insertions(+), 1863 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 8295493276d..04967bbf893 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -241,7 +241,7 @@ export default [ 'src/components/scenes/CreateWalletEditNameScene.tsx', 'src/components/scenes/CreateWalletImportOptionsScene.tsx', 'src/components/scenes/CreateWalletImportScene.tsx', - 'src/components/scenes/CreateWalletSelectCryptoScene.tsx', + 'src/components/scenes/CurrencyNotificationScene.tsx', 'src/components/scenes/CurrencySettingsScene.tsx', 'src/components/scenes/DefaultFiatSettingScene.tsx', diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap index 4002fdea69b..5bdfd6eeecf 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap @@ -1,214 +1,120 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`CreateWalletSelectCrypto should render with loading props 1`] = ` - - - - - - - - - - + - + - + + + + opacity={0.1} + > + + + + - - - - + + - - Choose Wallets to Add - - - - - + Choose Wallets to Add + + + + - -  - - - -  +  - - - + + +  + + + + - - -  - - - - - - - - + submitBehavior="submit" + testID="undefined.textInput" + textAlignVertical="top" + /> - - - +  + + + + + + + - - ETH - - + + + + - Ethereum - - - - + ETH + + + Ethereum + + + + + "selected": false, + } + } + onChange={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + onTintColor="#00f1a2" + style={ + [ + { + "height": 31, + "width": 51, + }, + { + "backgroundColor": "#888888", + "borderRadius": 16, + }, + ] + } + tintColor="#888888" + value={false} + /> + - - - + + + + + + + + } + > + + REP (Ethereum) + + + Augur + - + + - - REP (Ethereum) - - - Augur - - - - - - - - - - + > + + + + + + - + REPV2 (Ethereum) + + + > + Augur v2 + - + + - - REPV2 (Ethereum) - - - Augur v2 - - - - - - - - - - - - - - - - - - - HERC (Ethereum) - - - Hercules - - - - - - - - - - + > + + + + + + - + HERC (Ethereum) + + + > + Hercules + - + + - - DAI (Ethereum) - - - Dai Stablecoin - - - - - - - - - - - + + + + + + + + + + DAI (Ethereum) + + + Dai Stablecoin + + + - + + + + + + + - - + + - Add Custom Token - + ] + } + > + Add Custom Token + + - - - - + + + , + , +] `; diff --git a/src/components/scenes/CreateWalletSelectCryptoScene.tsx b/src/components/scenes/CreateWalletSelectCryptoScene.tsx index b7a8b1356d3..2c602cb542b 100644 --- a/src/components/scenes/CreateWalletSelectCryptoScene.tsx +++ b/src/components/scenes/CreateWalletSelectCryptoScene.tsx @@ -60,7 +60,7 @@ interface Props 'createWalletSelectCrypto' | 'createWalletSelectCryptoNewAccount' > {} -const CreateWalletSelectCryptoComponent = (props: Props) => { +const CreateWalletSelectCryptoComponent: React.FC = (props: Props) => { const { navigation, route } = props const { newAccountFlow, @@ -215,7 +215,7 @@ const CreateWalletSelectCryptoComponent = (props: Props) => { // Prompt user to choose a wallet const selectedWalletId = await Airship.show( bridge => { - const renderRow = (walletId: string) => { + const renderRow = (walletId: string): React.ReactElement => { if (walletId === PLACEHOLDER_WALLET_ID) { return ( { item === null ? 'customToken' : item.key ) - const renderNextButton = React.useMemo( - () => ( - 0} - enter={{ - type: 'fadeIn', - duration: defaultSelection.length > 0 ? 0 : 300 - }} - exit={{ type: 'fadeOut', duration: 300 }} - accessible={false} - > - - - ), - [defaultSelection.length, handleNextPress, selectedItems.size] - ) - return ( - + 0} + enter={{ + type: 'fadeIn', + duration: defaultSelection.length > 0 ? 0 : 300 + }} + exit={{ type: 'fadeOut', duration: 300 }} + accessible={false} + > + + + ) + }} + > {({ insetStyle, undoInsetStyle }) => ( { renderItem={renderRow} scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX} /> - {renderNextButton} + {/* {renderNextButton} */} )} From 78b77eb3842cd05a05509885e51392c290cd4482 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Mon, 15 Dec 2025 16:44:18 -0800 Subject: [PATCH 23/77] Remove layout animation for next button --- src/components/common/EdgeAnim.tsx | 18 +++++++++++++++++- .../scenes/CreateWalletSelectCryptoScene.tsx | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/components/common/EdgeAnim.tsx b/src/components/common/EdgeAnim.tsx index 2e5a682addb..78b72d4e3dd 100644 --- a/src/components/common/EdgeAnim.tsx +++ b/src/components/common/EdgeAnim.tsx @@ -92,6 +92,20 @@ interface Props { enter?: Anim exit?: Anim + /** + * The animation to use for all layout changes. (defaults {@link LAYOUT_ANIMATION}) + * + * TODO: Remove default once we have audited all instances of EdgeAnim + * explicitly enabling the default LAYOUT_ANIMATION for those instances. + */ + layout?: ComplexAnimationBuilder + + /** TODO: This is a temporary way to disable the `layout` default + * LAYOUT_ANIMATION. Remove this once we have audited all instances of + * EdgeAnim explicitly enabling the default LAYOUT_ANIMATE for those instances. + */ + noLayoutAnimation?: boolean + visible?: boolean children?: ViewProps['children'] @@ -154,6 +168,8 @@ export const EdgeAnim = ({ disableAnimation, enter, exit, + layout = LAYOUT_ANIMATION, + noLayoutAnimation = false, visible = true, ...rest }: Props): React.ReactElement | null => { @@ -168,7 +184,7 @@ export const EdgeAnim = ({ return ( = (props: Props) => { keyboardVisibleOnly: false, children: ( 0} enter={{ type: 'fadeIn', @@ -474,7 +475,6 @@ const CreateWalletSelectCryptoComponent: React.FC = (props: Props) => { renderItem={renderRow} scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX} /> - {/* {renderNextButton} */} )} From 0856d3a7a2245cdabdf2a2b2ae12dc82c643293e Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 27 Nov 2025 13:12:03 -0800 Subject: [PATCH 24/77] Add archive and restore maestro test --- .../C000032-archive-and-restore-wallets.yaml | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 maestro/07-wallets/C000032-archive-and-restore-wallets.yaml diff --git a/maestro/07-wallets/C000032-archive-and-restore-wallets.yaml b/maestro/07-wallets/C000032-archive-and-restore-wallets.yaml new file mode 100644 index 00000000000..1fd43f8d6d1 --- /dev/null +++ b/maestro/07-wallets/C000032-archive-and-restore-wallets.yaml @@ -0,0 +1,119 @@ +# Priority: Critical +# Test ID: C000032 +# Title: Archive wallets +# Expected Result: +# 1. Archive wallets is functional +# 2. Unable to archive last wallet +# 3. Restore wallets is functional +# 4. Restore button at bottom of wallet list + + +appId: ${MAESTRO_APP_ID} +tags: +- all +- C000032 +--- +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/create-account.yaml +- runFlow: + file: ../common/dismiss-modals.yaml + + +# Archive wallets +- evalScript: ${var walletNames = ["My Bitcoin", "My Bitcoin Cash", "My Dash", "My Ether", "My Litecoin"]} +- evalScript: ${var index = 0} +- tapOn: Assets +- repeat: + times: ${walletNames.length - 1} + commands: + - tapOn: ${walletNames[index]} + - tapOn: + id: gearIcon + - tapOn: ".*Archive Wallet" + # Modal + - assertVisible: "Archive Wallet" + - assertVisible: ${"Are you sure you want to archive " + walletNames[index] + "?"} + - assertVisible: Cancel + # Check if "My Ether" wallet + - runFlow: + when: + true: ${walletNames[index] == "My Ether"} + commands: + - assertVisible: "Archiving this wallet will also archive any enabled tokens for this wallet." + label: "Extra message for Ether wallets" + # Archive + - tapOn: Archive + - assertNotVisible: ${walletNames[index]} + + - evalScript: ${index++} + label: "Archive all wallets except last" + +# Unable to archive last wallet +- runFlow: + commands: + - longPressOn: ${walletNames[index]} + - tapOn: ".*Archive Wallet" + - assertVisible: "Cannot Archive Wallet" + - assertVisible: "At least one wallet required in this account." + - assertVisible: "If you'd like to archive this wallet, you'll need to add an additional wallet to this account." + - assertVisible: + id: "modal-close-button" + - tapOn: "OK" + label: "Unable to archive last wallet" + +# Restore button at bottom of wallet list +- scrollUntilVisible: + element: Restore Wallets + direction: DOWN + +# Restore from settings +- tapOn: + id: sideMenuButton +- tapOn: Settings +- scrollUntilVisible: + element: Restore Wallets + centerElement: true + direction: DOWN +- tapOn: Restore Wallets +- assertVisible: "Restore Wallets" + +# Toggle on wallets to restore +- evalScript: ${var walletCurrencyCodes = ["BTC", "BCH", "DASH", "ETH", "LTC"]} +- evalScript: ${var index = 0} +- repeat: + times: ${walletCurrencyCodes.length - 1} + commands: + - tapOn: ${walletCurrencyCodes[index]} + - evalScript: ${index++} +- tapOn: Restore + +# Modal +- assertVisible: Restore Wallets +- assertVisible: This will restore all selected wallets +- assertVisible: Cancel +- tapOn: Confirm + +# Confirm restored and returned to assets screen +- assertVisible: "Total Balance.*" +- evalScript: ${var index = 0} +- repeat: + times: ${walletNames.length} + commands: + - scrollUntilVisible: + element: ${walletNames[index]} + direction: DOWN + - evalScript: ${index++} + +# Ensure restore is not tappable when none left to restore +- tapOn: + id: sideMenuButton +- tapOn: Settings +- scrollUntilVisible: + element: Restore Wallets + direction: DOWN + centerElement: true +- tapOn: Restore Wallets +# Search bar appears on next scene only if wallets exist to restore +- assertNotVisible: Search Wallets \ No newline at end of file From caddafd6294941596ab76f2c1d74700a341074a6 Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 27 Nov 2025 17:27:01 -0800 Subject: [PATCH 25/77] Add storage of password in output variable --- maestro/common/create-account.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/maestro/common/create-account.yaml b/maestro/common/create-account.yaml index 83bdd59afe4..6c12f068318 100644 --- a/maestro/common/create-account.yaml +++ b/maestro/common/create-account.yaml @@ -47,6 +47,7 @@ env: - pressKey: key: Enter - inputText: ${NEW_ACCOUNT_PASSWORD} +- evalScript: ${output.newAccountPassword = NEW_ACCOUNT_PASSWORD} - pressKey: key: Enter From 54675550381767a4f0729d9326be2db122bbd75e Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 27 Nov 2025 17:28:50 -0800 Subject: [PATCH 26/77] Add web3 handle to dismiss modals flow --- maestro/common/create-account.yaml | 15 --------------- maestro/common/dismiss-modals.yaml | 13 +++++++++++-- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/maestro/common/create-account.yaml b/maestro/common/create-account.yaml index 6c12f068318..7765493e122 100644 --- a/maestro/common/create-account.yaml +++ b/maestro/common/create-account.yaml @@ -92,18 +92,3 @@ env: - tapOn: id: "nextButton" - -# # Dismiss Web3 Handle Modal -- runFlow: - when: - true: ${!MAESTRO_EDGE_KEEP_WEB3} - commands: - # Dismiss Web3 Handle Modal - - extendedWaitUntil: - visible: "Claim Your Web3 Handle" - timeout: 5000 - optional: true - - tapOn: - text: "Not Now" - optional: true - label: "Dismiss Web3 Handle Modal" \ No newline at end of file diff --git a/maestro/common/dismiss-modals.yaml b/maestro/common/dismiss-modals.yaml index 1f5913c2eb5..cbca978f040 100644 --- a/maestro/common/dismiss-modals.yaml +++ b/maestro/common/dismiss-modals.yaml @@ -5,7 +5,7 @@ appId: ${MAESTRO_APP_ID} # Cancel it - extendedWaitUntil: visible: "Security is Our Priority" - timeout: 10000 + timeout: 15000 optional: true - tapOn: text: "Cancel" @@ -14,12 +14,21 @@ appId: ${MAESTRO_APP_ID} # If the survey modal shows, dismiss it - extendedWaitUntil: visible: "How Did You Discover Edge?" - timeout: 10000 + timeout: 5000 optional: true - tapOn: text: "Dismiss" optional: true +# If the Web3 Handle modal shows, dismiss it +- runFlow: + when: + visible: "Claim Your Web3 Handle" + commands: + - tapOn: + text: "Not Now" + label: "Dismiss Web3 Handle Modal" + # If the enable 2FA notification shows, dismiss it - swipe: from: From b968addd8fb2d73d7ef5a841c3b927baa6168988 Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 27 Nov 2025 17:33:58 -0800 Subject: [PATCH 27/77] Add import wallet common flow --- maestro/common/import-wallets.yaml | 94 ++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 maestro/common/import-wallets.yaml diff --git a/maestro/common/import-wallets.yaml b/maestro/common/import-wallets.yaml new file mode 100644 index 00000000000..1d8c8666602 --- /dev/null +++ b/maestro/common/import-wallets.yaml @@ -0,0 +1,94 @@ +# Imports wallets by name + +appId: ${MAESTRO_APP_ID} +env: + ASSET_NAMES: ${ASSET_NAMES || ""} + SEED_PHRASE: ${SEED_PHRASE || MAESTRO_EDGE_MAX_IMPORT_SEED} + PIRATE_BDAY: ${PIRATE_BDAY || ""} + ZCASH_BDAY: ${ZCASH_BDAY || ""} +--- +# Parse env so we can accept a string or array input +- evalScript: ${if (ASSET_NAMES.startsWith('[')) ASSET_NAMES = JSON.parse(ASSET_NAMES); else ASSET_NAMES = [ASSET_NAMES]} + +# Open add wallet screen +- tapOn: Assets +- tapOn: + id: "addButton" +- tapOn: Search Wallets + +# Loop over wallet names and import them +- evalScript: ${var index = 0} +- repeat: + times: ${ASSET_NAMES.length} + commands: + - evalScript: ${var assetName = ASSET_NAMES[index]} + - tapOn: + id: "undefined.clearIcon" + - inputText: ${assetName} + + # Escape parentheses for regex matching - "Bitcoin (no Segwit)" breaks flow otherwise + - evalScript: ${assetName = assetName.replace(/\(/g, '\\(').replace(/\)/g, '\\)')} + - tapOn: + text: ${".*" + assetName} + index: 1 # First index is thesearch bar + - evalScript: ${index++} + +- tapOn: Next +- tapOn: Import Wallets + +- tapOn: Private Key or Private Seed +- inputText: ${SEED_PHRASE} + +# Drop keyboard - Android +- runFlow: + when: + platform: Android + commands: + - hideKeyboard + - tapOn: Next # odd additional tap required on android sometimes +# Drop keyboard - iOS +- runFlow: + when: + platform: iOS + commands: + - tapOn: "Private Key or Private Seed" + +- tapOn: Next + +# Add birthday height for Zcash and Pirate Chain +# Index for targetting the correct "edit" icon +- evalScript: ${var index = 0} +# Pirate Chain on top if both are displayed +- runFlow: + when: + true: ${PIRATE_BDAY} + commands: + - tapOn: + text: "" + index: ${index} + - inputText: ${PIRATE_BDAY} + - tapOn: Submit + - evalScript: ${index++} +# Zcash birthday +- runFlow: + when: + true: ${ZCASH_BDAY} + commands: + - tapOn: + text: "" + index: ${index} + - inputText: ${ZCASH_BDAY} + - tapOn: Submit + - evalScript: ${index++} + + +# tap on next until it becomes tapable and works to see "assets" button +- repeat: + while: + visible: Next + commands: + - tapOn: Next + - extendedWaitUntil: + visible: Assets + timeout: 2000 + optional: true \ No newline at end of file From 8fb33327e483182a04172020125394e4787a3fbf Mon Sep 17 00:00:00 2001 From: Jared Date: Fri, 28 Nov 2025 17:35:41 -0800 Subject: [PATCH 28/77] Add pause wallets test --- maestro/07-wallets/C000033-pause-wallets.yaml | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 maestro/07-wallets/C000033-pause-wallets.yaml diff --git a/maestro/07-wallets/C000033-pause-wallets.yaml b/maestro/07-wallets/C000033-pause-wallets.yaml new file mode 100644 index 00000000000..44ba250bf13 --- /dev/null +++ b/maestro/07-wallets/C000033-pause-wallets.yaml @@ -0,0 +1,97 @@ +# Priority: Critical +# Test ID: C000033 +# Title: Pause wallets +# Expected Result: +# 1. Pause wallets is functional + + +appId: ${MAESTRO_APP_ID} +env: + BALANCE: ".*0.000086.*" + WALLET_NAME: "My Bitcoin Cash 2" + ASSET_NAME: "Bitcoin Cash" +tags: +- all +- C000033 +--- +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/create-account.yaml +- runFlow: + file: ../common/dismiss-modals.yaml + +# Import a wallet to see a balance +- runFlow: + file: ../common/import-wallets.yaml + env: + ASSET_NAMES: ${ASSET_NAME} + label: "Import BCH wallet" +# Find new wallet +- scrollUntilVisible: + element: ${WALLET_NAME} + centerElement: true + direction: "DOWN" +# Ensure synced to show balance +- extendedWaitUntil: + visible: ${BALANCE} + timeout: 20000 +- longPressOn: ${WALLET_NAME} + +# Test pause wallet +- tapOn: + text: ".*Pause Wallet" +- assertVisible: "This wallet will no longer synchronize with the blockchain and will not detect new transactions or balance changes" +- assertVisible: "Wallet Paused" + +# Wait an arbitrary amount of time to ensure state is saved +# TODO: Reduce wait period if possible +- extendedWaitUntil: + notVisible: "Wallet Paused" + timeout: 45000 + optional: true # Expected to fail + label: "⏰ Waiting 45 seconds to ensure account state is saved" + +# Ralaunch with cleared state to test that wallet is paused +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/login-password.yaml + env: + USERNAME: ${output.newAccountUsername} + PASSWORD: ${output.newAccountPassword} +- runFlow: + file: ../common/dismiss-modals.yaml + +# Wait an arbitrary amount of time to ensure wallet is paused and does not sync to show balance +- tapOn: Assets +- scrollUntilVisible: + element: ${WALLET_NAME} + centerElement: true + direction: "DOWN" +- assertVisible: "Wallet Paused" +- extendedWaitUntil: + visible: ${BALANCE} + timeout: 10000 + optional: true # Expected to fail + label: "⏰ Waiting 10 seconds to ensure wallet is paused and does not sync to show balance" +- assertNotVisible: ${BALANCE} + +# Wallet should sync once you tap into it +- tapOn: ${WALLET_NAME} +- extendedWaitUntil: + visible: ${BALANCE} + timeout: 15000 + +# Unpause +- tapOn: + id: gearIcon +- tapOn: ".*Unpause Wallet" +- assertVisible: + text: "This wallet will now synchronize with the blockchain and detect new transactions and balance changes." + optional: true # Disappears too quick for Android +- tapOn: + id: "chevronBack" +- assertNotVisible: "Wallet Paused" + +- stopApp \ No newline at end of file From 45e130f0783f4b404868b920b43f683aacc30281 Mon Sep 17 00:00:00 2001 From: Jared Date: Fri, 28 Nov 2025 17:36:30 -0800 Subject: [PATCH 29/77] Add master private key test --- .../C000018-master-private-key.yaml | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 maestro/07-wallets/C000018-master-private-key.yaml diff --git a/maestro/07-wallets/C000018-master-private-key.yaml b/maestro/07-wallets/C000018-master-private-key.yaml new file mode 100644 index 00000000000..c53c39dab6c --- /dev/null +++ b/maestro/07-wallets/C000018-master-private-key.yaml @@ -0,0 +1,77 @@ +# Priority: Critical +# Test ID: C000018 +# Title: Master Private Key +# Expected Result: +# 1. Master Private Key is visible for new BTC wallet +# 2. Master Private Key is visible and correct for imported BCH wallet + +# Notes: Could test additional assets and for wallets created within the app + +appId: ${MAESTRO_APP_ID} +env: + ASSET_NAME: "Bitcoin Cash" + WALLET_NAME: "My Bitcoin Cash 2" + SEED_PHRASE: ${MAESTRO_EDGE_MAX_IMPORT_SEED} +tags: +- all +- C000018 +--- +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/create-account.yaml +- runFlow: + file: ../common/dismiss-modals.yaml + +# View master private key for new wallet +- tapOn: Assets +- longPressOn: My Bitcoin +- tapOn: ".*Master Private Key" +- tapOn: Enter your password +- inputText: ${output.newAccountPassword} +- assertVisible: Get Seed +- pressKey: Enter +# Displays 24 words separated by spaces +- assertVisible: + text: '^(\w+\s+){23}\w+$' +- tapOn: + id: "modal-close-button" + +# Import an existing BCH wallet +- runFlow: + file: ../common/import-wallets.yaml + env: + ASSET_NAMES: ${ASSET_NAME} + SEED_PHRASE: ${SEED_PHRASE} + label: "Import BCH wallet" + +# Locate new wallet and check seed phrase +- scrollUntilVisible: + element: ${WALLET_NAME} + centerElement: true + direction: DOWN +- longPressOn: ${WALLET_NAME} +- tapOn: ".*Master Private Key" + +# Password Modal +- assertVisible: "Reveal Master Private Key" +- assertVisible: "Warning" +- assertVisible: "Sharing your master private key may put you at risk of fraudulent tokens and loss of funds. + + + Do not share your key with anyone. + + + By entering your password, you are confirming that you understand the risks." +- tapOn: Enter your password +- inputText: ${output.newAccountPassword} +- assertVisible: Get Seed +- pressKey: Enter + +# View Seed Modal +- assertVisible: "Get Seed" +- assertVisible: ${SEED_PHRASE} +- tapOn: OK +- assertVisible: Assets + +- stopApp From 3bf635c7c09cc6a55b1733bac2d82fcbd2c67015 Mon Sep 17 00:00:00 2001 From: Jared Date: Fri, 28 Nov 2025 17:37:00 -0800 Subject: [PATCH 30/77] Add get raw keys test --- maestro/07-wallets/C000035-get-raw-keys.yaml | 86 ++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 maestro/07-wallets/C000035-get-raw-keys.yaml diff --git a/maestro/07-wallets/C000035-get-raw-keys.yaml b/maestro/07-wallets/C000035-get-raw-keys.yaml new file mode 100644 index 00000000000..76c571aec79 --- /dev/null +++ b/maestro/07-wallets/C000035-get-raw-keys.yaml @@ -0,0 +1,86 @@ +# Priority: Critical +# Test ID: C000035 +# Title: Get Raw Keys +# Expected Result: +# 1. Get Raw Keys is visible and functional for new BTC wallet +# 2. Get Raw Keys is visible and correct for imported ETH wallet +# 3. Checks for accurate "imported:" tag in raw keys + +# Notes: Could test additional assets and for wallets created within the app + +appId: ${MAESTRO_APP_ID} +env: + ASSET_NAME: "Ethereum" + SEED_PHRASE: ${MAESTRO_EDGE_MAX_IMPORT_SEED} +tags: +- all +- C000035 +--- +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/create-account.yaml + env: + NEW_WALLETS: '["Bitcoin"]' +- runFlow: + file: ../common/dismiss-modals.yaml + +# View master private key for new wallet +- tapOn: Assets +- longPressOn: My Bitcoin +- tapOn: ".*Get Raw Keys" +- tapOn: Enter your password +- inputText: ${output.newAccountPassword} +- assertVisible: Get Raw Keys +- pressKey: Enter +# Displays raw keys +- assertVisible: Raw Keys +- assertVisible: ".*format.*" +- assertVisible: ".*dataKey.*" +- assertVisible: ".*syncKey.*" +- assertVisible: '.*(\w+\s+){23}\w+.*' +- assertVisible: '.*imported": false.*' +- tapOn: + id: "modal-close-button" + +# Import an existing BCH wallet +- runFlow: + file: ../common/import-wallets.yaml + env: + ASSET_NAMES: ${ASSET_NAME} + SEED_PHRASE: ${SEED_PHRASE} + label: "Import Ethereum wallet" + +# Locate new wallet and check seed phrase +- scrollUntilVisible: + element: ETH + direction: DOWN + centerElement: true +- longPressOn: ETH +- tapOn: ".*Get Raw Keys" + +# Password Modal +- assertVisible: "Reveal Raw Keys" +- assertVisible: "Warning" +- assertVisible: "Sharing your raw keys may put you at risk of fraudulent tokens and loss of funds. + + + Do not share your keys with anyone. + + + By entering your password, you are confirming that you understand the risks." +- tapOn: Enter your password +- inputText: ${output.newAccountPassword} +- assertVisible: Get Raw Keys +- pressKey: Enter + +# View Raw Keys Modal +- assertVisible: "Raw Keys" +# Displays raw keys +- assertVisible: Raw Keys +- assertVisible: "${'.*ethereumMnemonic\": \"' + SEED_PHRASE + '.*'}" +- assertVisible: ".*dataKey.*" +- assertVisible: ".*syncKey.*" +- assertVisible: ".*imported\": true.*" +- tapOn: + id: "modal-close-button" From b46e15d951b3b00603aa33f250acad5b40b217e4 Mon Sep 17 00:00:00 2001 From: Jared Date: Fri, 28 Nov 2025 19:58:41 -0800 Subject: [PATCH 31/77] Add test to add wallets by asset names --- maestro/common/add-wallets.yaml | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 maestro/common/add-wallets.yaml diff --git a/maestro/common/add-wallets.yaml b/maestro/common/add-wallets.yaml new file mode 100644 index 00000000000..34e8dce96dd --- /dev/null +++ b/maestro/common/add-wallets.yaml @@ -0,0 +1,45 @@ +# Accepts an array of wallet names and adds them as new wallets to the account + +## Somewhat fragile +# Fails if no assets are provided +# Expects assets to be listed at the top of search results if they share name + +appId: ${MAESTRO_APP_ID} +env: + ASSET_NAMES: ${ASSET_NAMES || ""} +--- +# Parse if an array/list- all passed env become strings with maestro's Rhino JS runtime +- evalScript: ${if (ASSET_NAMES.startsWith('[')) ASSET_NAMES = JSON.parse(ASSET_NAMES); else ASSET_NAMES = [ASSET_NAMES]} + +- tapOn: Assets +- tapOn: + id: "addButton" +- tapOn: Search Wallets + +# Loop over wallet names and add them +- evalScript: ${var index = 0} +- repeat: + times: ${ASSET_NAMES.length} + commands: + - tapOn: + id: "undefined.clearIcon" + - inputText: ${ASSET_NAMES[index]} + - tapOn: + text: ${".*" + ASSET_NAMES[index]} + index: 1 # First index is thesearch bar + - evalScript: ${index++} + +- tapOn: Next + +- tapOn: Create Wallets + +# tap on next until it becomes tapable and works to see "assets" +- repeat: + while: + visible: Next + commands: + - tapOn: Next + - extendedWaitUntil: + visible: Assets + timeout: 2000 + optional: true From 6e1165a22f27040d4b9d037627231e6cf294f057 Mon Sep 17 00:00:00 2001 From: Jared Date: Sat, 29 Nov 2025 01:11:28 -0800 Subject: [PATCH 32/77] Add test to add all major supported types of wallets --- .../C000044-create-all-wallet-types.yaml | 63 +++++++++++++++++++ maestro/common/add-all-wallet-types.yaml | 60 ++++++++++++++++++ maestro/common/no-errors.yaml | 11 ++++ 3 files changed, 134 insertions(+) create mode 100644 maestro/07-wallets/C000044-create-all-wallet-types.yaml create mode 100644 maestro/common/add-all-wallet-types.yaml create mode 100644 maestro/common/no-errors.yaml diff --git a/maestro/07-wallets/C000044-create-all-wallet-types.yaml b/maestro/07-wallets/C000044-create-all-wallet-types.yaml new file mode 100644 index 00000000000..c3ae7f1b2b0 --- /dev/null +++ b/maestro/07-wallets/C000044-create-all-wallet-types.yaml @@ -0,0 +1,63 @@ +# Priority: Critical +# Test ID: C000044 +# Title: Create all wallet types +# Expected Result: +# 1. User can create wallets for all primary asset types +# 2. Checks for alerts and errors + +# Note: Can be disabled once other test uses the subflow + +appId: ${MAESTRO_APP_ID} +tags: +- all +- C000044 +--- +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/create-account.yaml +- runFlow: + file: ../common/dismiss-modals.yaml + +- tapOn: Assets +- runFlow: + file: ../common/add-all-wallet-types.yaml + +- runFlow: + file: ../common/no-errors.yaml + +# Validate that all wallets were created +- evalScript: ${var index = 0} +- evalScript: ${var walletNames = JSON.parse(output.assetNames)} +- repeat: + times: ${walletNames.length} + commands: + # option 1 + # Preferred quicker method, but iOS wallet search is not currently reliable when maestro types too fast + # - tapOn: + # id: "undefined.textInput" + # - tapOn: + # id: "undefined.clearIcon" + # - inputText: ${walletNames[index]} + # - assertVisible: ${"My .*"} + + # Option 2 + # Wallet nickname often excludes extra words (e.g. "Pirate Chain" -> "My Pirate") + - evalScript: ${var nickName = "My " + walletNames[index].split(" ")[0] + ".*"} + # Hack to address inconsistent wallet name for XRP + - evalScript: ${if (walletNames[index] === "XRPL") { nickName = "My XRP"}} + - retry: + maxRetries: 2 + commands: + - scrollUntilVisible: + element: "Wallets" + direction: UP + speed: 150 + timeout: 30000 + - scrollUntilVisible: + element: ${nickName} + centerElement: true + timeout: 60000 + speed: 65 + + - evalScript: ${index++} \ No newline at end of file diff --git a/maestro/common/add-all-wallet-types.yaml b/maestro/common/add-all-wallet-types.yaml new file mode 100644 index 00000000000..9f4e68d7283 --- /dev/null +++ b/maestro/common/add-all-wallet-types.yaml @@ -0,0 +1,60 @@ +# Adds (or imports?) wallets for all primary asset types supported by Edge for testing +# Update this list to update all tests that apply to all major asset types +# In parent flow: Reference `output.assetNames` to iterate over all assets + +appId: ${MAESTRO_APP_ID} +--- + +# Parse string into an array - all passed env become strings with maestro's Rhino JS runtime +# Add more UTXO, EVM, or privacy coins when necessary +- evalScript: ${ + output.assetNames = '[ + "Ravencoin", + "Base", + "Cosmos Hub", + "PulseChain", + "Solana", + "Cardano", + "Polkadot", + "Algorand", + "Tron", + "XRPL", + "Stellar", + "Tezos", + "Hedera", + "Toncoin", + "Sui", + "Filecoin", + "FIO", + "Monero", + "Zcash", + "Zano", + "Pirate Chain" + ]'} + +# Run import flow with all networks as environment variable +- runFlow: + file: ../common/add-wallets.yaml + env: + ASSET_NAMES: ${output.assetNames} + + + +# 1. UTXO (Bitcoin-based) - Bitcoin and similar UTXO chains +# 2. EVM (Ethereum Virtual Machine) - Ethereum and all EVM-compatible chains +# 3. Cosmos SDK - Cosmos ecosystem chains using Tendermint +# 4. CryptoNote (Monero-based) - Monero and Zano privacy coins +# 5. Zcash - Zcash privacy protocol chains +# 6. Solana - Solana blockchain +# 7. Cardano - Cardano with Ouroboros PoS +# 8. Polkadot/Substrate - Polkadot and Substrate-based chains +# 9. Algorand - Algorand Pure PoS +# 10. Tron - Tron DPoS blockchain +# 11. Ripple (XRP Ledger) - Ripple Consensus Ledger +# 12. Stellar - Stellar Consensus Protocol +# 13. Tezos - Tezos Liquid PoS +# 14. Hedera Hashgraph - Hedera with hashgraph consensus +# 15. TON - The Open Network +# 16. Sui - Sui with Move language +# 17. Filecoin - Filecoin native (non-EVM) +# 18. FIO Protocol - FIO for human-readable addresses \ No newline at end of file diff --git a/maestro/common/no-errors.yaml b/maestro/common/no-errors.yaml new file mode 100644 index 00000000000..6d35837cbeb --- /dev/null +++ b/maestro/common/no-errors.yaml @@ -0,0 +1,11 @@ +# Checks for alerts and errors + +appId: ${MAESTRO_APP_ID} +--- + + +- assertNotVisible: ".*Alert.*" +- assertNotVisible: ".*Error.*" +- assertNotVisible: ".*Failed.*" +- assertNotVisible: ".*Unable.*" +- assertNotVisible: ".*Insufficient.*" \ No newline at end of file From 96c1b395b1a693ca157da30e75c156b47d2c85db Mon Sep 17 00:00:00 2001 From: Jared Date: Fri, 5 Dec 2025 12:52:51 -0800 Subject: [PATCH 33/77] Add private view key test --- .../07-wallets/C000043-private-view-key.yaml | 71 +++++++++++++++++++ maestro/common/create-account.yaml | 30 +++++++- 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 maestro/07-wallets/C000043-private-view-key.yaml diff --git a/maestro/07-wallets/C000043-private-view-key.yaml b/maestro/07-wallets/C000043-private-view-key.yaml new file mode 100644 index 00000000000..d2b94ab2d12 --- /dev/null +++ b/maestro/07-wallets/C000043-private-view-key.yaml @@ -0,0 +1,71 @@ +# Priority: Critical +# Test ID: C000043 +# Title: Private View Key +# Expected Result: +# 1. User can view private view key for supported wallets (monero, piratechain, zcash, zano) +# 2. Private View Key option is visible in wallet menu +# 3. Warning message is displayed about sharing the key + +### Available for: monero, piratechain, zcash, zano + +appId: ${MAESTRO_APP_ID} +tags: +- all +- C000043 +--- +- runFlow: + file: ../common/launch-cleared.yaml + +# Define wallets that support private view key with their expected formats +# Note: Pirate chain is abbreviated to "My Pirate" in the wallet name +# Monero/Zano (CryptoNote): 64-char hex +# Zcash/Pirate (zk-SNARKs): longer alphanumeric strings +- evalScript: ${ + var walletFormats = { + "Monero":"[a-fA-F0-9]{64}", + "Pirate Chain":"[a-zA-Z0-9]{64,}", + "Zcash":"[a-zA-Z0-9]{64,}", + "Zano":"[a-fA-F0-9]{64}" + } + } +- evalScript: ${var walletNames = Object.keys(walletFormats)} + +# Create account with wallets that support private view key +- runFlow: + file: ../common/create-account.yaml + env: + NEW_WALLETS: ${JSON.stringify(walletNames)} + label: "Add wallets with private view key support" + +- runFlow: + file: ../common/dismiss-modals.yaml + +- tapOn: Assets + +# Loop over each wallet and test Private View Key +- evalScript: ${var index = 0} +- repeat: + times: ${walletNames.length} + commands: + # Wallet nickname often excludes extra words (e.g. "Pirate Chain" -> "Pirate") + - evalScript: ${var nickName = "My " + walletNames[index].split(" ")[0]} + - scrollUntilVisible: + element: ${nickName} + direction: DOWN + centerElement: true + - longPressOn: ${nickName} + - assertVisible: ".*Private View Key" + - tapOn: ".*Private View Key" + # Private View Key Modal + - assertVisible: "Private View Key" + - assertVisible: "Warning" + - assertVisible: ".*private view key allows the receiver to see the balance.*" + - assertVisible: "Copy" + # Validate view key format for this network + - assertVisible: + text: ${walletFormats[walletNames[index]]} + - tapOn: + id: "modal-close-button" + - evalScript: ${index++} + +# - stopApp diff --git a/maestro/common/create-account.yaml b/maestro/common/create-account.yaml index 7765493e122..0d974d525ae 100644 --- a/maestro/common/create-account.yaml +++ b/maestro/common/create-account.yaml @@ -9,6 +9,7 @@ appId: ${MAESTRO_APP_ID} env: NEW_ACCOUNT_PASSWORD: ${NEW_ACCOUNT_PASSWORD || MAESTRO_EDGE_NEW_ACCOUNT_PASSWORD} NEW_ACCOUNT_PIN: ${NEW_ACCOUNT_PIN || MAESTRO_EDGE_NEW_ACCOUNT_PIN} + NEW_WALLETS: ${NEW_WALLETS || ""} --- # Start account creation flow - tapOn: Already have an account? Sign in @@ -88,7 +89,34 @@ env: text: "Cancel" optional: true -# Could toggle on wallets here to assert that they all can be created +# Toggle on wallets here +- runFlow: + when: + true: ${NEW_WALLETS} + commands: + # Remove default wallets + - evalScript: ${var defaults = ["Bitcoin", "Ethereum", "Litecoin", "Bitcoin Cash", "Dash"]} + - evalScript: ${var defIndex = 0} + - repeat: + times: ${defaults.length} + commands: + - tapOn: ${".*" + defaults[defIndex]} + - evalScript: ${defIndex++} + # Add new wallets + - evalScript: ${var newIndex = 0} + - evalScript: ${var wallets = JSON.parse(NEW_WALLETS)} + - repeat: + times: ${wallets.length} + commands: + - tapOn: Search Wallets + - tapOn: + id: "undefined.clearIcon" + - inputText: ${wallets[newIndex]} + - tapOn: + text: ${".*" + wallets[newIndex]} + index: 1 + - evalScript: ${newIndex++} + - tapOn: id: "nextButton" From afb1633b550eb6c522297f88dcdec4289ba7b722 Mon Sep 17 00:00:00 2001 From: Jared Date: Mon, 8 Dec 2025 14:26:55 -0800 Subject: [PATCH 34/77] Add detect and disable tokens test --- .../C000034-detect-disable-tokens.yaml | 143 ++++++++++++++++++ maestro/common/import-wallets.yaml | 17 ++- 2 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 maestro/07-wallets/C000034-detect-disable-tokens.yaml diff --git a/maestro/07-wallets/C000034-detect-disable-tokens.yaml b/maestro/07-wallets/C000034-detect-disable-tokens.yaml new file mode 100644 index 00000000000..7b63b1741eb --- /dev/null +++ b/maestro/07-wallets/C000034-detect-disable-tokens.yaml @@ -0,0 +1,143 @@ +# Priority: Critical +# Test ID: C000034 +# Title: Add/Edit tokens and detect tokens +# Expected Result: +# 1. Autodetect tokens is functional +# 2. Add/Edit tokens is functional +# 3. Disable tokens is functional +# 4. Detect/enable tokens is functional + +## Note: Does not test tokens being actively received +## Note: Android too slow to see "Tokens Detected" notification + +appId: ${MAESTRO_APP_ID} +env: + WALLET_NAME: "My Ether" + ASSET_NAME: "Ethereum" + TOKEN_NAME: "SHIB" # With balance +tags: +- all +- C000034 +--- +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/create-account.yaml + env: + NEW_WALLETS: ${'["Bitcoin"]'} +- runFlow: + file: ../common/dismiss-modals.yaml + +## Test detection wallet import +# Import a wallet to see a balance +- runFlow: + label: "Import wallet and open detected tokens" + commands: + - runFlow: + file: ../common/import-wallets.yaml + env: + ASSET_NAMES: ${ASSET_NAME} + NO_COMPLETE: TRUE + label: "Import ETH wallet with a token" + # Complete import + - tapOn: Next + # Ensure receive token detected notification and tap on it + - repeat: + while: + visible: "Total Balance.*" + commands: + - tapOn: + text: "Tokens Detected.*" + optional: true + # Ensure scene displays correctly + - assertVisible: ${WALLET_NAME} + - assertVisible: "Select tokens to display in wallet:" + - assertVisible: "Auto Detected Tokens" + - assertVisible: "All Tokens" + - tapOn: + text: ${TOKEN_NAME + ".*"} + index: 1 + - tapOn: Done + - assertNotVisible: ${TOKEN_NAME} + +# # Skip testing resync behavior until changed - expected NOT to rediscover tokens from a resync +# - runFlow: +# label: "Detect token on resync" +# commands: +# # Arbitrary wait to let wallet state settle +# - extendedWaitUntil: +# visible: ${TOKEN_NAME} +# timeout: 30000 +# optional: true +# label: "⏰ Waiting 30 seconds to let wallet state settle" +# # Resync and check for token +# - scrollUntilVisible: +# element: ${WALLET_NAME} +# direction: DOWN +# timeout: 10000 +# - longPressOn: ${WALLET_NAME} +# - tapOn: ".*Resync" + +# - tapOn: +# text: "Resync" +# below: +# text: "Resync Wallet" +# # Ensure receive token detected notification +# - extendedWaitUntil: +# visible: ".*Tokens Detected.*" +# timeout: 60000 +# - scrollUntilVisible: +# element: ${TOKEN_NAME} +# direction: DOWN +# timeout: 10000 + +- runFlow: + label: "Detect token from settings" + commands: + # Navigagte to Asset Settings scene + - tapOn: + id: sideMenuButton + - tapOn: Settings + - scrollUntilVisible: + element: Asset Settings + direction: DOWN + - tapOn: Asset Settings + + # Ensure toast messages are displayed (Android too slow to see toasts) + - runFlow: + when: + platform: iOS + commands: + - tapOn: Detect & Enable Tokens + - extendedWaitUntil: + visible: Enabled detected tokens + timeout: 5000 + - tapOn: Detect & Enable Tokens + - extendedWaitUntil: + visible: No balances found on disabled tokens + timeout: 5000 + - tapOn: Detect & Enable Tokens + + # Ensure token is enabled + - tapOn: + id: "chevronBack" + - tapOn: + id: "chevronBack" + - scrollUntilVisible: + element: ${TOKEN_NAME} + direction: DOWN + centerElement: true + timeout: 10000 + +# Test disable token from wallet options +- runFlow: + commands: + # Hide token from "Disable Token" button in token wallet options menu + - longPressOn: ${TOKEN_NAME} + - tapOn: ".*Disable Token" + # Modal + - assertVisible: Disable Token + - assertVisible: ${"Are you sure you want to disable token " + TOKEN_NAME + " in your " + WALLET_NAME + " wallet?"} + - assertVisible: Cancel + - tapOn: Archive + - assertNotVisible: ${TOKEN_NAME} diff --git a/maestro/common/import-wallets.yaml b/maestro/common/import-wallets.yaml index 1d8c8666602..0d6025a3f9a 100644 --- a/maestro/common/import-wallets.yaml +++ b/maestro/common/import-wallets.yaml @@ -45,7 +45,7 @@ env: platform: Android commands: - hideKeyboard - - tapOn: Next # odd additional tap required on android sometimes + - tapOn: "Enter your.*" # Drop keyboard - iOS - runFlow: when: @@ -55,6 +55,13 @@ env: - tapOn: Next +# # Sometimes android requires additional tap +# - runFlow: +# when: +# visible: Import Wallet +# commands: +# - tapOn: Next + # Add birthday height for Zcash and Pirate Chain # Index for targetting the correct "edit" icon - evalScript: ${var index = 0} @@ -83,12 +90,12 @@ env: # tap on next until it becomes tapable and works to see "assets" button +# skip if NO_COMPLETE is true (for autodetect tokens flow test) - repeat: while: visible: Next + true: ${!NO_COMPLETE} commands: - - tapOn: Next - - extendedWaitUntil: - visible: Assets - timeout: 2000 + - tapOn: + text: Next optional: true \ No newline at end of file From 65b46ba2dc96bb17c9e655b6648ce65e5830eb0f Mon Sep 17 00:00:00 2001 From: Jared Date: Mon, 8 Dec 2025 14:29:35 -0800 Subject: [PATCH 35/77] Add test for add edit tokens --- .../07-wallets/C000045-add-edit-tokens.yaml | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 maestro/07-wallets/C000045-add-edit-tokens.yaml diff --git a/maestro/07-wallets/C000045-add-edit-tokens.yaml b/maestro/07-wallets/C000045-add-edit-tokens.yaml new file mode 100644 index 00000000000..73dde3a9abe --- /dev/null +++ b/maestro/07-wallets/C000045-add-edit-tokens.yaml @@ -0,0 +1,183 @@ + +## Continuation of "detect-disable" test, but currently redundant + +# Priority: Critical +# Test ID: C000045 +# Title: Add/Edit tokens +# Expected Result: +# 1. Add/Edit tokens is functional +# 2. Validates first token activation warning - "Need ETH" + +# TODO: validate "delete token" action (need targeting for edit icon) +# TODO: remove extra "A" and delete for iOS autocomplete + + +appId: ${MAESTRO_APP_ID} +env: + WALLET_NAME: "My Ether" + UNI_CONTRACT_ADDRESS: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984" # $UNI token + MOG_CONTRACT_ADDRESS: "0xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a" + # ^^ Must be a token that is not defualt and higher in alphabetically than $TOKEN_NAME + # Backup: NPC - 0x8ed97a637a790be1feff5e888d43629dc05408f6 +tags: +- all +- C000045 +--- +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/create-account.yaml + env: + NEW_WALLETS: ${'["Bitcoin"]'} +- runFlow: + file: ../common/dismiss-modals.yaml + +# Import wallet with a balance of MOG +- runFlow: + file: ../common/import-wallets.yaml + env: + ASSET_NAMES: "Ethereum" + +# Navigate to scene +- tapOn: Assets +- scrollUntilVisible: + element: ${WALLET_NAME} + direction: DOWN + centerElement: true +- longPressOn: ${WALLET_NAME} +- tapOn: ".*Add / Edit Tokens" + +# First time token warning modal +- tapOn: "1INCH.*" +- assertVisible: "ETH Needed to Send Tokens" +- tapOn: "ETH is required to pay the mining fees when sending tokens. The associated ETH wallet must contain a sufficient.*" +- tapOn: "Please confirm your understanding below:" +- tapOn: "I understand and agree to the terms" +- tapOn: "Confirm & Finish" + +- runFlow: + label: "Add custom token" + commands: + # Test search + - tapOn: Search Tokens + # Test custom token - details for $UNI token + - tapOn: + id: "undefined.clearIcon" + - tapOn: Add Custom + - assertVisible: Add Token + - assertVisible: Contract Address # Different for some networks + - assertVisible: Token Code + - assertVisible: Token Name + - assertVisible: Number of Decimal Places + # Validate invalid contract modal + - tapOn: Save + - tapOn: "Please enter valid token information and try again" + - tapOn: OK + # Slightly inconsistent coming from maestro + - retry: + label: "Test autocomplete and save token" + maxRetries: 3 + commands: + # Clear text input on retries + - tapOn: + id: "undefined.clearIcon" + index: 0 + optional: true + - tapOn: + text: "Contract Address" + - inputText: ${UNI_CONTRACT_ADDRESS} + # Fix: iOS is inconsistent - may type too fast on maestro + - runFlow: + when: + platform: iOS + commands: + - inputText: "A" + - pressKey: Backspace + # Ensure autocomplete is working + - pressKey: Enter # Drop keyboard + - extendedWaitUntil: + visible: UNI # Token Code + timeout: 15000 + - assertVisible: Uniswap # Token Name + - assertVisible: "18" # Decimals + - tapOn: Save + # Modal for token that exists already + - assertVisible: "The entered token already exists as a built-in token UNI" + - tapOn: OK + # Enter new contract address - details for $MOG token + - tapOn: + id: "undefined.clearIcon" + index: 0 + - tapOn: + text: "Contract Address" + - inputText: ${MOG_CONTRACT_ADDRESS} + # Fix: iOS is inconsistent - may type too fast on maestro + - runFlow: + when: + platform: iOS + commands: + - inputText: "A" + - pressKey: Backspace + # Ensure autocomplete is working + - pressKey: Enter # Drop keyboard + - extendedWaitUntil: + visible: MOG # Token Code + timeout: 15000 + # Save token + - tapOn: Save + - assertVisible: ".*MOG.*" + + +# Confirm +- tapOn: Done + +# Ensure tokens are visible +- scrollUntilVisible: + element: 1INCH + direction: DOWN + timeout: 10000 +- scrollUntilVisible: + element: Mog + direction: DOWN + centerElement: true + timeout: 10000 + +# Wait a few seconds for wallet state to save +- extendedWaitUntil: + visible: Error + timeout: 15000 + optional: true + label: "⏰ Waiting 15 seconds to let wallet state settle" + +# Test token still present after relogin +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/login-password.yaml + env: + USERNAME: ${output.newAccountUsername} + PASSWORD: ${output.newAccountPassword} +- runFlow: + file: ../common/dismiss-modals.yaml +- tapOn: Assets +- scrollUntilVisible: + element: "Mog" + direction: DOWN + centerElement: true + timeout: 10000 +- tapOn: Mog + +- extendedWaitUntil: + visible: ".*348,083.*" + timeout: 90000 + +# # Delete custom token +# - tapOn: +# id: gearIcon +# - tapOn: ".*Go to Parent Wallet" +# - tapOn: +# id: gearIcon +# - tapOn: ".*Add / Edit Tokens" +# # Tap on 'edit' icon +# - tapOn: "" +# - tapOn: Delete \ No newline at end of file From c25afa090858970bf0512ceb70b862ed2662a182 Mon Sep 17 00:00:00 2001 From: Jared Date: Mon, 8 Dec 2025 15:53:05 -0800 Subject: [PATCH 36/77] Add rename wallet test --- maestro/07-wallets/C000046-rename-wallet.yaml | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 maestro/07-wallets/C000046-rename-wallet.yaml diff --git a/maestro/07-wallets/C000046-rename-wallet.yaml b/maestro/07-wallets/C000046-rename-wallet.yaml new file mode 100644 index 00000000000..29b4b06ca5a --- /dev/null +++ b/maestro/07-wallets/C000046-rename-wallet.yaml @@ -0,0 +1,57 @@ +# Priority: Critical +# Test ID: C000046 +# Title: Rename wallet +# Expected Result: +# 1. Rename wallet is functional and persistent after relogin + +appId: ${MAESTRO_APP_ID} +env: + WALLET_NAME: "My Bitcoin" +tags: +- all +- C000046 +--- +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/create-account.yaml +- runFlow: + file: ../common/dismiss-modals.yaml + +- tapOn: Assets +- longPressOn: ${WALLET_NAME} +- tapOn: ".*Rename" +# Modal +- assertVisible: Rename Wallet +- assertVisible: + id: "modal-close-button" +- tapOn: + id: "undefined.clearIcon" +- inputRandomText +# Copy new name and save +- copyTextFrom: + id: "undefined.textInput" +- evalScript: ${var newWalletName = maestro.copiedText} +- tapOn: Submit + +- assertVisible: ${newWalletName} +# Arbitrary wait to ensure wallet state is saved to sync server (~30 seconds) +- extendedWaitUntil: + visible: ${WALLET_NAME} + timeout: 45000 + optional: true + label: "Wait 45 seconds for wallet state to be saved" + +# Relogin to ensure wallet stays renamed +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/login-password.yaml + env: + USERNAME: ${output.newAccountUsername} + PASSWORD: ${output.newAccountPassword} +- runFlow: + file: ../common/dismiss-modals.yaml + +- tapOn: Assets +- assertVisible: ${newWalletName} \ No newline at end of file From c79dd52d5065a8283f9f24b7038089fec70ff79e Mon Sep 17 00:00:00 2001 From: Jared Date: Mon, 8 Dec 2025 19:16:10 -0800 Subject: [PATCH 37/77] Add view xpub test --- maestro/07-wallets/C000047-view-xpub.yaml | 64 +++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 maestro/07-wallets/C000047-view-xpub.yaml diff --git a/maestro/07-wallets/C000047-view-xpub.yaml b/maestro/07-wallets/C000047-view-xpub.yaml new file mode 100644 index 00000000000..31afeecf0f1 --- /dev/null +++ b/maestro/07-wallets/C000047-view-xpub.yaml @@ -0,0 +1,64 @@ +# Priority: Critical +# Test ID: C000047 +# Title: View XPub Address +# Expected Result: +# 1. XPub Address is visible for new Dash wallet +# 2. XPub Address is visible for imported Dogecoin wallet + +# Notes: Tests xpub visibility for Dash, Bitcoin Cash, and Dogecoin + +appId: ${MAESTRO_APP_ID} +env: + SEED_PHRASE: ${MAESTRO_EDGE_MAX_IMPORT_SEED} +tags: +- all +- C000047 +--- +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/create-account.yaml + env: + NEW_WALLETS: '["Dash"]' +- runFlow: + file: ../common/dismiss-modals.yaml + +# View xpub for new Dash wallet (created by default) +- tapOn: Assets +- longPressOn: My Dash +- tapOn: ".*View XPub Address" +- assertVisible: "View XPub Address" +# Dash xpub starts with "drkp" followed by base58 characters (111 chars total) +- assertVisible: + text: "^drkp[1-9A-HJ-NP-Za-km-z]{107}$" +- assertVisible: Copy +- tapOn: + id: "modal-close-button" + + +# Import Dogecoin wallet +- runFlow: + file: ../common/import-wallets.yaml + env: + ASSET_NAMES: "Dogecoin" + SEED_PHRASE: ${SEED_PHRASE} + label: "Import DOGE wallet" + +# View xpub for imported Dogecoin wallet +- scrollUntilVisible: + element: "My Doge" + direction: DOWN +- longPressOn: "My Doge" +- tapOn: ".*View XPub Address" +- assertVisible: "View XPub Address" +# Dogecoin xpub starts with "dgub" followed by base58 characters (111 chars total) +- assertVisible: + text: "^dgub[1-9A-HJ-NP-Za-km-z]{107}$" +- assertVisible: Copy +- tapOn: + id: "modal-close-button" + +- assertVisible: Assets + +- stopApp + From ddb237ad890fab859def38a65520d20aae983f05 Mon Sep 17 00:00:00 2001 From: Jared Date: Mon, 8 Dec 2025 18:12:10 -0800 Subject: [PATCH 38/77] Add split wallets test --- maestro/07-wallets/C000037-split-wallets.yaml | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 maestro/07-wallets/C000037-split-wallets.yaml diff --git a/maestro/07-wallets/C000037-split-wallets.yaml b/maestro/07-wallets/C000037-split-wallets.yaml new file mode 100644 index 00000000000..65f38140050 --- /dev/null +++ b/maestro/07-wallets/C000037-split-wallets.yaml @@ -0,0 +1,135 @@ +# Priority: Critical +# Test ID: C000037 +# Title: Split wallets +# Expected Result: +# 1. Split wallet option appears for splittable wallets +# 2. Can split EVM wallet (Base → Ethereum) +# 3. Can split Bitcoin wallet (Litecoin → Bitcoin) + +# TODO: Re-add litecoin once wallet search works + +appId: ${MAESTRO_APP_ID} +env: + SEED_PHRASE: ${MAESTRO_EDGE_MAX_IMPORT_SEED} +tags: +- all +- C000037 +--- +- runFlow: + file: ../common/launch-cleared.yaml +# Only create with 1 wallet to decrease sync +- runFlow: + file: ../common/create-account.yaml + env: + NEW_WALLETS: '["Bitcoin Cash"]' +- runFlow: + file: ../common/dismiss-modals.yaml + +# Import Base and Litecoin wallets +- runFlow: + file: ../common/import-wallets.yaml + env: + ASSET_NAMES: '["Base"]' #', "Litecoin (Segwit)"]' + SEED_PHRASE: ${SEED_PHRASE} + +## Base +# Navigate to Base wallet +- tapOn: Assets +- scrollUntilVisible: + element: "My Base" + direction: DOWN + timeout: 10000 + +# Open wallet options menu +- longPressOn: "My Base" + +# Verify split option is available and tap it +- tapOn: ".*Split Wallet" + +# Select Ethereum as target +- assertVisible: Create Wallet From Seed +- assertVisible: Search Wallets +- tapOn: ".*Ethereum" +- tapOn: Next + +# Next scene +- assertVisible: Split Wallet +- assertVisible: This action creates wallets from pre-existing wallets. +- assertVisible: Tap on wallet to edit name +- evalScript: ${var newETHName = ".*My Ether \\(Split from My Base\\).*"} +- assertVisible: ${newETHName} + +# Rename modal +- tapOn: + text: ${"ETH.*"} + index: 0 +- tapOn: + id: "undefined.clearIcon" +- inputRandomText +## Unable to target text input field to test changing name +# - copyTextFrom: +# id: "undefined.textInput" +# - evalScript: ${var newETHName = maestro.copiedText} +# - tapOn: Submit +- assertVisible: Submit +- tapOn: + id: "modal-close-button" + +# Button shares name with scene title +- tapOn: + text: Split Wallet + index: 1 + + + +# ## LITECOIN +# # Locate Litecoin wallet and split +# - scrollUntilVisible: +# element: "My Litecoin" +# direction: DOWN +# centerElement: true +# timeout: 10000 +# - longPressOn: "My Litecoin" +# - tapOn: ".*Split Wallet" + +# - assertVisible: "Create Wallet From Seed" +# - tapOn: ".*Bitcoin" +# - tapOn: Next + +# - assertVisible: Split Wallet +# - assertVisible: This action creates wallets from pre-existing wallets. +# - assertVisible: Tap on wallet to edit name +# - evalScript: ${var newBTCName = ".*My Bitcoin \\(Split from My Litecoin\\).*"} +# - assertVisible: ${newBTCName} +# - tapOn: +# text: Split Wallet +# index: 1 + + +# ## Verify new wallets and balances +# # ETH recovered +# - scrollUntilVisible: +# element: ${newETHName} +# direction: DOWN +# centerElement: true +# timeout: 10000 +# - extendedWaitUntil: +# visible: "Ξ 0.0000133" +# timeout: 30000 +# optional: true +# label: "⏰ Waiting 30 seconds for recovered ETH balance to be visible" + +# # BTC recovered +# - scrollUntilVisible: +# element: ${newBTCName} +# direction: DOWN +# centerElement: true +# timeout: 10000 +# - extendedWaitUntil: +# visible : "₿ 0.000011" +# timeout: 30000 +# optional: true +# label: "⏰ Waiting 30 seconds for BTC balance to be visible" + + +# - stopApp From e4d614926e68d3620746ccc4cd7f9e96720f7eef Mon Sep 17 00:00:00 2001 From: Jared Date: Mon, 8 Dec 2025 20:13:17 -0800 Subject: [PATCH 39/77] Add test for auto detect tokens for all major network types --- .../C000036-autodetect-all-networks.yaml | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 maestro/07-wallets/C000036-autodetect-all-networks.yaml diff --git a/maestro/07-wallets/C000036-autodetect-all-networks.yaml b/maestro/07-wallets/C000036-autodetect-all-networks.yaml new file mode 100644 index 00000000000..4d9bdbc4e31 --- /dev/null +++ b/maestro/07-wallets/C000036-autodetect-all-networks.yaml @@ -0,0 +1,150 @@ +# Priority: Critical +# Test ID: C000036 +# Title: Autodetect all networks +# Expected Result: +# 1. Autodetect all networks is functional + +appId: ${MAESTRO_APP_ID} +env: + WALLET_NAME: "My Ether 2" +tags: +- all +- C000036 +--- +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/create-account.yaml + env: + NEW_WALLETS: ${'["Dash"]'} +- runFlow: + file: ../common/dismiss-modals.yaml + +# All networks that support tokens (and import seed) +# EVMs: Only Sonic and Pulsechain (others tested via regular asset sync) +- evalScript: ${ + var wallets = '[ + "Coreum", + "Osmosis", + "Avalanche", + "Solana", + "Optimism", + "Sui", + "Thorchain", + "Tron", + "XRPL" + ]'} + + +# Run import flow with all networks as environment variable +- runFlow: + file: ../common/import-wallets.yaml + env: + ASSET_NAMES: ${wallets} + # label: "Import wallets" + +- runFlow: + file: ../common/no-errors.yaml + +# Ensure receive token detected notification +- extendedWaitUntil: + visible: ".*Tokens Detected.*" + timeout: 30000 + optional: true + +# Tokens matching the imported networks +- evalScript: ${ + var tokens = [ + "ATOM", + "JOE", + "PYTH", + "OP", + "USDC", + "SUN", + "Ripple USD" + ]} +# TODO: re-add TCY once Thorchain autodetect works +# TODO: re-add ION once Osmosis sync works on iOS + +- scrollUntilVisible: + element: Add Wallet + direction: DOWN + timeout: 30000 + speed: 150 + optional: true + label: "Scroll to bottom." + +- extendedWaitUntil: + visible: error + timeout: 30000 + optional: true + label: "⏰ Wait 30 seconds for wallets to load" + +# # attempt #2 +# - scrollUntilVisible: +# element: ${WALLET_NAME} +# centerElement: true +# - extendedWaitUntil: +# visible: SHIB +# timeout: 60000 + +# attempt #1 +# For each token in list, scroll to verify wallet created +# - extendedWaitUntil: +# visible: ${".*New tokens were detected and enabled on " + WALLET_NAME + ".*"} +# timeout: 60000 + +- evalScript: ${var index = 0} +- repeat: + times: ${tokens.length} + commands: + - scrollUntilVisible: + element: ${tokens[index]} + direction: UP + timeout: 20000 + optional: true + - scrollUntilVisible: + element: ${tokens[index]} + direction: DOWN + timeout: 90000 + - scrollUntilVisible: + element: Add Wallet + direction: DOWN + speed: 150 + timeout: 10000 + optional: true + label: "Scroll to bottom." + - evalScript: ${index++} + + + +# # Assets and matching tokens +## EVM Networks - only testing 1 L1 and 1 L2 currently +# Ethereum - SHIB +# Celo - CEUR +# Base - BRETT +# PulseChain - HEX +# Sonic - OS +# Filecoin FEVM - iFIL +# EthereumPoW - WETHW +# Optimism - OP +# Avalanche - JOE +# zkSync - DAI +# Fantom - L3USD +# Polygon - AAVE +# Rootstock - RIF +# Binance Smart Chain - ASTER +# Arbitrum One - ARB + +## Cosmos-based Networks +# Thorchain - TCY +# Coreum - ATOM - not conflict because ATOM (Cosmos HUB) does not support tokens +# Osmosis - ION + +## Other Networks +# Sui - USDC +# Solana - PYTH +# XRPL - RLUSD +# Tron - SUN + +# Algorand - USDC -- conflict but only default token on algorand and algorand does not support import \ No newline at end of file From 63d467d12f450b07ab28a98c0cf72656ecf3339d Mon Sep 17 00:00:00 2001 From: Jared Date: Tue, 9 Dec 2025 16:06:58 -0800 Subject: [PATCH 40/77] Add migrate wallets test --- .../07-wallets/C000029a-migrate-wallets.yaml | 145 ++++++++++++++++++ scripts/runMaestro.ts | 4 + 2 files changed, 149 insertions(+) create mode 100644 maestro/07-wallets/C000029a-migrate-wallets.yaml diff --git a/maestro/07-wallets/C000029a-migrate-wallets.yaml b/maestro/07-wallets/C000029a-migrate-wallets.yaml new file mode 100644 index 00000000000..e166e442070 --- /dev/null +++ b/maestro/07-wallets/C000029a-migrate-wallets.yaml @@ -0,0 +1,145 @@ +# Priority: Critical +# Test ID: C000029 +# Title: Migrate wallets +# Expected Result: +# 1. Migrate wallets appears functional from settings scene +# 2. Migrate wallets appears functional from import flow + +# Note: Does not complete migration so funds are not moved +## TODO: Unable to target Next button in migrate flow (iOS) + + +appId: ${MAESTRO_APP_ID} +env: + WALLET_NAME: "My Doge" + WALLET_BALLANCE: "8.134" + IMPORT_ASSET: "Solana" + IMPORT_SEED: ${MAESTRO_EDGE_MAX_IMPORT_SEED} +tags: +- android +- C000029 +--- +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/login-password.yaml + env: + USERNAME: ${MAESTRO_EDGE_ASSETS_USERNAME} + PASSWORD: ${MAESTRO_EDGE_ASSETS_PASSWORD} +- runFlow: + file: ../common/dismiss-modals.yaml + +# Navigate to migrate wallets +- tapOn: + id: "sideMenuButton" +- tapOn: "Settings" +- scrollUntilVisible: + element: "Migrate Wallets" + direction: "DOWN" + centerElement: true + timeout: 20000 +- tapOn: "Migrate Wallets" + +# Select wallets +- assertVisible: Choose Assets to Migrate +- scrollUntilVisible: + element: ${".*" + WALLET_NAME} + direction: "DOWN" + centerElement: true +- tapOn: ${".*" + WALLET_NAME} +- tapOn: + text: Next + +# Check migrate wallets scene +- assertVisible: + text: Syncing... + optional: true # Toast dependable for Maestro to notice? +- assertVisible: Confirm Migration +- assertVisible: ${".*" + WALLET_NAME + ".*"} +- assertVisible: "Migrating assets to a new wallet incurs a network fee for each.*" + +# Displays network fee with DOGE symbol when able to slide to confirm +- extendedWaitUntil: + visible: ".*Ð.*" + timeout: 15000 + +# DO NOT SLIDE WITH THIS ACCOUNT +- assertVisible: Slide to Confirm + +######################################################### +# Optimization: Can remove new account creation if canceling out of migrate flow does not still import the wallet +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/create-account.yaml +- runFlow: + file: ../common/dismiss-modals.yaml + +# ## Test from import wallet flow +# - tapOn: +# id: "chevronBack" +# - tapOn: +# id: "chevronBack" +# - tapOn: +# id: "chevronBack" + +- tapOn: Assets +- tapOn: + id: "addButton" +- tapOn: Search Wallets +- inputText: ${IMPORT_ASSET} +- tapOn: + text: ${".*" + IMPORT_ASSET} + index: 1 +- tapOn: Next +- tapOn: Import Wallets +- tapOn: Private Key or Private Seed +- inputText: ${IMPORT_SEED} + +# Drop keyboard - Android +- runFlow: + when: + platform: Android + commands: + - hideKeyboard + # - tapOn: Next # odd additional tap required on android sometimes +# Drop keyboard - iOS +- runFlow: + when: + platform: iOS + commands: + - tapOn: "Private Key or Private Seed" + +- tapOn: Next + +# Sometimes android requires additional tap +- runFlow: + when: + notVisible: Migrate + commands: + - tapOn: Next + +# Wait until green checkmark is visible to continue (android only) +- extendedWaitUntil: + visible: ".*" + timeout: 5000 + optional: true + +# Ensure migrate wallets scene is displayed +- tapOn: Migrate +- assertVisible: + text: Syncing... + optional: true # Toast dependable for Maestro to notice? +- assertVisible: Confirm Migration +- assertVisible: ${".*" + IMPORT_ASSET + ".*"} +- assertVisible: "Migrating assets to a new wallet incurs a network fee for each.*" + +# Displays network fee with Solana symbol when able to slide to confirm +- extendedWaitUntil: + visible: ".*◎.*" + timeout: 45000 + +# DO NOT SLIDE WITH THIS ACCOUNT +- assertVisible: Slide to Confirm + +- stopApp \ No newline at end of file diff --git a/scripts/runMaestro.ts b/scripts/runMaestro.ts index 5f651c2a18a..71a69a00657 100644 --- a/scripts/runMaestro.ts +++ b/scripts/runMaestro.ts @@ -26,6 +26,8 @@ const asTestConfig = asObject({ MAESTRO_EDGE_XRP_PASSWORD: asOptional(asString, 'passwd'), MAESTRO_EDGE_TXDETAILS_USERNAME: asOptional(asString, 'user'), MAESTRO_EDGE_TXDETAILS_PASSWORD: asOptional(asString, 'passwd'), + MAESTRO_EDGE_ASSETS_USERNAME: asOptional(asString, 'user'), + MAESTRO_EDGE_ASSETS_PASSWORD: asOptional(asString, 'passwd'), MAESTRO_EDGE_NEW_ACCOUNT_PASSWORD: asOptional(asString, 'passwd'), MAESTRO_EDGE_NEW_ACCOUNT_PIN: asOptional(asString, 'pin'), MAESTRO_EDGE_NEW_ACCOUNT_PIN_SINGLE: asOptional(asString, 'pin'), @@ -54,6 +56,8 @@ const asTestConfig = asObject({ MAESTRO_EDGE_XRP_PASSWORD: 'passwd', MAESTRO_EDGE_TXDETAILS_USERNAME: 'user', MAESTRO_EDGE_TXDETAILS_PASSWORD: 'passwd', + MAESTRO_EDGE_ASSETS_USERNAME: 'user', + MAESTRO_EDGE_ASSETS_PASSWORD: 'passwd', MAESTRO_EDGE_NEW_ACCOUNT_PASSWORD: 'passwd', MAESTRO_EDGE_NEW_ACCOUNT_PIN: 'pin', MAESTRO_EDGE_NEW_ACCOUNT_PIN_SINGLE: 'pin', From e5187fd9cb567d3df69ca2cd900a9cecc0ed8761 Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 11 Dec 2025 15:02:45 -0800 Subject: [PATCH 41/77] Add scroll to help delete account test --- maestro/common/delete-account.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/maestro/common/delete-account.yaml b/maestro/common/delete-account.yaml index 5a4a70e5bac..c25a09b2ae8 100644 --- a/maestro/common/delete-account.yaml +++ b/maestro/common/delete-account.yaml @@ -25,6 +25,7 @@ env: # Delete account confirmation (scroll for small screens) - scroll - tapOn: "I understand and agree to the terms" +- scroll - tapOn: "Confirm & Finish" # Ensure username is displayed - assertVisible: From 4b35de266354d0d5ad328a2ebc759dc4407d836c Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 11 Dec 2025 14:23:24 -0800 Subject: [PATCH 42/77] Add split wallet test to split all available evm networks --- maestro/07-wallets/C000048-split-all-evm.yaml | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 maestro/07-wallets/C000048-split-all-evm.yaml diff --git a/maestro/07-wallets/C000048-split-all-evm.yaml b/maestro/07-wallets/C000048-split-all-evm.yaml new file mode 100644 index 00000000000..e841ad96ee8 --- /dev/null +++ b/maestro/07-wallets/C000048-split-all-evm.yaml @@ -0,0 +1,93 @@ +# Priority: Critical +# Test ID: C000048 +# Title: Split EVM networks available +# Expected Result: +# 1. Split wallet option appears for Ethereum wallet +# 2. All expected EVM networks are visible in the split wallet list +# 3. No errors when splitting to all supported EVM networks + +appId: ${MAESTRO_APP_ID} +tags: +- all +- C000048 +--- +- runFlow: + file: ../common/launch-cleared.yaml +- runFlow: + file: ../common/create-account.yaml + env: + NEW_WALLETS: '["Ethereum"]' +- runFlow: + file: ../common/dismiss-modals.yaml + +# Navigate to Ethereum wallet +- tapOn: Assets +- scrollUntilVisible: + element: "My Ether" + direction: DOWN + timeout: 10000 + +# Open wallet options menu +- longPressOn: "My Ether" + +# Verify split option is available and tap it +- tapOn: ".*Split Wallet" + +# Verify split wallet scene +- assertVisible: Create Wallet From Seed +- assertVisible: Search Wallets + +# Define expected EVM networks (ordered for scrolling down) +- evalScript: | + ${var evmNetworks = [ + "zkSync", + "Optimism", + "Ethereum Classic", + "Rootstock", + "Fantom", + "Polygon", + "Avalanche", + "BNB Smart Chain", + "BOB", + "Abstract", + "Arbitrum One", + "Base", + "Botanix Bitcoin", + "Celo", + "EthereumPoW", + "Filecoin FEVM", + "HyperEVM", + "PulseChain", + "Sonic" + ]} + +# Loop over each network and scroll until visible +- evalScript: ${var index = 0} +- repeat: + times: ${evmNetworks.length} + commands: + - scrollUntilVisible: + element: ${".*" + evmNetworks[index]} + direction: DOWN + centerElement: true + speed: 10 + timeout: 10000 + - tapOn: ${".*" + evmNetworks[index]} + - evalScript: ${index++} + +- tapOn: Next + +- tapOn: + text: Split Wallet + index: 1 + +- extendedWaitUntil: + visible: Assets + timeout: 25000 + +- runFlow: + file: ../common/no-errors.yaml + + +- stopApp + From 80799e6ab8473519a83446dc2788cb28f040b282 Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 11 Dec 2025 15:20:51 -0800 Subject: [PATCH 43/77] Improve light account flow robustness --- maestro/01-accounts/C000006-light-account.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/maestro/01-accounts/C000006-light-account.yaml b/maestro/01-accounts/C000006-light-account.yaml index e06b4697561..557f3da9316 100644 --- a/maestro/01-accounts/C000006-light-account.yaml +++ b/maestro/01-accounts/C000006-light-account.yaml @@ -71,7 +71,6 @@ tags: - extendedWaitUntil: visible: Security is Our Priority timeout: 15000 - optional: true - runFlow: when: visible: "Security is Our Priority" @@ -136,9 +135,12 @@ tags: - tapOn: text: "Dismiss" label: "Dismiss Discover Edge modal" - # Learn More button + # Learn More button (From Assets scene so no conflict with promo card "Learn More" button) + - tapOn: Assets - tapOn: id: "notifBackup" + - extendedWaitUntil: + visible: Continue with Guest Account - tapOn: Learn More - assertVisible: "Guest Account" # Support article title - launchApp: @@ -213,6 +215,8 @@ tags: - tapOn: Continue with Guest Account # Learn More button - tapOn: Back Up Account + - extendedWaitUntil: + visible: Continue with Guest Account - tapOn: Learn More - assertVisible: "Guest Account" # Support article title - launchApp: From 20257618c7f8b2297545ae02ab3e3558fe7115ac Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 11 Dec 2025 19:00:58 -0800 Subject: [PATCH 44/77] Fix modal dismiss for some tests --- .../01-accounts/C000000-change-password.yaml | 4 ++++ .../C000001-switch-and-forget-account.yaml | 19 +++---------------- .../C000015-ip-validation-reminder.yaml | 9 +++++++++ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/maestro/01-accounts/C000000-change-password.yaml b/maestro/01-accounts/C000000-change-password.yaml index 42cb7b41638..df9573e6d8a 100644 --- a/maestro/01-accounts/C000000-change-password.yaml +++ b/maestro/01-accounts/C000000-change-password.yaml @@ -22,6 +22,10 @@ tags: file: ../common/create-account.yaml label: "Create new account" +# Dismiss modals +- runFlow: + file: ../common/dismiss-modals.yaml + # Navigate to Settings - tapOn: id: "sideMenuButton" diff --git a/maestro/01-accounts/C000001-switch-and-forget-account.yaml b/maestro/01-accounts/C000001-switch-and-forget-account.yaml index b24afd5c769..a903d87ed0d 100644 --- a/maestro/01-accounts/C000001-switch-and-forget-account.yaml +++ b/maestro/01-accounts/C000001-switch-and-forget-account.yaml @@ -31,22 +31,9 @@ tags: USERNAME: ${USERNAME1} PASSWORD: ${PASSWORD1} -# Dismiss allow-notifications modal -- extendedWaitUntil: - visible: "Security is Our Priority" - timeout: 10000 - optional: true -- tapOn: - text: "Cancel" - optional: true -# If the survey modal shows, dismiss it -- extendedWaitUntil: - visible: "How Did You Discover Edge?" - timeout: 10000 - optional: true -- tapOn: - text: "Dismiss" - optional: true +# Dismiss modals +- runFlow: + file: ../common/dismiss-modals.yaml # Login into account #2 - tapOn: diff --git a/maestro/12-notifications/C000015-ip-validation-reminder.yaml b/maestro/12-notifications/C000015-ip-validation-reminder.yaml index 41731c5face..ac075d25411 100644 --- a/maestro/12-notifications/C000015-ip-validation-reminder.yaml +++ b/maestro/12-notifications/C000015-ip-validation-reminder.yaml @@ -20,6 +20,15 @@ tags: file: ../common/create-account.yaml label: "Create new account" +# Dismiss web3 handle modal +- runFlow: + when: + visible: "Claim Your Web3 Handle" + commands: + - tapOn: + text: "Not Now" + label: "Dismiss Web3 Handle Modal" + # Proper notification card is displayed # Note: Unable to assert text in this notification card # - assertVisible: IP Validation Protection From 6c5db63e93b0ffcd2e35a1da5b57dbf34e036f35 Mon Sep 17 00:00:00 2001 From: Jared Date: Fri, 12 Dec 2025 12:26:08 -0800 Subject: [PATCH 45/77] Make notifications modal dismiss optional Separate test will validate modal --- maestro/01-accounts/C000006-light-account.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/maestro/01-accounts/C000006-light-account.yaml b/maestro/01-accounts/C000006-light-account.yaml index 557f3da9316..58b22439746 100644 --- a/maestro/01-accounts/C000006-light-account.yaml +++ b/maestro/01-accounts/C000006-light-account.yaml @@ -71,6 +71,7 @@ tags: - extendedWaitUntil: visible: Security is Our Priority timeout: 15000 + optional: true - runFlow: when: visible: "Security is Our Priority" From 022934c50ee677f823b8e62cb02f1a7df2cb2832 Mon Sep 17 00:00:00 2001 From: Jared Date: Wed, 10 Dec 2025 18:20:41 -0800 Subject: [PATCH 46/77] Improve create account flow --- maestro/common/create-account.yaml | 62 ++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/maestro/common/create-account.yaml b/maestro/common/create-account.yaml index 0d974d525ae..d3fbb6b4032 100644 --- a/maestro/common/create-account.yaml +++ b/maestro/common/create-account.yaml @@ -10,20 +10,44 @@ env: NEW_ACCOUNT_PASSWORD: ${NEW_ACCOUNT_PASSWORD || MAESTRO_EDGE_NEW_ACCOUNT_PASSWORD} NEW_ACCOUNT_PIN: ${NEW_ACCOUNT_PIN || MAESTRO_EDGE_NEW_ACCOUNT_PIN} NEW_WALLETS: ${NEW_WALLETS || ""} + DEFAULT_WALLETS: '["Bitcoin", "Ethereum", "Litecoin", "Bitcoin Cash", "Dash"]' --- # Start account creation flow +# # In case of retries - +# - runFlow: +# when: +# notVisible: "Already have an account? Sign in" +# commands: +# - runFlow: +# file: ../common/launch-cleared.yaml + + - tapOn: Already have an account? Sign in - waitForAnimationToEnd: timeout: 10000 - tapOn: Create account -- extendedWaitUntil: - visible: "Choose Username" - timeout: 10000 +# Captcha +- assertVisible: + text: "Are you a human?" + optional: true +- assertVisible: + text: ".*ALTCHA.*" + optional: true +- assertVisible: Choose Username +- assertVisible: "Your username will be required.*" # Enter and save username # Wait for altcha to complete - waitForAnimationToEnd: timeout: 10000 +- extendedWaitUntil: + visible: Next + timeout: 10000 + +# Enter new account username +# - tapOn: +# id: "undefined.clearIcon" +# - tapOn: Username - inputRandomText - copyTextFrom: id: "undefined.textInput" @@ -31,6 +55,8 @@ env: - waitForAnimationToEnd: timeout: 10000 - assertVisible: Username available + +# Continue - assertVisible: Next - pressKey: key: Enter @@ -54,6 +80,7 @@ env: # Set PIN # Using UTXO creds for now +- assertVisible: Set PIN - inputText: ${MAESTRO_EDGE_NEW_ACCOUNT_PIN} - tapOn: Next @@ -62,13 +89,22 @@ env: - tapOn: "I understand that if I lose this device or uninstall the app, my digital assets can only be recovered with my username and password" - tapOn: "I understand that if I lose my username and password, Edge will not be able to recover my account, unless I set up password recovery" - tapOn: "I understand that I am responsible for safekeeping of my passwords, private key pairs, PIN, and any other codes to access the software. Edge is not responsible if my information is compromised or accessed by a 3rd party where funds are lost" +- waitForAnimationToEnd +- assertVisible: "I have read, understood, and agree to the Terms of Use" - tapOn: "Confirm" +# Creating account screen +- assertVisible: "Great Job!" +- assertVisible: "Hang tight while we create.*" + # Create button fades in - may encounter captcha -# Use regex to match "View account information" or "View account information xx" (iOS) - extendedWaitUntil: visible: ${"View account information.*"} - timeout: 15000 + timeout: 45000 +- assertVisible: "Tap the dropdown below to review your account information" +- assertVisible: "Warning!.*" +- assertVisible: "If you lose your account information.*" +# Dropdown correct - tapOn: ${"View account information.*"} - assertVisible: "Username:" - assertVisible: ${output.newAccountUsername} @@ -95,25 +131,27 @@ env: true: ${NEW_WALLETS} commands: # Remove default wallets - - evalScript: ${var defaults = ["Bitcoin", "Ethereum", "Litecoin", "Bitcoin Cash", "Dash"]} + - evalScript: ${var wallets = JSON.parse(NEW_WALLETS)} + - evalScript: ${var defaults = JSON.parse(DEFAULT_WALLETS)} + - evalScript: ${var filteredDefaults = defaults.filter(d => !wallets.includes(d))} + - evalScript: ${var filteredWallets = wallets.filter(w => !defaults.includes(w))} - evalScript: ${var defIndex = 0} - repeat: - times: ${defaults.length} + times: ${filteredDefaults.length} commands: - - tapOn: ${".*" + defaults[defIndex]} + - tapOn: ${".*" + filteredDefaults[defIndex]} - evalScript: ${defIndex++} # Add new wallets - evalScript: ${var newIndex = 0} - - evalScript: ${var wallets = JSON.parse(NEW_WALLETS)} - repeat: - times: ${wallets.length} + times: ${filteredWallets.length} commands: - tapOn: Search Wallets - tapOn: id: "undefined.clearIcon" - - inputText: ${wallets[newIndex]} + - inputText: ${filteredWallets[newIndex]} - tapOn: - text: ${".*" + wallets[newIndex]} + text: ${".*" + filteredWallets[newIndex]} index: 1 - evalScript: ${newIndex++} From 93a4084986a36741cba49072b4337f48a9434398 Mon Sep 17 00:00:00 2001 From: Jared Date: Mon, 15 Dec 2025 16:37:52 -0800 Subject: [PATCH 47/77] Add UFO balance to UTXO test --- maestro/10-assets/C184035-utxo-01.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maestro/10-assets/C184035-utxo-01.yaml b/maestro/10-assets/C184035-utxo-01.yaml index 7ab7f81d5e8..1c995f8afe3 100644 --- a/maestro/10-assets/C184035-utxo-01.yaml +++ b/maestro/10-assets/C184035-utxo-01.yaml @@ -159,7 +159,7 @@ tags: timeout: 50000 - tapOn: UFO - extendedWaitUntil: - visible: "0 UFO" + visible: "1,337 UFO" timeout: 15000 - tapOn: Assets From f131122450b6ed277f4afcbf8de7d1278cdc55db Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 17 Dec 2025 00:51:15 -0800 Subject: [PATCH 48/77] Fix tcy stake plugin tokenId in quote allocation --- eslint.config.mjs | 2 +- .../generic/policyAdapters/ThorchainYieldAdaptor.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 04967bbf893..8f73693c035 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -467,7 +467,7 @@ export default [ 'src/plugins/stake-plugins/generic/policyAdapters/EthereumKilnAdaptor.ts', 'src/plugins/stake-plugins/generic/policyAdapters/GlifInfinityPoolAdapter.ts', 'src/plugins/stake-plugins/generic/policyAdapters/TarotPoolAdaptor.ts', - 'src/plugins/stake-plugins/generic/policyAdapters/ThorchainYieldAdaptor.ts', + 'src/plugins/stake-plugins/generic/util/EdgeWalletSigner.ts', 'src/plugins/stake-plugins/generic/util/KilnApi.ts', 'src/plugins/stake-plugins/generic/util/tarotUtils.ts', diff --git a/src/plugins/stake-plugins/generic/policyAdapters/ThorchainYieldAdaptor.ts b/src/plugins/stake-plugins/generic/policyAdapters/ThorchainYieldAdaptor.ts index f75783c6939..2d86668d976 100644 --- a/src/plugins/stake-plugins/generic/policyAdapters/ThorchainYieldAdaptor.ts +++ b/src/plugins/stake-plugins/generic/policyAdapters/ThorchainYieldAdaptor.ts @@ -147,7 +147,7 @@ export const makeThorchainYieldAdapter = ( { allocationType: 'stake', pluginId: requestAssetId.pluginId, - tokenId: null, + tokenId: requestAssetId.tokenId, currencyCode: requestAssetId.currencyCode, nativeAmount: requestNativeAmount }, @@ -160,7 +160,7 @@ export const makeThorchainYieldAdapter = ( } ] - const approve = async () => { + const approve = async (): Promise => { let signedTx = await wallet.signTx(edgeTx) signedTx = await wallet.broadcastTx(signedTx) await wallet.saveTx(signedTx) @@ -241,7 +241,7 @@ export const makeThorchainYieldAdapter = ( } ] - const approve = async () => { + const approve = async (): Promise => { let signedTx = await wallet.signTx(edgeTx) signedTx = await wallet.broadcastTx(signedTx) await wallet.saveTx(signedTx) From e6b619d339a510263bc2a72c40aa82628c665f0c Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 19 Dec 2025 12:53:13 -0800 Subject: [PATCH 49/77] Upgrade edge-currency-accountbased@^4.68.0 --- ios/Podfile.lock | 4 ++-- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c8762b2dd4b..7fd98e36b4f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -17,7 +17,7 @@ PODS: - DoubleConversion (1.1.6) - edge-core-js (2.37.0): - React-Core - - edge-currency-accountbased (4.67.0): + - edge-currency-accountbased (4.68.0): - React-Core - edge-currency-plugins (3.8.9): - React-Core @@ -3334,7 +3334,7 @@ SPEC CHECKSUMS: disklet: 8a20bf8a568635b6e6bb8f93297dac13ee5cef98 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb edge-core-js: 248f7d28942a5ea6c9835eca6f9f16969c89476c - edge-currency-accountbased: 993920e46f000e04df92d0a49eabb57973096d1c + edge-currency-accountbased: b526ee12efefad410125c51135222b0c63e42f12 edge-currency-plugins: 0d8a1a8da63672342cbc9bd5055feb4b397544e7 edge-exchange-plugins: f35930ddcd5a4551a6e45334cb3f4c0295c23acd edge-login-ui-rn: c9648a772533c092f4526a189cd4da9d6f729639 diff --git a/package.json b/package.json index 14a951bb1c6..fad9096a7e4 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "detect-bundler": "^1.1.0", "disklet": "^0.5.2", "edge-core-js": "^2.37.0", - "edge-currency-accountbased": "^4.67.0", + "edge-currency-accountbased": "^4.68.0", "edge-currency-monero": "^2.0.1", "edge-currency-plugins": "^3.8.9", "edge-exchange-plugins": "^2.40.2", diff --git a/yarn.lock b/yarn.lock index 15d7e5dcbb4..054d7b0b812 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9448,10 +9448,10 @@ edge-core-js@^2.37.0: yaob "^0.3.12" yavent "^0.1.5" -edge-currency-accountbased@^4.67.0: - version "4.67.0" - resolved "https://registry.yarnpkg.com/edge-currency-accountbased/-/edge-currency-accountbased-4.67.0.tgz#15e984ceb2d32455fc5c163f1def47184bd80b74" - integrity sha512-TGvp/nQGjkMKwENY7f842GLnir0Q2k4SXkEzz6vATBiB/S6zMkNJW6sTmG9+VyVG3SP1KDoSinnjEL/oObTcdA== +edge-currency-accountbased@^4.68.0: + version "4.68.0" + resolved "https://registry.yarnpkg.com/edge-currency-accountbased/-/edge-currency-accountbased-4.68.0.tgz#aa964d77ccd1b681c54e30b02c2bd3bcbf64f058" + integrity sha512-ZfsDKiromCqlNfh9aFGh4jzZCtSE9nL69LysUikjZOgzLB+nl8V68vFSUPFwTkzAHdBJ8IM2ut8ZNE8B8NQ9dQ== dependencies: "@chain-registry/client" "^2.0.28" "@chain-registry/types" "^2.0.28" From ce35e0629bea31d7d8d723a8e40f16980ea9629a Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Sun, 14 Dec 2025 11:38:37 -0800 Subject: [PATCH 50/77] Add Zcash buy/sell support with Banxa Add ZEC to CURRENCY_PLUGINID_MAP in the Banxa ramp plugin to enable Zcash buy/sell when supported by Banxa's API. --- CHANGELOG.md | 2 ++ src/plugins/ramps/banxa/banxaRampPlugin.ts | 33 ++++++++++++++-------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca32240e515..aa4a9d32c6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased (develop) +- added: Zcash buy/sell support with Banxa + ## 4.41.0 (staging) - changed: Replace `currencyCode` usage with `EdgeTokenId` throughout the app diff --git a/src/plugins/ramps/banxa/banxaRampPlugin.ts b/src/plugins/ramps/banxa/banxaRampPlugin.ts index 16b48e309c7..16aca6fe0a5 100644 --- a/src/plugins/ramps/banxa/banxaRampPlugin.ts +++ b/src/plugins/ramps/banxa/banxaRampPlugin.ts @@ -307,7 +307,8 @@ const CURRENCY_PLUGINID_MAP: Record = { TON: 'ton', XLM: 'stellar', XRP: 'ripple', - XTZ: 'tezos' + XTZ: 'tezos', + ZEC: 'zcash' } const COIN_TO_CURRENCY_CODE_MAP: StringMap = { BTC: 'BTC' } @@ -1206,16 +1207,24 @@ export const banxaRampPlugin: RampPluginFactory = ( ) return } - // Prefer segwit where available; fallback to default public address + // Prefer transparent or segwit address where available; fallback to default const addresses = await coreWallet.getAddresses({ tokenId: null }) - const [defaultAddress] = addresses - if (defaultAddress == null) - throw new Error('Banxa missing receive address') - const segwitAddress = addresses.find( - row => row.addressType === 'segwitAddress' + const getAddressTypePriority = ( + type: string | undefined + ): number => { + if (type === 'transparentAddress') return 1 + if (type === 'segwitAddress') return 1 + return 2 + } + // Sort addresses by priority + addresses.sort( + (a, b) => + getAddressTypePriority(a.addressType) - + getAddressTypePriority(b.addressType) ) - const receivePublicAddress = - segwitAddress?.publicAddress ?? defaultAddress.publicAddress + const [receiveAddress] = addresses + if (receiveAddress == null) + throw new Error('Banxa missing receive address') const bodyParams: any = { payment_method_id: paymentObj?.id ?? '', @@ -1240,13 +1249,13 @@ export const banxaRampPlugin: RampPluginFactory = ( if (testnet && banxaChain === 'BTC') { bodyParams.wallet_address = TESTNET_ADDRESS } else { - bodyParams.wallet_address = receivePublicAddress + bodyParams.wallet_address = receiveAddress.publicAddress } } else { if (testnet && banxaChain === 'BTC') { bodyParams.refund_address = TESTNET_ADDRESS } else { - bodyParams.refund_address = receivePublicAddress + bodyParams.refund_address = receiveAddress.publicAddress } } @@ -1482,7 +1491,7 @@ export const banxaRampPlugin: RampPluginFactory = ( // Post the txid back to Banxa const bodyParams = { tx_hash: txid, - source_address: receivePublicAddress, + source_address: receiveAddress.publicAddress, destination_address: publicAddress } await banxaFetch({ From 4955a9be9ccd2565dc46b6bcf4f271eef6ebc772 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 19 Dec 2025 13:14:00 -0800 Subject: [PATCH 51/77] Upgrade edge-core-js@^2.38.1 --- ios/Podfile.lock | 4 ++-- package.json | 2 +- src/constants/txActionConstants.ts | 1 + src/locales/en_US.ts | 1 + src/locales/strings/enUS.json | 1 + yarn.lock | 8 ++++---- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7fd98e36b4f..4670cf4bae3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -15,7 +15,7 @@ PODS: - disklet (0.5.2): - React - DoubleConversion (1.1.6) - - edge-core-js (2.37.0): + - edge-core-js (2.38.1): - React-Core - edge-currency-accountbased (4.68.0): - React-Core @@ -3333,7 +3333,7 @@ SPEC CHECKSUMS: CNIOWindows: 3047f2d8165848a3936a0a755fee27c6b5ee479b disklet: 8a20bf8a568635b6e6bb8f93297dac13ee5cef98 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb - edge-core-js: 248f7d28942a5ea6c9835eca6f9f16969c89476c + edge-core-js: 2da898293870d04110733ebf3bdc495a7c659065 edge-currency-accountbased: b526ee12efefad410125c51135222b0c63e42f12 edge-currency-plugins: 0d8a1a8da63672342cbc9bd5055feb4b397544e7 edge-exchange-plugins: f35930ddcd5a4551a6e45334cb3f4c0295c23acd diff --git a/package.json b/package.json index fad9096a7e4..9af837bd2dc 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "deprecated-react-native-prop-types": "^5.0.0", "detect-bundler": "^1.1.0", "disklet": "^0.5.2", - "edge-core-js": "^2.37.0", + "edge-core-js": "^2.38.1", "edge-currency-accountbased": "^4.68.0", "edge-currency-monero": "^2.0.1", "edge-currency-plugins": "^3.8.9", diff --git a/src/constants/txActionConstants.ts b/src/constants/txActionConstants.ts index 0138427f0ee..13d811cb8ec 100644 --- a/src/constants/txActionConstants.ts +++ b/src/constants/txActionConstants.ts @@ -6,6 +6,7 @@ export const TX_ACTION_LABEL_MAP: Record = { buy: lstrings.transaction_details_bought_1s, claim: lstrings.transaction_details_claim, claimOrder: lstrings.transaction_details_claim_order, + giftCard: lstrings.transaction_details_gift_card, sell: lstrings.transaction_details_sold_1s, sellNetworkFee: lstrings.fiat_plugin_sell_network_fee, swap: lstrings.transaction_details_swap, diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 98466e12609..617988d33c9 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -878,6 +878,7 @@ const strings = { transaction_details_swap_order_fill: 'Swap Order Filled', transaction_details_claim: 'Claim Staked Funds', transaction_details_claim_order: 'Claim Order', + transaction_details_gift_card: 'Gift Card Purchase', transaction_details_stake: 'Stake Funds', transaction_details_stake_order: 'Stake Order', transaction_details_stake_network_fee: 'Stake Network Fee', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 0c477d435e7..6dd15164287 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -704,6 +704,7 @@ "transaction_details_swap_order_fill": "Swap Order Filled", "transaction_details_claim": "Claim Staked Funds", "transaction_details_claim_order": "Claim Order", + "transaction_details_gift_card": "Gift Card Purchase", "transaction_details_stake": "Stake Funds", "transaction_details_stake_order": "Stake Order", "transaction_details_stake_network_fee": "Stake Network Fee", diff --git a/yarn.lock b/yarn.lock index 054d7b0b812..45b08a42117 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9423,10 +9423,10 @@ ed25519@0.0.4: bindings "^1.2.1" nan "^2.0.9" -edge-core-js@^2.37.0: - version "2.37.0" - resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-2.37.0.tgz#1f215676acacbb694b78208145faf476c5ae3139" - integrity sha512-DwR9VJXmB8WgXuMaF8/iqsbGgE7XCNBre/L1dhrHe0eKHwqAPlU/tYXESxXygDOumlQN0x4qbDfzOUxs8NtpqQ== +edge-core-js@^2.38.1: + version "2.38.1" + resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-2.38.1.tgz#983aea8120b32f6b7165c066e4e8b74062989bc7" + integrity sha512-S3DGZJJKIWAOn2AL/MRJWq2RD/jXaDKEqGT0jl8KyWPwbiiNWS9QQXKEWc/UhK79i7y6UOEuq3iVeRflB4yyFQ== dependencies: aes-js "^3.1.0" base-x "^4.0.0" From 586712d601eb4d65d12a658eaa66428d79f6c35e Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 19 Dec 2025 13:29:30 -0800 Subject: [PATCH 52/77] Upgrade edge-currency-plugins@^3.8.10 --- ios/Podfile.lock | 4 ++-- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4670cf4bae3..eb6e3ab5b3b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -19,7 +19,7 @@ PODS: - React-Core - edge-currency-accountbased (4.68.0): - React-Core - - edge-currency-plugins (3.8.9): + - edge-currency-plugins (3.8.10): - React-Core - edge-exchange-plugins (2.40.2): - React-Core @@ -3335,7 +3335,7 @@ SPEC CHECKSUMS: DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb edge-core-js: 2da898293870d04110733ebf3bdc495a7c659065 edge-currency-accountbased: b526ee12efefad410125c51135222b0c63e42f12 - edge-currency-plugins: 0d8a1a8da63672342cbc9bd5055feb4b397544e7 + edge-currency-plugins: 6b3341707a6a5c74f837a012768dd2f6c55a691b edge-exchange-plugins: f35930ddcd5a4551a6e45334cb3f4c0295c23acd edge-login-ui-rn: c9648a772533c092f4526a189cd4da9d6f729639 EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8 diff --git a/package.json b/package.json index 9af837bd2dc..55f93b8b5c2 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "edge-core-js": "^2.38.1", "edge-currency-accountbased": "^4.68.0", "edge-currency-monero": "^2.0.1", - "edge-currency-plugins": "^3.8.9", + "edge-currency-plugins": "^3.8.10", "edge-exchange-plugins": "^2.40.2", "edge-info-server": "^3.10.0", "edge-login-ui-rn": "^3.34.6", diff --git a/yarn.lock b/yarn.lock index 45b08a42117..49e1d337419 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9520,10 +9520,10 @@ edge-currency-monero@^2.0.1: buffer "^5.0.6" uri-js "^3.0.2" -edge-currency-plugins@^3.8.9: - version "3.8.9" - resolved "https://registry.yarnpkg.com/edge-currency-plugins/-/edge-currency-plugins-3.8.9.tgz#ba11e44acd2819ad84f417ce2a50965ac7ecd33f" - integrity sha512-iMSWm4W3GV9ZvSzlsCCDgQj3ZuNRAiahZyPy3qMP78LijGifENwahhTgV7oYnLsfvpQYQ+58VzQbGzJ3UOebYg== +edge-currency-plugins@^3.8.10: + version "3.8.10" + resolved "https://registry.yarnpkg.com/edge-currency-plugins/-/edge-currency-plugins-3.8.10.tgz#b154ddf945287645bbb5ca0678187443a5f4ad2e" + integrity sha512-N7MaPL2YuIS8ADH9oKvcyvl772PePf5SPoCyu5IYmpXUDPe1204wNEbGgyGAyeAVnzzflIrjfnzuohjPaWgpaA== dependencies: "@bitcoinerlab/secp256k1" "^1.2.0" altcoin-js "^1.0.0" From 499b11cb6917a1c5fcaf48cd40eaf9bc9471da26 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Mon, 15 Dec 2025 15:30:14 -0800 Subject: [PATCH 53/77] Disable layout animation for elements in WelcomHero Layout animations on elements within WelcomeHero were causing visually slow gesture-based animation. Disabling them ensures smoother rendering of the gesture-based animation during the welcome flow. --- src/components/scenes/GettingStartedScene.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/scenes/GettingStartedScene.tsx b/src/components/scenes/GettingStartedScene.tsx index 7df81b02d05..0bf2d8b7e62 100644 --- a/src/components/scenes/GettingStartedScene.tsx +++ b/src/components/scenes/GettingStartedScene.tsx @@ -228,6 +228,7 @@ export const GettingStartedScene: React.FC = props => { = props => { = props => { = props => { Date: Mon, 15 Dec 2025 16:21:48 -0800 Subject: [PATCH 54/77] Fix fly-in "Next" button animation on Android onboarding screen Use layout positioning (not absolute nor relative) for ButtonFadeContainer on both platforms to prevent layout shift when buttons swap visibility. This fixes the issue where the "Next" button would fly in from the upper section of the screen on Android instead of fading in place like on iOS. Also unified TertiaryTouchable margins across platforms. The HACK was introduced in PR #5590 (May 2025) as a workaround after PR #5566 fixed a button animation issue using absolute positioning but broke iOS. The conditional styling attempted to address "iOS/Android parity mismatches when the animation fires" without understanding the root cause. The actual root cause is that EdgeAnim applies `layout={LAYOUT_ANIMATION}` (LinearTransition) to all animated views. With relative positioning, when the "Get Started" button unmounts and "Next" mounts, there's a layout shift that triggers the layout animation - causing the button to fly in from its initial position. Absolute positioning prevents any layout shift, so the layout animation has nothing to animate, and the button simply fades in place as intended. This is the same approach the original fix (PR #5566) used, but now correctly applied to both platforms. --- src/components/scenes/GettingStartedScene.tsx | 104 ++++++++---------- 1 file changed, 47 insertions(+), 57 deletions(-) diff --git a/src/components/scenes/GettingStartedScene.tsx b/src/components/scenes/GettingStartedScene.tsx index 0bf2d8b7e62..7052e879356 100644 --- a/src/components/scenes/GettingStartedScene.tsx +++ b/src/components/scenes/GettingStartedScene.tsx @@ -182,38 +182,6 @@ export const GettingStartedScene: React.FC = props => { } }) - const footerButtons = ( - <> - - - - - - - - - - - - {lstrings.getting_started_already_have_an_account} - {lstrings.getting_started_sign_in} - - - - ) - return ( @@ -334,7 +302,44 @@ export const GettingStartedScene: React.FC = props => { ) })} - {footerButtons} + + + + + + + + + + + + + {lstrings.getting_started_already_have_an_account} + {lstrings.getting_started_sign_in} + + @@ -346,16 +351,10 @@ export const GettingStartedScene: React.FC = props => { // Local Components // ----------------------------------------------------------------------------- -const TertiaryTouchable = styled(EdgeTouchableOpacity)(theme => { - const platform = Platform.OS - // HACK: Address iOS/Android parity mismatches when the animation fires - return { - marginVertical: platform === 'ios' ? undefined : theme.rem(0.5), - marginBottom: platform === 'ios' ? theme.rem(0.5) : undefined, - marginTop: platform === 'ios' ? theme.rem(4.5) : undefined, - alignItems: 'center' - } -}) +const TertiaryTouchable = styled(EdgeTouchableOpacity)(theme => ({ + marginVertical: theme.rem(0.5), + alignItems: 'center' +})) const TertiaryText = styled(EdgeText)(theme => props => ({ color: theme.textInputTextColorDisabled @@ -598,6 +597,7 @@ const Sections = styled(Animated.View)<{ const { swipeOffset } = props return [ { + flexGrow: 1, paddingBottom: theme.rem(1) }, useAnimatedStyle(() => { @@ -662,17 +662,7 @@ const Footnote = styled(EdgeText)(theme => ({ includeFontPadding: false })) -const ButtonFadeContainer = styled(View)(theme => { - // HACK: Address iOS/Android parity mismatches when the animation fires - return Platform.OS === 'ios' - ? { - position: 'absolute', - bottom: theme.rem(5), - left: 0, - right: 0, - zIndex: 1 - } - : { - position: 'relative' - } -}) +const ButtonFadeContainer = styled(View)(theme => ({ + flexShrink: 1, + flexGrow: 0 +})) From 625b8769e197d63fa30c657b7c6ae74a82e7b4fe Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 17 Dec 2025 17:02:43 -0800 Subject: [PATCH 55/77] Remove styled components from GettingStartedScene Replace styled() usage with a single getStyles cacheStyles pattern. Extract reusable animated components (HeroItem, PageIndicator, SectionItem) placed at the bottom of the file. Replace showNextButton state with animated button opacity. Add EdgeAnim fade-in for pagination and button container. Also fixes bug where static flexGrow: 1 was overridden by animated style. --- src/components/scenes/GettingStartedScene.tsx | 806 +++++++++--------- 1 file changed, 400 insertions(+), 406 deletions(-) diff --git a/src/components/scenes/GettingStartedScene.tsx b/src/components/scenes/GettingStartedScene.tsx index 7052e879356..3083e8f27f3 100644 --- a/src/components/scenes/GettingStartedScene.tsx +++ b/src/components/scenes/GettingStartedScene.tsx @@ -1,6 +1,7 @@ import * as React from 'react' -import { Image, Platform, Pressable, View } from 'react-native' +import { Image, Pressable, View } from 'react-native' import { GestureDetector, ScrollView } from 'react-native-gesture-handler' +import { cacheStyles } from 'react-native-patina' import Animated, { Extrapolation, interpolate, @@ -30,11 +31,11 @@ import type { ImageProp } from '../../types/Theme' import { parseMarkedText } from '../../util/parseMarkedText' import { logEvent } from '../../util/tracking' import { ButtonsView } from '../buttons/ButtonsView' -import { EdgeAnim, fadeIn, fadeOut } from '../common/EdgeAnim' +import { EdgeAnim } from '../common/EdgeAnim' import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' import { SceneWrapper } from '../common/SceneWrapper' -import { styled } from '../hoc/styled' import { Space } from '../layout/Space' +import { type Theme, useTheme } from '../services/ThemeContext' import { UnscaledText } from '../text/UnscaledText' import { EdgeText } from '../themed/EdgeText' @@ -85,21 +86,15 @@ const sections: SectionData[] = [ export const GettingStartedScene: React.FC = props => { const { navigation, route } = props const dispatch = useDispatch() + const theme = useTheme() + const styles = getStyles(theme) + const insets = useSafeAreaInsets() const { experimentConfig } = route.params const context = useSelector(state => state.core.context) const hasLocalUsers = context.localUsers.length > 0 - - // Which button label to show: "Get Started" or "Next" - const [showNextButton, setShowNextButton] = React.useState(false) + const { width: screenWidth } = useSafeAreaFrame() const handleIndexChange = (index: number): void => { - // Update the button visibility based on scrollIndex - if (index > 0 && !showNextButton) { - setShowNextButton(true) - } else if (index <= 0 && showNextButton) { - setShowNextButton(false) - } - // Redirect to login or new account screen // if the user swipes past the last USP section if (index === paginationCount) { @@ -109,7 +104,6 @@ export const GettingStartedScene: React.FC = props => { // Section 0 is the welcome hero, which isn't in the array: const paginationCount = sections.length + 1 - const { width: screenWidth } = useSafeAreaFrame() const { gesture, scrollIndex } = useCarouselGesture( // Add 1 so we can swipe off the end: paginationCount + 1, @@ -156,7 +150,7 @@ export const GettingStartedScene: React.FC = props => { } }) - const handlePressIndicator = useHandler((itemIndex: number) => { + const handlePressIndicator = useHandler((itemIndex: number) => () => { scrollIndex.value = withTiming(itemIndex) handleIndexChange(itemIndex) }) @@ -182,19 +176,109 @@ export const GettingStartedScene: React.FC = props => { } }) + // --------------------------------------------------------------------------- + // Animated Styles + // --------------------------------------------------------------------------- + + // Skip button animation + const skipButtonAnimatedStyle = useAnimatedStyle(() => ({ + opacity: interpolate(scrollIndex.value, [0, 1], [0, 1], Extrapolation.CLAMP) + })) + + // Welcome hero animation + const welcomeHeroAnimatedStyle = useAnimatedStyle(() => ({ + opacity: interpolate(scrollIndex.value, [0, 0.5], [1, 0]), + transform: [ + { + scale: interpolate( + scrollIndex.value, + [0, 1], + [1, 0], + Extrapolation.CLAMP + ) + } + ] + })) + + // Section cover animation + const themeRem = theme.rem(1) + const themeModal = theme.modal + const themeModalLikeBackground = theme.modalLikeBackground + + const sectionCoverAnimatedStyle = useAnimatedStyle(() => { + const backgroundColor = interpolateColor( + scrollIndex.value, + [0, 1], + [`${themeModal}00`, themeModalLikeBackground] + ) + const paddingVertical = interpolate( + scrollIndex.value, + [0, 1], + [0, themeRem], + Extrapolation.CLAMP + ) + const flexGrow = interpolate( + scrollIndex.value, + [0, 1], + [0, 1.2], + Extrapolation.CLAMP + ) + return { backgroundColor, paddingVertical, flexGrow } + }) + + const sectionCoverStaticStyle = React.useMemo( + () => ({ + alignItems: 'stretch' as const, + justifyContent: 'flex-end' as const, + paddingVertical: theme.rem(1), + paddingBottom: insets.bottom + theme.rem(1), + marginBottom: -insets.bottom + }), + [theme, insets.bottom] + ) + + // Sections container animation + const sectionsAnimatedStyle = useAnimatedStyle(() => ({ + flexGrow: interpolate(scrollIndex.value, [0, 1], [0, 1.5]) + })) + + // Button animations - "Get Started" at index 0, "Next" at index > 0 + const getStartedButtonAnimatedStyle = useAnimatedStyle(() => ({ + opacity: interpolate( + scrollIndex.value, + [0, 0.5], + [1, 0], + Extrapolation.CLAMP + ) + })) + const nextButtonAnimatedStyle = useAnimatedStyle(() => ({ + opacity: interpolate( + scrollIndex.value, + [0, 0.5], + [0, 1], + Extrapolation.CLAMP + ) + })) + + // --------------------------------------------------------------------------- + // Render + // --------------------------------------------------------------------------- + return ( - + {lstrings.skip} - + - - - + + + = props => { distance: 60 }} > - {parseMarkedText(lstrings.getting_started_welcome_title)} - + = props => { distance: 40 }} > - + {lstrings.getting_started_welcome_message} - + = props => { }} > - {lstrings.learn_more} + + {lstrings.learn_more} + - - {sections.map((section, index) => { - return ( - + {sections.map((section, index) => ( + + ))} + + + + {Array.from({ length: paginationCount }).map((_, index) => ( + + + + ))} + + + + + {sections.map((section, index) => ( + - - - - - ) - })} - - - {Array.from({ length: paginationCount }).map((_, index) => ( - { - handlePressIndicator(index) - }} - > - - - ))} - - - - {sections.map((section, index) => { - return ( -
- - - {parseMarkedText(section.title)} - - - {section.message} - - {section.footnote == null ? null : ( - - {lstrings.getting_started_slide_1_footnote} - - )} - -
- ) - })} -
- - - - - + ))} + + - + + + + + + - - - + + + {lstrings.getting_started_already_have_an_account} - {lstrings.getting_started_sign_in} - - -
-
+ + {lstrings.getting_started_sign_in} + + + + +
) } // ----------------------------------------------------------------------------- -// Local Components +// Styles // ----------------------------------------------------------------------------- -const TertiaryTouchable = styled(EdgeTouchableOpacity)(theme => ({ - marginVertical: theme.rem(0.5), - alignItems: 'center' -})) - -const TertiaryText = styled(EdgeText)(theme => props => ({ - color: theme.textInputTextColorDisabled +const getStyles = cacheStyles((theme: Theme) => ({ + container: { + flex: 1 + }, + heroContainer: { + flex: 1, + alignItems: 'center' + }, + welcomeHero: { + alignItems: 'center', + justifyContent: 'center', + flex: 1 + }, + welcomeHeroTitle: { + color: theme.primaryText, + fontFamily: theme.fontFaceDefault, + fontSize: theme.rem(2.25), + includeFontPadding: false, + lineHeight: theme.rem(2.8), + paddingVertical: theme.rem(1), + textAlign: 'center' + }, + welcomeHeroMessage: { + fontSize: theme.rem(0.78), + paddingVertical: theme.rem(1), + textAlign: 'center' + }, + welcomeHeroPrompt: { + fontSize: theme.rem(0.75), + color: theme.textLink, + fontFamily: theme.fontFaceBold, + textAlign: 'center', + margin: theme.rem(0.5) + }, + heroItem: { + alignItems: 'center', + aspectRatio: 1, + padding: theme.rem(1), + position: 'absolute', + height: '100%', + width: '100%' + }, + heroImageContainer: { + alignItems: 'stretch', + aspectRatio: 1, + backgroundColor: 'white', + borderRadius: 1000, + maxHeight: '100%', + overflow: 'hidden', + width: '100%' + }, + heroImage: { + maxHeight: '100%', + maxWidth: '100%', + aspectRatio: 1 + }, + pagination: { + flexDirection: 'row', + justifyContent: 'center', + marginVertical: theme.rem(0.7) + }, + pageIndicator: { + borderRadius: 10, + margin: theme.rem(0.3), + height: theme.rem(0.6), + width: theme.rem(0.6) + }, + sections: { + paddingBottom: theme.rem(1) + }, + section: { + marginHorizontal: theme.rem(2), + position: 'absolute', + height: '100%' + }, + sectionTitle: { + color: theme.primaryText, + fontFamily: theme.fontFaceDefault, + fontSize: theme.rem(1.6875), + includeFontPadding: false + }, + sectionParagraph: { + fontSize: theme.rem(0.75), + marginVertical: theme.rem(1) + }, + footnote: { + color: theme.primaryText, + fontFamily: theme.fontFaceDefault, + fontSize: theme.rem(0.75), + marginBottom: theme.rem(1), + includeFontPadding: false + }, + buttonFadeContainer: { + flexShrink: 1, + flexGrow: 0 + }, + buttonAbsolute: { + position: 'absolute', + left: 0, + right: 0 + }, + tertiaryTouchable: { + marginVertical: theme.rem(0.5), + alignItems: 'center' + }, + tertiaryText: { + color: theme.textInputTextColorDisabled + }, + tappableText: { + color: theme.iconTappable + } })) -const TappableText = styled(EdgeText)(theme => props => ({ - color: theme.iconTappable -})) +// ----------------------------------------------------------------------------- +// Animated Components +// ----------------------------------------------------------------------------- -const Container = styled(View)({ - flex: 1 -}) - -// -// Skip Button -// - -const SkipButton = styled(Animated.View)<{ swipeOffset: SharedValue }>( - _theme => props => { - const { swipeOffset } = props - return useAnimatedStyle(() => { - return { - opacity: interpolate( - swipeOffset.value, - [0, 1], - [0, 1], - Extrapolation.CLAMP - ) - } - }) - } -) +interface HeroItemProps { + image: ImageProp + itemIndex: number + scrollIndex: SharedValue +} -// -// Hero -// +const HeroItem: React.FC = props => { + const { image, itemIndex, scrollIndex } = props + const theme = useTheme() + const styles = getStyles(theme) + const { width: screenWidth } = useSafeAreaFrame() -const HeroContainer = styled(View)({ - flex: 1, - alignItems: 'center' -}) + const animatedStyle = useAnimatedStyle(() => { + const isFirstItem = itemIndex === 1 + const opacity = interpolate( + scrollIndex.value, + [itemIndex - 1, itemIndex, itemIndex + 1], + [0, 1, 0], + Extrapolation.CLAMP + ) + const scale = interpolate( + scrollIndex.value, + [itemIndex - 1, itemIndex, itemIndex + 1], + [0.3, 1, 0.3] + ) + const translateX = interpolate( + scrollIndex.value, + [itemIndex - 1, itemIndex, itemIndex + 1], + [isFirstItem ? 0 : screenWidth, 0, -screenWidth] + ) + return { opacity, transform: [{ translateX }, { scale }] } + }) -const WelcomeHero = styled(Animated.View)<{ swipeOffset: SharedValue }>( - _theme => props => { - const { swipeOffset } = props - return [ - { - alignItems: 'center', - justifyContent: 'center', - flex: 1 - }, - useAnimatedStyle(() => ({ - opacity: interpolate(swipeOffset.value, [0, 0.5], [1, 0]), - transform: [ - { - scale: interpolate( - swipeOffset.value, - [0, 1], - [1, 0], - Extrapolation.CLAMP - ) - } - ] - })) - ] - } -) - -const WelcomeHeroTitle = styled(UnscaledText)(theme => ({ - color: theme.primaryText, - fontFamily: theme.fontFaceDefault, - fontSize: theme.rem(2.25), - includeFontPadding: false, - lineHeight: theme.rem(2.8), - paddingVertical: theme.rem(1), - textAlign: 'center' -})) -const WelcomeHeroMessage = styled(EdgeText)(theme => ({ - fontSize: theme.rem(0.78), - paddingVertical: theme.rem(1), - textAlign: 'center' -})) -const WelcomeHeroPrompt = styled(EdgeText)(theme => ({ - fontSize: theme.rem(0.75), - color: theme.textLink, - fontFamily: theme.fontFaceBold, - textAlign: 'center', - margin: theme.rem(0.5) -})) + return ( + + + + + + ) +} -const HeroItem = styled(Animated.View)<{ - swipeOffset: SharedValue +interface PageIndicatorProps { itemIndex: number -}>(theme => props => { - const { swipeOffset, itemIndex } = props - const isFirstItem = itemIndex === 1 - const { width: screenWidth } = useSafeAreaFrame() - const translateWidth = screenWidth - return [ - { - alignItems: 'center', - aspectRatio: 1, - padding: theme.rem(1), - position: 'absolute', - height: '100%', - width: '100%' - }, - useAnimatedStyle(() => { - const opacity = interpolate( - swipeOffset.value, - [itemIndex - 1, itemIndex, itemIndex + 1], - [0, 1, 0], - Extrapolation.CLAMP - ) - const scale = interpolate( - swipeOffset.value, - [itemIndex - 1, itemIndex, itemIndex + 1], - [0.3, 1, 0.3] - ) - const translateX = interpolate( - swipeOffset.value, - [itemIndex - 1, itemIndex, itemIndex + 1], - [isFirstItem ? 0 : translateWidth, 0, -translateWidth] - ) - return { - opacity, - transform: [{ translateX }, { scale }] - } - }) - ] -}) - -const HeroImageContainer = styled(View)({ - alignItems: 'stretch', - aspectRatio: 1, - backgroundColor: 'white', - borderRadius: 1000, - maxHeight: '100%', - overflow: 'hidden', - width: '100%' -}) -const HeroImage = styled(Image)({ - maxHeight: '100%', - maxWidth: '100%', - aspectRatio: 1 -}) - -// -// Pagination -// - -const Pagination = styled(View)(theme => ({ - flexDirection: 'row', - justifyContent: 'center', - marginVertical: theme.rem(0.7) -})) + scrollIndex: SharedValue +} -const PageIndicator = styled(Animated.View)<{ - swipeOffset: SharedValue - itemIndex: number -}>(theme => props => { +const PageIndicator: React.FC = props => { + const { itemIndex, scrollIndex } = props + const theme = useTheme() + const styles = getStyles(theme) const themeIcon = theme.icon const themeIconTappable = theme.iconTappable - const { itemIndex, swipeOffset } = props - return [ - { - borderRadius: 10, - margin: theme.rem(0.3), - height: theme.rem(0.6), - width: theme.rem(0.6) - }, - useAnimatedStyle(() => { - const delta = - 1 - Math.max(0, Math.min(1, Math.abs(itemIndex - swipeOffset.value))) - const opacity = interpolate(delta, [0, 1], [0.5, 1]) - const backgroundColor = interpolateColor( + + const animatedStyle = useAnimatedStyle(() => { + const delta = + 1 - Math.max(0, Math.min(1, Math.abs(itemIndex - scrollIndex.value))) + return { + backgroundColor: interpolateColor( delta, [0, 1], [themeIcon, themeIconTappable] - ) - return { - backgroundColor, - opacity - } - }) - ] -}) - -// -// Sections -// - -const SectionCoverAnimated = styled(Animated.View)<{ - swipeOffset: SharedValue -}>(theme => props => { - const { swipeOffset } = props - const themeRem = theme.rem(1) - const themeModal = theme.modal - const themeModalLikeBackground = theme.modalLikeBackground - const insets = useSafeAreaInsets() + ), + opacity: interpolate(delta, [0, 1], [0.5, 1]) + } + }) - return [ - { - alignItems: 'stretch', - justifyContent: 'flex-end', - paddingVertical: theme.rem(1), - paddingBottom: insets.bottom + theme.rem(1), - marginBottom: -insets.bottom - }, - useAnimatedStyle(() => { - const backgroundColor = interpolateColor( - swipeOffset.value, - [0, 1], - [`${themeModal}00`, themeModalLikeBackground] - ) - const paddingVertical = interpolate( - swipeOffset.value, - [0, 1], - [0, themeRem], - Extrapolation.CLAMP - ) - const flexGrow = interpolate( - swipeOffset.value, - [0, 1], - [0, 1.2], - Extrapolation.CLAMP - ) - return { - backgroundColor, - paddingVertical, - flexGrow - } - }) - ] -}) - -const Sections = styled(Animated.View)<{ - swipeOffset: SharedValue -}>(theme => props => { - const { swipeOffset } = props - return [ - { - flexGrow: 1, - paddingBottom: theme.rem(1) - }, - useAnimatedStyle(() => { - const flexGrow = interpolate(swipeOffset.value, [0, 1], [0, 1.5]) - return { - flexGrow - } - }) - ] -}) + return +} -const Section = styled(Animated.View)<{ - swipeOffset: SharedValue +interface SectionItemProps { + section: SectionData itemIndex: number -}>(theme => props => { - const { itemIndex, swipeOffset } = props - const isFirstItem = itemIndex === 1 + scrollIndex: SharedValue +} + +const SectionItem: React.FC = props => { + const { section, itemIndex, scrollIndex } = props + const theme = useTheme() + const styles = getStyles(theme) const { width: screenWidth } = useSafeAreaFrame() const translateWidth = screenWidth / 2 - return [ - { - marginHorizontal: theme.rem(2), - position: 'absolute', - height: '100%' - }, - useAnimatedStyle(() => { - const opacity = interpolate( - swipeOffset.value, - [itemIndex - 1, itemIndex, itemIndex + 1], - [0, 1, 0] - ) - const translateX = interpolate( - swipeOffset.value, - [itemIndex - 1, itemIndex, itemIndex + 1], - [isFirstItem ? 0 : translateWidth, 0, -translateWidth] - ) - return { - transform: [{ translateX }], - opacity - } - }) - ] -}) - -const SectionTitle = styled(EdgeText)(theme => ({ - color: theme.primaryText, - fontFamily: theme.fontFaceDefault, - fontSize: theme.rem(1.6875), - includeFontPadding: false -})) - -const SectionParagraph = styled(EdgeText)(theme => ({ - fontSize: theme.rem(0.75), - marginVertical: theme.rem(1) -})) -const Footnote = styled(EdgeText)(theme => ({ - color: theme.primaryText, - fontFamily: theme.fontFaceDefault, - fontSize: theme.rem(0.75), - marginBottom: theme.rem(1), - includeFontPadding: false -})) + const animatedStyle = useAnimatedStyle(() => { + const isFirstItem = itemIndex === 1 + const opacity = interpolate( + scrollIndex.value, + [itemIndex - 1, itemIndex, itemIndex + 1], + [0, 1, 0] + ) + const translateX = interpolate( + scrollIndex.value, + [itemIndex - 1, itemIndex, itemIndex + 1], + [isFirstItem ? 0 : translateWidth, 0, -translateWidth] + ) + return { transform: [{ translateX }], opacity } + }) -const ButtonFadeContainer = styled(View)(theme => ({ - flexShrink: 1, - flexGrow: 0 -})) + return ( + + + + {parseMarkedText(section.title)} + + + {section.message} + + {section.footnote == null ? null : ( + + {section.footnote} + + )} + + + ) +} From 5e12f53cb8151d032a7425ccd9ef0d76838d17d2 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 17 Dec 2025 17:02:49 -0800 Subject: [PATCH 56/77] Add eslint warning for styled() usage Encourage migration away from styled components to regular components with useTheme() and cacheStyles(). --- eslint.config.mjs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 8f73693c035..c3ee138c788 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -41,7 +41,17 @@ export default [ // Add our own rules: 'edge/useAbortable-abort-check-param': 'error', - 'edge/useAbortable-abort-check-usage': 'error' + 'edge/useAbortable-abort-check-usage': 'error', + + // Warn on styled() usage to encourage migration away from styled components + 'no-restricted-syntax': [ + 'warn', + { + selector: "CallExpression[callee.name='styled']", + message: + 'Avoid using styled() - prefer regular components with useTheme() and cacheStyles()' + } + ] } }, From 69cc7bf67714a1a49c09eddac8dff8e11a549e40 Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 18 Dec 2025 15:50:05 -0800 Subject: [PATCH 57/77] Add balance validation to EVM split --- maestro/07-wallets/C000037-split-wallets.yaml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/maestro/07-wallets/C000037-split-wallets.yaml b/maestro/07-wallets/C000037-split-wallets.yaml index 65f38140050..afe956fe4b2 100644 --- a/maestro/07-wallets/C000037-split-wallets.yaml +++ b/maestro/07-wallets/C000037-split-wallets.yaml @@ -106,18 +106,18 @@ tags: # index: 1 -# ## Verify new wallets and balances -# # ETH recovered -# - scrollUntilVisible: -# element: ${newETHName} -# direction: DOWN -# centerElement: true -# timeout: 10000 -# - extendedWaitUntil: -# visible: "Ξ 0.0000133" -# timeout: 30000 -# optional: true -# label: "⏰ Waiting 30 seconds for recovered ETH balance to be visible" +## Verify new wallets and balances +# ETH recovered +- scrollUntilVisible: + element: ${newETHName} + direction: DOWN + centerElement: true + timeout: 10000 +- extendedWaitUntil: + visible: "Ξ 0.0000133" + timeout: 60000 + optional: true + label: "⏰ Waiting 60 seconds for recovered ETH balance to be visible" # # BTC recovered # - scrollUntilVisible: From b4937451347d268102ef88732a1ff05009d1f112 Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 18 Dec 2025 16:03:27 -0800 Subject: [PATCH 58/77] Improve element targeting and re-login dependability --- .../C000015-ip-validation-reminder.yaml | 17 ++++++++++++++--- maestro/common/relogin-pin.yaml | 5 +---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/maestro/12-notifications/C000015-ip-validation-reminder.yaml b/maestro/12-notifications/C000015-ip-validation-reminder.yaml index ac075d25411..c1b6d148f37 100644 --- a/maestro/12-notifications/C000015-ip-validation-reminder.yaml +++ b/maestro/12-notifications/C000015-ip-validation-reminder.yaml @@ -6,6 +6,8 @@ # 2. Verify IP validation reminder notification is functional # 3. Verify IP validation reminder notification does not re-appear after relaunching the app 10x +# Note: Does not apply to light account, as there is no use of IP validation + appId: ${MAESTRO_APP_ID} tags: @@ -45,11 +47,13 @@ tags: # Back to app - launchApp: stopApp: false -- assertNotVisible: IP Validation Protection +- assertNotVisible: + id: "notifIp2Fa" + # Realaunch multiple times to verify not still present - repeat: - times: 10 + times: 5 commands: - runFlow: file: ../common/relogin-pin.yaml @@ -59,6 +63,13 @@ tags: commands: - tapOn: text: "Dismiss" - - assertNotVisible: IP Validation Protection + - runFlow: + when: + visible: "Claim Your Web3 Handle" + commands: + - tapOn: + text: "Not Now" + - assertNotVisible: + id: "notifIp2Fa" - stopApp \ No newline at end of file diff --git a/maestro/common/relogin-pin.yaml b/maestro/common/relogin-pin.yaml index c6ea72e3ff9..d946e48ab51 100644 --- a/maestro/common/relogin-pin.yaml +++ b/maestro/common/relogin-pin.yaml @@ -6,10 +6,7 @@ appId: ${MAESTRO_APP_ID} env: PIN_RELOGIN: ${PIN_RELOGIN || MAESTRO_EDGE_NEW_ACCOUNT_PIN_SINGLE} # Must be a single digit PIN --- -- tapOn: - id: sideMenuButton -- tapOn: Logout - +- launchApp - tapOn: ${PIN_RELOGIN} - tapOn: ${PIN_RELOGIN} - tapOn: ${PIN_RELOGIN} From 172b85b99340b6ca1f6c07ec640abbdaca22fa41 Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 18 Dec 2025 17:19:06 -0800 Subject: [PATCH 59/77] Improve scrolling on large devices --- maestro/07-wallets/C000044-create-all-wallet-types.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/maestro/07-wallets/C000044-create-all-wallet-types.yaml b/maestro/07-wallets/C000044-create-all-wallet-types.yaml index c3ae7f1b2b0..95cb1e13473 100644 --- a/maestro/07-wallets/C000044-create-all-wallet-types.yaml +++ b/maestro/07-wallets/C000044-create-all-wallet-types.yaml @@ -16,6 +16,8 @@ tags: file: ../common/launch-cleared.yaml - runFlow: file: ../common/create-account.yaml + env: + NEW_WALLETS: ${'["Bitcoin"]'} - runFlow: file: ../common/dismiss-modals.yaml @@ -58,6 +60,6 @@ tags: element: ${nickName} centerElement: true timeout: 60000 - speed: 65 + speed: 5 - evalScript: ${index++} \ No newline at end of file From 16ea15f36dfd55e4e199610f778e8d027ff408bf Mon Sep 17 00:00:00 2001 From: Jared Date: Fri, 19 Dec 2025 00:27:20 -0800 Subject: [PATCH 60/77] Add iOS only assertion --- maestro/07-wallets/C000033-pause-wallets.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/maestro/07-wallets/C000033-pause-wallets.yaml b/maestro/07-wallets/C000033-pause-wallets.yaml index 44ba250bf13..3f2ba44b4ee 100644 --- a/maestro/07-wallets/C000033-pause-wallets.yaml +++ b/maestro/07-wallets/C000033-pause-wallets.yaml @@ -41,7 +41,12 @@ tags: # Test pause wallet - tapOn: text: ".*Pause Wallet" -- assertVisible: "This wallet will no longer synchronize with the blockchain and will not detect new transactions or balance changes" +# Android is too slow to reliably assert this message before it disappears +- runFlow: + when: + platform: iOS + commands: + - assertVisible: "This wallet will no longer synchronize with the blockchain and will not detect new transactions or balance changes" - assertVisible: "Wallet Paused" # Wait an arbitrary amount of time to ensure state is saved From 898a26fc97e380d7c3fb714d739a30994af5d520 Mon Sep 17 00:00:00 2001 From: Jared Date: Fri, 19 Dec 2025 00:44:17 -0800 Subject: [PATCH 61/77] Extend wait for slow devices --- maestro/07-wallets/C000045-add-edit-tokens.yaml | 5 ++--- maestro/common/relogin-pin.yaml | 3 +++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/maestro/07-wallets/C000045-add-edit-tokens.yaml b/maestro/07-wallets/C000045-add-edit-tokens.yaml index 73dde3a9abe..1c863e05cae 100644 --- a/maestro/07-wallets/C000045-add-edit-tokens.yaml +++ b/maestro/07-wallets/C000045-add-edit-tokens.yaml @@ -14,7 +14,6 @@ appId: ${MAESTRO_APP_ID} env: - WALLET_NAME: "My Ether" UNI_CONTRACT_ADDRESS: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984" # $UNI token MOG_CONTRACT_ADDRESS: "0xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a" # ^^ Must be a token that is not defualt and higher in alphabetically than $TOKEN_NAME @@ -41,10 +40,10 @@ tags: # Navigate to scene - tapOn: Assets - scrollUntilVisible: - element: ${WALLET_NAME} + element: ETH direction: DOWN centerElement: true -- longPressOn: ${WALLET_NAME} +- longPressOn: ETH - tapOn: ".*Add / Edit Tokens" # First time token warning modal diff --git a/maestro/common/relogin-pin.yaml b/maestro/common/relogin-pin.yaml index d946e48ab51..5906626856a 100644 --- a/maestro/common/relogin-pin.yaml +++ b/maestro/common/relogin-pin.yaml @@ -7,6 +7,9 @@ env: PIN_RELOGIN: ${PIN_RELOGIN || MAESTRO_EDGE_NEW_ACCOUNT_PIN_SINGLE} # Must be a single digit PIN --- - launchApp +- extendedWaitUntil: + visible: Exit PIN + timeout: 35000 - tapOn: ${PIN_RELOGIN} - tapOn: ${PIN_RELOGIN} - tapOn: ${PIN_RELOGIN} From 1973bea665c9b3862dad9cca66f9b160376e5a55 Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 18 Dec 2025 16:50:06 -0800 Subject: [PATCH 62/77] Extend wait time and remove scroll for split test --- maestro/07-wallets/C000048-split-all-evm.yaml | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/maestro/07-wallets/C000048-split-all-evm.yaml b/maestro/07-wallets/C000048-split-all-evm.yaml index e841ad96ee8..f31dcbf205b 100644 --- a/maestro/07-wallets/C000048-split-all-evm.yaml +++ b/maestro/07-wallets/C000048-split-all-evm.yaml @@ -37,7 +37,7 @@ tags: - assertVisible: Create Wallet From Seed - assertVisible: Search Wallets -# Define expected EVM networks (ordered for scrolling down) +# Define expected EVM networks (ordered by appearance) - evalScript: | ${var evmNetworks = [ "zkSync", @@ -66,24 +66,42 @@ tags: - repeat: times: ${evmNetworks.length} commands: - - scrollUntilVisible: - element: ${".*" + evmNetworks[index]} - direction: DOWN - centerElement: true - speed: 10 - timeout: 10000 - - tapOn: ${".*" + evmNetworks[index]} - - evalScript: ${index++} + # # Once search can accomodate multiple words + # # Search is slower but more reliable + # - tapOn: Search Wallets + # - tapOn: + # id: "undefined.clearIcon" + # - inputText: ${evmNetworks[index]} + # - tapOn: + # text: ${".*" + evmNetworks[index]} + # index: 1 + # - evalScript: ${index++} + # Retry for inconsistent android behavior + - retry: + maxRetries: 2 + commands: + - scrollUntilVisible: + element: ${".*" + evmNetworks[index]} + direction: DOWN + centerElement: true + speed: 1 + timeout: 1000 + optional: true + - tapOn: ${".*" + evmNetworks[index]} + + - evalScript: ${index++} + - tapOn: Next +# Title of scene is index 0 - tapOn: text: Split Wallet index: 1 - extendedWaitUntil: visible: Assets - timeout: 25000 + timeout: 60000 - runFlow: file: ../common/no-errors.yaml From f494a7e5b05cf85cccde0f7d19b5276c7e760b45 Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 18 Dec 2025 17:03:04 -0800 Subject: [PATCH 63/77] Extend wait time and add two checks for light account test --- .../01-accounts/C000006-light-account.yaml | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/maestro/01-accounts/C000006-light-account.yaml b/maestro/01-accounts/C000006-light-account.yaml index 58b22439746..e04e8111640 100644 --- a/maestro/01-accounts/C000006-light-account.yaml +++ b/maestro/01-accounts/C000006-light-account.yaml @@ -70,7 +70,7 @@ tags: # If the request notifications modal show with "Security is Our Priority" then cancel it - Currently appears before wallets are created - extendedWaitUntil: visible: Security is Our Priority - timeout: 15000 + timeout: 25000 optional: true - runFlow: when: @@ -79,13 +79,15 @@ tags: - tapOn: text: "Cancel" label: "Request notifications modal" - - assertVisible: "Choose Wallets to Add" + - extendedWaitUntil: + visible: "Choose Wallets to Add" + timeout: 20000 - assertVisible: ${"BTC.*"} - assertVisible: ${"ETH.*"} - assertVisible: ${"LTC.*"} - tapOn: Next - waitForAnimationToEnd: - timeout: 2000 + timeout: 5000 # Dismiss Web3 Handle Modal - runFlow: @@ -96,8 +98,9 @@ tags: text: "Not Now" label: "Dismiss Web3 Handle Modal" # Confirm flow completed successfully -- assertVisible: - text: "Assets" +- extendedWaitUntil: + visible: "Assets" + timeout: 20000 label: "Light account created successfully" # Ensure visible reminder to backup account & correct modal @@ -116,18 +119,27 @@ tags: - tapOn: Back Up Account - waitForAnimationToEnd: timeout: 10000 - - assertVisible: Choose Username + - extendedWaitUntil: + visible: Choose Username + timeout: 10000 - assertVisible: Next - tapOn: id: "headerLeftButton" + # could pause here + # Test Receive Scene via "Deposit" button + - tapOn: Deposit + - tapOn: "From Another Wallet/Exchange" + - tapOn: "BTC" + - assertVisible: "Receive" + - assertVisible: "To buy, sell, and receive.*" + - assertVisible: "Back Up Account" - tapOn: - id: "sideMenuButton" - - tapOn: "Logout" - - assertNotVisible: ${".*guest.*"} - - tapOn: ${MAESTRO_EDGE_NEW_ACCOUNT_PIN_SINGLE} - - tapOn: ${MAESTRO_EDGE_NEW_ACCOUNT_PIN_SINGLE} - - tapOn: ${MAESTRO_EDGE_NEW_ACCOUNT_PIN_SINGLE} - - tapOn: ${MAESTRO_EDGE_NEW_ACCOUNT_PIN_SINGLE} + id: "chevronBack" + + # Relogin with PIN + - runFlow: + file: ../common/relogin-pin.yaml + label: "Relogin with PIN" # Dismiss "Discover Edge" modal - runFlow: when: @@ -144,15 +156,19 @@ tags: visible: Continue with Guest Account - tapOn: Learn More - assertVisible: "Guest Account" # Support article title + + # Reopen to same screen and check unable to view seed phrase - launchApp: stopApp: false - - tapOn: - id: "sideMenuButton" - - tapOn: "Logout" - - tapOn: ${MAESTRO_EDGE_NEW_ACCOUNT_PIN_SINGLE} - - tapOn: ${MAESTRO_EDGE_NEW_ACCOUNT_PIN_SINGLE} - - tapOn: ${MAESTRO_EDGE_NEW_ACCOUNT_PIN_SINGLE} - - tapOn: ${MAESTRO_EDGE_NEW_ACCOUNT_PIN_SINGLE} + - longPressOn: BTC + - assertNotVisible: ".*Master Private Key" + - assertNotVisible: ".*Get Raw Keys" + + + # Relogin with PIN + - runFlow: + file: ../common/relogin-pin.yaml + label: "Relogin with PIN" # Dismiss "Discover Edge" modal - runFlow: when: @@ -190,7 +206,9 @@ tags: - tapOn: Get Started - waitForAnimationToEnd: timeout: 10000 - - assertVisible: Choose Username + - extendedWaitUntil: + visible: Choose Username + timeout: 10000 - assertVisible: Next - tapOn: id: "headerLeftButton" @@ -238,7 +256,9 @@ tags: optional: true - waitForAnimationToEnd: timeout: 5000 - - assertVisible: Choose Username + - extendedWaitUntil: + visible: Choose Username + timeout: 10000 - assertVisible: Next # Complete backup of account @@ -341,6 +361,12 @@ tags: - pressKey: Enter - inputText: ${MAESTRO_EDGE_NEW_ACCOUNT_PASSWORD} - pressKey: Enter + + # Dismiss option to save password to keychain + - tapOn: + text: Not Now + optional: true + - extendedWaitUntil: visible: "Assets" timeout: 5000 From bc47da296d139256ad912b8c18653575d8f07afd Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 28 Nov 2025 11:52:04 -0800 Subject: [PATCH 64/77] style: fix lint errors --- eslint.config.mjs | 8 ++-- src/actions/LocalSettingsActions.ts | 2 +- src/actions/LoginActions.tsx | 41 ++++++++++--------- src/actions/RequestReviewActions.tsx | 5 ++- .../scenes/ReviewTriggerTestScene.tsx | 2 +- src/components/scenes/WalletListScene.tsx | 4 +- 6 files changed, 32 insertions(+), 30 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index c3ee138c788..3091d05a469 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -91,14 +91,12 @@ export default [ 'src/actions/FioAddressActions.ts', 'src/actions/FirstOpenActions.tsx', 'src/actions/LoanWelcomeActions.tsx', - 'src/actions/LocalSettingsActions.ts', - 'src/actions/LoginActions.tsx', 'src/actions/NotificationActions.ts', 'src/actions/PaymentProtoActions.tsx', 'src/actions/ReceiveDropdown.tsx', 'src/actions/RecoveryReminderActions.tsx', - 'src/actions/RequestReviewActions.tsx', + 'src/actions/ScamWarningActions.tsx', 'src/actions/ScanActions.tsx', @@ -298,7 +296,7 @@ export default [ 'src/components/scenes/OtpSettingsScene.tsx', 'src/components/scenes/PasswordRecoveryScene.tsx', 'src/components/scenes/PromotionSettingsScene.tsx', - 'src/components/scenes/ReviewTriggerTestScene.tsx', + 'src/components/scenes/SecurityAlertsScene.tsx', 'src/components/scenes/SettingsScene.tsx', @@ -318,7 +316,7 @@ export default [ 'src/components/scenes/TransactionsExportScene.tsx', 'src/components/scenes/UpgradeUsernameScreen.tsx', - 'src/components/scenes/WalletListScene.tsx', + 'src/components/scenes/WalletRestoreScene.tsx', 'src/components/scenes/WcConnectionsScene.tsx', 'src/components/scenes/WcConnectScene.tsx', diff --git a/src/actions/LocalSettingsActions.ts b/src/actions/LocalSettingsActions.ts index b7b9ac2e199..44c381e8706 100644 --- a/src/actions/LocalSettingsActions.ts +++ b/src/actions/LocalSettingsActions.ts @@ -33,7 +33,7 @@ export const getLocalAccountSettings = async ( return settings } -export function useAccountSettings() { +export function useAccountSettings(): LocalAccountSettings { const [accountSettings, setAccountSettings] = React.useState(localAccountSettings) React.useEffect(() => watchAccountSettings(setAccountSettings), []) diff --git a/src/actions/LoginActions.tsx b/src/actions/LoginActions.tsx index 033ea132aea..810293c0deb 100644 --- a/src/actions/LoginActions.tsx +++ b/src/actions/LoginActions.tsx @@ -16,7 +16,10 @@ import { getCurrencies } from 'react-native-localize' import performance from 'react-native-performance' import { sprintf } from 'sprintf-js' -import { readSyncedSettings } from '../actions/SettingsActions' +import { + type DenominationSettings, + readSyncedSettings +} from '../actions/SettingsActions' import { ConfirmContinueModal } from '../components/modals/ConfirmContinueModal' import { FioCreateHandleModal } from '../components/modals/FioCreateHandleModal' import { SurveyModal } from '../components/modals/SurveyModal' @@ -122,14 +125,14 @@ export function initializeAccount( 'createWalletSelectCrypto' | 'createWalletSelectCryptoNewAccount' >['navigation'], items: WalletCreateItem[] - ) => { + ): Promise => { navigation.replace('edgeTabs', { screen: 'home' }) const createWalletsPromise = createCustomWallets( account, fiatCurrencyCode, items, dispatch - ).catch(error => { + ).catch((error: unknown) => { showError(error) }) @@ -231,8 +234,8 @@ export function initializeAccount( }) } }) - .catch(err => { - showError(err) + .catch((error: unknown) => { + showError(error) }) } @@ -249,8 +252,8 @@ export function initializeAccount( const { context } = state.core // Sign up for push notifications: - dispatch(registerNotificationsV2()).catch(e => { - console.error(e) + dispatch(registerNotificationsV2()).catch((error: unknown) => { + console.error(error) }) const walletInfos = account.allKeys @@ -293,20 +296,20 @@ export function initializeAccount( const defaultDenominationSettings = state.ui.settings.denominationSettings const syncedDenominationSettings = syncedSettings?.denominationSettings ?? {} - const mergedDenominationSettings = {} + const mergedDenominationSettings: DenominationSettings = {} for (const plugin of Object.keys(defaultDenominationSettings)) { - // @ts-expect-error - mergedDenominationSettings[plugin] = {} - // @ts-expect-error - for (const code of Object.keys(defaultDenominationSettings[plugin])) { - // @ts-expect-error - mergedDenominationSettings[plugin][code] = { - // @ts-expect-error - ...defaultDenominationSettings[plugin][code], - ...(syncedDenominationSettings?.[plugin]?.[code] ?? {}) + const entries: DenominationSettings[string] = {} + for (const code of Object.keys(entries)) { + entries[code] = { + ...defaultDenominationSettings[plugin]?.[code], + ...syncedDenominationSettings[plugin]?.[code], + name: '', + multiplier: '', + symbol: '' } } + mergedDenominationSettings[plugin] = entries } accountInitObject.denominationSettings = { ...mergedDenominationSettings } @@ -328,7 +331,7 @@ export function initializeAccount( }, onNotificationPermit(info) { dispatch(updateNotificationSettings(info.notificationOptIns)).catch( - error => { + (error: unknown) => { trackError(error, 'LoginScene:onLogin:setDeviceSettings') console.error(error) } @@ -434,7 +437,7 @@ async function createCustomWallets( account.createCurrencyWallets(options), timeoutMs, new Error(lstrings.error_creating_wallets) - ).catch(error => { + ).catch((error: unknown) => { dispatch(logEvent('Signup_Wallets_Created_Failed', { error })) throw error }) diff --git a/src/actions/RequestReviewActions.tsx b/src/actions/RequestReviewActions.tsx index 9abe150f622..96b9bf49e1e 100644 --- a/src/actions/RequestReviewActions.tsx +++ b/src/actions/RequestReviewActions.tsx @@ -112,14 +112,15 @@ export const readReviewTriggerData = async ( const swapCountData = JSON.parse(swapCountDataStr) // Initialize new data structure with old swap count data + const swapCount = parseInt(swapCountData.swapCount) const migratedData: ReviewTriggerData = { ...initReviewTriggerData(), - swapCount: parseInt(swapCountData.swapCount) || 0 + swapCount: Number.isNaN(swapCount) ? 0 : swapCount } // If user was already asked for review in the old system, // set nextTriggerDate to 1 year in the future - if (swapCountData.hasReviewBeenRequested) { + if (swapCountData.hasReviewBeenRequested === true) { const nextYear = new Date() nextYear.setFullYear(nextYear.getFullYear() + 1) migratedData.nextTriggerDate = nextYear diff --git a/src/components/scenes/ReviewTriggerTestScene.tsx b/src/components/scenes/ReviewTriggerTestScene.tsx index 00f2f50a322..65fe90b480f 100644 --- a/src/components/scenes/ReviewTriggerTestScene.tsx +++ b/src/components/scenes/ReviewTriggerTestScene.tsx @@ -37,7 +37,7 @@ import { EdgeText } from '../themed/EdgeText' interface Props extends EdgeSceneProps<'reviewTriggerTest'> {} -export const ReviewTriggerTestScene = (props: Props) => { +export const ReviewTriggerTestScene: React.FC = () => { const dispatch = useDispatch() const theme = useTheme() const styles = getStyles(theme) diff --git a/src/components/scenes/WalletListScene.tsx b/src/components/scenes/WalletListScene.tsx index d994355aaf2..47d2b00601a 100644 --- a/src/components/scenes/WalletListScene.tsx +++ b/src/components/scenes/WalletListScene.tsx @@ -38,7 +38,7 @@ import { WalletListSwipeable } from '../themed/WalletListSwipeable' interface Props extends WalletsTabSceneProps<'walletList'> {} -export function WalletListScene(props: Props) { +export const WalletListScene: React.FC = props => { const { navigation } = props const theme = useTheme() const styles = getStyles(theme) @@ -77,7 +77,7 @@ export function WalletListScene(props: Props) { setSorting(true) } }) - .catch(error => { + .catch((error: unknown) => { showError(error) }) }) From ea8a6ae48edc4f7d7b56d1ced9988ea75ccc4a9f Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 26 Nov 2025 12:56:08 -0800 Subject: [PATCH 65/77] fix: remove disabled scamWarningModal calls from login flow The showScamWarningModal function was already disabled (always returns false), making these calls unnecessary async overhead during login and other operations. Removes calls from: - initializeAccount (firstLogin) - WcConnectionsScene (firstWalletConnect) - WalletListMenuActions (firstPrivateKeyView) --- src/actions/LoginActions.tsx | 4 ---- src/actions/WalletListMenuActions.tsx | 7 ------- src/components/scenes/WcConnectionsScene.tsx | 4 ---- 3 files changed, 15 deletions(-) diff --git a/src/actions/LoginActions.tsx b/src/actions/LoginActions.tsx index 810293c0deb..a428ee06084 100644 --- a/src/actions/LoginActions.tsx +++ b/src/actions/LoginActions.tsx @@ -53,7 +53,6 @@ import { registerNotificationsV2, updateNotificationSettings } from './NotificationActions' -import { showScamWarningModal } from './ScamWarningActions' const PER_WALLET_TIMEOUT = 5000 const MIN_CREATE_WALLET_TIMEOUT = 20000 @@ -239,9 +238,6 @@ export function initializeAccount( }) } - // Show the scam warning modal if needed - if (await showScamWarningModal('firstLogin')) hideSurvey = true - // Check for security alerts: if (hasSecurityAlerts(account)) { navigation.push('securityAlerts') diff --git a/src/actions/WalletListMenuActions.tsx b/src/actions/WalletListMenuActions.tsx index a705c294847..5dd611b7315 100644 --- a/src/actions/WalletListMenuActions.tsx +++ b/src/actions/WalletListMenuActions.tsx @@ -27,7 +27,6 @@ import { logActivity } from '../util/logger' import { validatePassword } from './AccountActions' import { showDeleteWalletModal } from './DeleteWalletModalActions' import { showResyncWalletModal } from './ResyncWalletModalActions' -import { showScamWarningModal } from './ScamWarningActions' import { toggleUserPausedWallet } from './SettingsActions' export type WalletListMenuKey = @@ -208,9 +207,6 @@ export function walletListMenuAction( const wallet = account.currencyWallets[walletId] const { xpubExplorer } = wallet.currencyInfo - // Show the scam warning modal if needed - await showScamWarningModal('firstPrivateKeyView') - const displayPublicSeed = await account.getDisplayPublicKey(wallet.id) const copy: ButtonInfo = { @@ -283,9 +279,6 @@ export function walletListMenuAction( const { currencyWallets } = account const wallet = currencyWallets[walletId] - // Show the scam warning modal if needed - await showScamWarningModal('firstPrivateKeyView') - const passwordValid = (await dispatch( validatePassword({ diff --git a/src/components/scenes/WcConnectionsScene.tsx b/src/components/scenes/WcConnectionsScene.tsx index bccad41f3aa..b4178d4762f 100644 --- a/src/components/scenes/WcConnectionsScene.tsx +++ b/src/components/scenes/WcConnectionsScene.tsx @@ -8,7 +8,6 @@ import AntDesignIcon from 'react-native-vector-icons/AntDesign' import { sprintf } from 'sprintf-js' import { checkAndShowLightBackupModal } from '../../actions/BackupModalActions' -import { showScamWarningModal } from '../../actions/ScamWarningActions' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { SPECIAL_CURRENCY_INFO } from '../../constants/WalletAndCurrencyConstants' import { useAsyncEffect } from '../../hooks/useAsyncEffect' @@ -125,9 +124,6 @@ export const WcConnectionsScene = (props: Props) => { } const handleNewConnectionPress = async () => { - // Show the scam warning modal if needed - await showScamWarningModal('firstWalletConnect') - if (checkAndShowLightBackupModal(account, navigation as NavigationBase)) { await Promise.resolve() } else { From 41c1c985a26e9468b00b8140f307d7f72c996c32 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 26 Nov 2025 12:56:31 -0800 Subject: [PATCH 66/77] perf: avoid writing defaults in readSyncedSettings When Settings.json doesn't exist (new account or first login), return default values without writing them to disk. Defaults are derived from cleaners, so they don't need to be persisted. Settings will be written when the user explicitly changes a value. This eliminates unnecessary disk I/O during the login flow. --- src/actions/SettingsActions.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/actions/SettingsActions.tsx b/src/actions/SettingsActions.tsx index 21f4ae9425c..0c1f488dfec 100644 --- a/src/actions/SettingsActions.tsx +++ b/src/actions/SettingsActions.tsx @@ -565,10 +565,9 @@ export async function readSyncedSettings( const text = await account.disklet.getText(SYNCED_SETTINGS_FILENAME) const settingsFromFile = JSON.parse(text) return asSyncedAccountSettings(settingsFromFile) - } catch (e: any) { - console.log(e) - // If Settings.json doesn't exist yet, create it, and return it - await writeSyncedSettings(account, SYNCED_ACCOUNT_DEFAULTS) + } catch (error: unknown) { + // If Settings.json doesn't exist yet, return defaults without writing. + // Defaults can be derived from cleaners. Only write when values change. return SYNCED_ACCOUNT_DEFAULTS } } From dfb227f6a70c5c64b1b6d0103f24e45e70d65b69 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 26 Nov 2025 12:56:53 -0800 Subject: [PATCH 67/77] perf: avoid writing defaults in readLocalAccountSettings When local Settings.json doesn't exist (new account), return default values without writing them to disk. Defaults are derived from cleaners, so they don't need to be persisted. Settings will be written when values actually change. This eliminates unnecessary disk I/O during the login flow. --- src/actions/LocalSettingsActions.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/actions/LocalSettingsActions.ts b/src/actions/LocalSettingsActions.ts index 44c381e8706..e8b0986ad91 100644 --- a/src/actions/LocalSettingsActions.ts +++ b/src/actions/LocalSettingsActions.ts @@ -268,9 +268,12 @@ export const readLocalAccountSettings = async ( emitAccountSettings(settings) readSettingsFromDisk = true return settings - } catch (e) { + } catch (error: unknown) { + // If Settings.json doesn't exist yet, return defaults without writing. + // Defaults can be derived from cleaners. Only write when values change. const defaults = asLocalAccountSettings({}) - return await writeLocalAccountSettings(account, defaults) + emitAccountSettings(defaults) + return defaults } } From 3f8ef4c61bf96c8487123b366fa7bb9eebb63800 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 26 Nov 2025 13:13:08 -0800 Subject: [PATCH 68/77] perf: load biometric state in background during login Move biometric checks (isTouchEnabled, getSupportedBiometryType) from blocking awaits to background Promise execution. This removes two blocking native module calls from the critical login path. The biometric state is loaded asynchronously and dispatched via a new UI/SETTINGS/SET_TOUCH_ID_SUPPORT action when available. The UI will use default values (false) until the background check completes. Also removes the redundant refreshTouchId call - this is already called by edge-login-ui-rn in submitLogin() before the onLogin callback, so the call here was unnecessary. From 4bea57089b711931faa92d276dc192f6bdd9711e Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 5 Dec 2025 13:32:50 -0800 Subject: [PATCH 69/77] style: fix lint errors --- eslint.config.mjs | 1 - src/components/scenes/SettingsScene.tsx | 36 ++++++++++++++----------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 3091d05a469..5bad5c129ef 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -299,7 +299,6 @@ export default [ 'src/components/scenes/SecurityAlertsScene.tsx', - 'src/components/scenes/SettingsScene.tsx', 'src/components/scenes/SpendingLimitsScene.tsx', 'src/components/scenes/Staking/EarnScene.tsx', 'src/components/scenes/Staking/StakeOptionsScene.tsx', diff --git a/src/components/scenes/SettingsScene.tsx b/src/components/scenes/SettingsScene.tsx index d5c9339d762..d8c555b60d5 100644 --- a/src/components/scenes/SettingsScene.tsx +++ b/src/components/scenes/SettingsScene.tsx @@ -57,7 +57,7 @@ import { SettingsTappableRow } from '../settings/SettingsTappableRow' type Props = EdgeAppSceneProps<'settingsOverview'> -export const SettingsScene = (props: Props) => { +export const SettingsScene: React.FC = props => { const { navigation } = props const theme = useTheme() const dispatch = useDispatch() @@ -135,10 +135,12 @@ export const SettingsScene = (props: Props) => { }) setValidatedPassword(undefined) } else { - const password = await handleShowUnlockSettingsModal().catch(err => { - showError(err) - return undefined - }) + const password = await handleShowUnlockSettingsModal().catch( + (error: unknown) => { + showError(error) + return undefined + } + ) setValidatedPassword(password) } }) @@ -146,10 +148,12 @@ export const SettingsScene = (props: Props) => { /** Returns true if the settings are locked. Otherwise false if they're unlocked. */ const hasLock = async (): Promise => { if (isLocked) { - const password = await handleShowUnlockSettingsModal().catch(err => { - showError(err) - return undefined - }) + const password = await handleShowUnlockSettingsModal().catch( + (error: unknown) => { + showError(error) + return undefined + } + ) if (password == null) return true setValidatedPassword(password) dispatch({ @@ -289,8 +293,8 @@ export const SettingsScene = (props: Props) => { bridge={bridge} message={sprintf(lstrings.delete_account_feedback, username)} /> - )).catch(err => { - showDevError(err) + )).catch((error: unknown) => { + showDevError(error) }) return true }} @@ -341,7 +345,7 @@ export const SettingsScene = (props: Props) => { defaultLogLevel: newDefaultLogLevel, sources: {} }) - .catch(error => { + .catch((error: unknown) => { showError(error) }) }) @@ -355,7 +359,7 @@ export const SettingsScene = (props: Props) => { await writeForceLightAccountCreate(!forceLightAccountCreate) }) - const loadBiometryType = async () => { + const loadBiometryType = async (): Promise => { if (Platform.OS === 'ios') { const biometryType = await getSupportedBiometryType() switch (biometryType) { @@ -387,7 +391,7 @@ export const SettingsScene = (props: Props) => { React.useEffect(() => { if (!supportsTouchId) return - loadBiometryType().catch(error => { + loadBiometryType().catch((error: unknown) => { showError(error) }) @@ -398,7 +402,7 @@ export const SettingsScene = (props: Props) => { // Cleanup function to remove the watcher on unmount return () => { - if (cleanup) cleanup() + if (cleanup != null) cleanup() } }, [context, supportsTouchId]) @@ -406,7 +410,7 @@ export const SettingsScene = (props: Props) => { React.useEffect(() => { return navigation.addListener('focus', () => { if (account.otpResetDate != null) { - showReEnableOtpModal(account).catch(error => { + showReEnableOtpModal(account).catch((error: unknown) => { showError(error) }) } From acc2adbca660a3dec24df2f817e5d185c443ba5d Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 5 Dec 2025 13:34:03 -0800 Subject: [PATCH 70/77] refactor: move biometric state from Redux to local SettingsScene state Remove isTouchEnabled and isTouchSupported from global Redux state. Biometric state is now loaded lazily in SettingsScene using useAsyncValue, which removes it from the login critical path. - Remove biometric fields from SettingsState and AccountInitPayload - Remove CHANGE_TOUCH_ID_SETTINGS action type - Remove updateTouchIdEnabled thunk from SettingsActions - SettingsScene loads biometric state locally on mount via useAsyncValue - Toggle calls enableTouchId/disableTouchId directly with local state --- eslint.config.mjs | 2 +- .../__snapshots__/RootReducer.test.ts.snap | 2 - src/actions/LoginActions.tsx | 5 +- src/actions/SettingsActions.tsx | 20 ---- src/components/scenes/SettingsScene.tsx | 104 ++++++++++++------ src/reducers/scenes/SettingsReducer.ts | 17 --- src/types/reduxActions.ts | 4 - src/util/fake/FakeProviders.tsx | 27 +++-- 8 files changed, 92 insertions(+), 89 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 5bad5c129ef..957e21a666c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -512,7 +512,7 @@ export default [ 'src/util/CurrencyWalletHelpers.ts', 'src/util/exchangeRates.ts', - 'src/util/fake/FakeProviders.tsx', + 'src/util/FioAddressUtils.ts', 'src/util/getAccountUsername.ts', 'src/util/GuiPluginTools.ts', diff --git a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap index 3caf6936468..03af2fbd4b3 100644 --- a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap +++ b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap @@ -115,8 +115,6 @@ exports[`initialState 1`] = ` "denominationSettings": {}, "developerModeOn": false, "isAccountBalanceVisible": true, - "isTouchEnabled": false, - "isTouchSupported": false, "mostRecentWallets": [], "notifState": {}, "passwordRecoveryRemindersShown": { diff --git a/src/actions/LoginActions.tsx b/src/actions/LoginActions.tsx index a428ee06084..169431fab03 100644 --- a/src/actions/LoginActions.tsx +++ b/src/actions/LoginActions.tsx @@ -4,9 +4,7 @@ import type { EdgeTokenId } from 'edge-core-js/types' import { - getSupportedBiometryType, hasSecurityAlerts, - isTouchEnabled, refreshTouchId, showNotificationPermissionReminder } from 'edge-login-ui-rn' @@ -262,11 +260,10 @@ export function initializeAccount( account, tokenId: null, pinLoginEnabled: false, - isTouchEnabled: await isTouchEnabled(account), - isTouchSupported: (await getSupportedBiometryType()) !== false, walletId: '', walletsSort: 'manual' } + try { if (!newAccount) { // We have a wallet diff --git a/src/actions/SettingsActions.tsx b/src/actions/SettingsActions.tsx index 0c1f488dfec..04554e2260e 100644 --- a/src/actions/SettingsActions.tsx +++ b/src/actions/SettingsActions.tsx @@ -14,7 +14,6 @@ import type { EdgeDenomination, EdgeSwapPluginType } from 'edge-core-js' -import { disableTouchId, enableTouchId } from 'edge-login-ui-rn' import * as React from 'react' import { ButtonsModal } from '../components/modals/ButtonsModal' @@ -225,25 +224,6 @@ export function setDenominationKeyRequest( } } -// touch id interaction -export function updateTouchIdEnabled( - isTouchEnabled: boolean, - account: EdgeAccount -): ThunkAction> { - return async (dispatch, getState) => { - // dispatch the update for the new state for - dispatch({ - type: 'UI/SETTINGS/CHANGE_TOUCH_ID_SETTINGS', - data: { isTouchEnabled } - }) - if (isTouchEnabled) { - await enableTouchId(account) - } else { - await disableTouchId(account) - } - } -} - export function togglePinLoginEnabled( pinLoginEnabled: boolean ): ThunkAction> { diff --git a/src/components/scenes/SettingsScene.tsx b/src/components/scenes/SettingsScene.tsx index d8c555b60d5..f3bab7249b7 100644 --- a/src/components/scenes/SettingsScene.tsx +++ b/src/components/scenes/SettingsScene.tsx @@ -1,5 +1,11 @@ +import { useQuery } from '@tanstack/react-query' import type { EdgeLogType } from 'edge-core-js' -import { getSupportedBiometryType } from 'edge-login-ui-rn' +import { + disableTouchId, + enableTouchId, + getSupportedBiometryType, + isTouchEnabled +} from 'edge-login-ui-rn' import * as React from 'react' import { Platform } from 'react-native' import { check } from 'react-native-permissions' @@ -23,8 +29,7 @@ import { setAutoLogoutTimeInSecondsRequest, showReEnableOtpModal, showUnlockSettingsModal, - togglePinLoginEnabled, - updateTouchIdEnabled + togglePinLoginEnabled } from '../../actions/SettingsActions' import { ENV } from '../../env' import { useAsyncEffect } from '../../hooks/useAsyncEffect' @@ -74,12 +79,39 @@ export const SettingsScene: React.FC = props => { state => state.ui.settings.pinLoginEnabled ) const spamFilterOn = useSelector(state => state.ui.settings.spamFilterOn) - const supportsTouchId = useSelector( - state => state.ui.settings.isTouchSupported - ) - const touchIdEnabled = useSelector(state => state.ui.settings.isTouchEnabled) const account = useSelector(state => state.core.account) + + // Load biometric state locally (not from Redux) + const { data: biometricState } = useQuery({ + queryKey: ['biometricState', account.id], + queryFn: async () => { + const [touchEnabled, supportedType] = await Promise.all([ + isTouchEnabled(account), + getSupportedBiometryType() + ]) + return { + isTouchEnabled: touchEnabled, + isTouchSupported: supportedType !== false, + biometryType: supportedType + } + }, + enabled: account != null + }) + + // Local state to track touch ID enabled status (can be toggled by user) + const [touchIdEnabled, setTouchIdEnabled] = React.useState( + null + ) + + // Sync local state with loaded state + React.useEffect(() => { + if (biometricState != null && touchIdEnabled == null) { + setTouchIdEnabled(biometricState.isTouchEnabled) + } + }, [biometricState, touchIdEnabled]) + + const supportsTouchId = biometricState?.isTouchSupported ?? false const username = useWatch(account, 'username') const allKeys = useWatch(account, 'allKeys') const hasRestoreWallets = @@ -165,7 +197,20 @@ export const SettingsScene: React.FC = props => { } const handleUpdateTouchId = useHandler(async () => { - await dispatch(updateTouchIdEnabled(!touchIdEnabled, account)) + if (touchIdEnabled == null) return + const newValue = !touchIdEnabled + setTouchIdEnabled(newValue) + try { + if (newValue) { + await enableTouchId(account) + } else { + await disableTouchId(account) + } + } catch (error: unknown) { + // Revert on error + setTouchIdEnabled(!newValue) + showError(error) + } }) const handleClearLogs = useHandler(async () => { @@ -359,52 +404,45 @@ export const SettingsScene: React.FC = props => { await writeForceLightAccountCreate(!forceLightAccountCreate) }) - const loadBiometryType = async (): Promise => { + useAsyncEffect( + async () => { + const currentContactsPermission = await check(permissionNames.contacts) + setLocalContactsPermissionOn(currentContactsPermission === 'granted') + }, + [], + 'SettingsScene' + ) + + // Update biometry text based on loaded biometry type + React.useEffect(() => { + if (biometricState == null) return + if (Platform.OS === 'ios') { - const biometryType = await getSupportedBiometryType() - switch (biometryType) { + switch (biometricState.biometryType) { case 'FaceID': setTouchIdText(lstrings.settings_button_use_faceID) break case 'TouchID': setTouchIdText(lstrings.settings_button_use_touchID) break - case false: break } } else { setTouchIdText(lstrings.settings_button_use_biometric) } - } - - useAsyncEffect( - async () => { - const currentContactsPermission = await check(permissionNames.contacts) - setLocalContactsPermissionOn(currentContactsPermission === 'granted') - }, - [], - 'SettingsScene' - ) + }, [biometricState]) - // Load biometry type on mount + // Watch for logSettings changes React.useEffect(() => { - if (!supportsTouchId) return - - loadBiometryType().catch((error: unknown) => { - showError(error) - }) - - // Watch for logSettings changes const cleanup = context.watch('logSettings', logSettings => { setDefaultLogLevel(logSettings.defaultLogLevel) }) - // Cleanup function to remove the watcher on unmount return () => { if (cleanup != null) cleanup() } - }, [context, supportsTouchId]) + }, [context]) // Show a modal if we have a pending OTP resent when we enter the scene: React.useEffect(() => { @@ -524,7 +562,7 @@ export const SettingsScene: React.FC = props => { onPress={handleTogglePinLoginEnabled} /> )} - {supportsTouchId && !isLightAccount && ( + {supportsTouchId && !isLightAccount && touchIdEnabled != null && ( = T extends object ? { [P in keyof T]?: DeepPartial @@ -22,7 +31,7 @@ interface Props { initialState?: FakeState } -export function FakeProviders(props: Props) { +export function FakeProviders(props: Props): React.JSX.Element { const { children, initialState = {} } = props const store = React.useMemo( @@ -30,13 +39,15 @@ export function FakeProviders(props: Props) { [initialState] ) return ( - - {renderStateProviders( - - {children} - - )} - + + + {renderStateProviders( + + {children} + + )} + + ) } From 8aa6db307871d5ef1e11c73898be1460bcfe8ea2 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 26 Nov 2025 13:17:20 -0800 Subject: [PATCH 71/77] perf: add one-time migration to clean up denomination settings Add a migration that removes default denomination values from the synced settings file. This migration: 1. Runs once per account (tracked via denominationSettingsOptimized flag) 2. Compares each saved denomination to the default from currencyInfo 3. Removes entries that match the default (they're derived on-demand) 4. Reduces settings file size for existing accounts The migration runs in the background after ACCOUNT_INIT_COMPLETE to avoid blocking the login flow. For existing accounts with many saved denominations, this significantly reduces the settings file size. --- .../__snapshots__/RootReducer.test.ts.snap | 1 + src/actions/LoginActions.tsx | 8 ++ src/actions/SettingsActions.tsx | 94 +++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap index 03af2fbd4b3..4e2b61981c9 100644 --- a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap +++ b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap @@ -113,6 +113,7 @@ exports[`initialState 1`] = ` "defaultFiat": "USD", "defaultIsoFiat": "iso:USD", "denominationSettings": {}, + "denominationSettingsOptimized": false, "developerModeOn": false, "isAccountBalanceVisible": true, "mostRecentWallets": [], diff --git a/src/actions/LoginActions.tsx b/src/actions/LoginActions.tsx index 169431fab03..b5bbda44096 100644 --- a/src/actions/LoginActions.tsx +++ b/src/actions/LoginActions.tsx @@ -16,6 +16,7 @@ import { sprintf } from 'sprintf-js' import { type DenominationSettings, + migrateDenominationSettings, readSyncedSettings } from '../actions/SettingsActions' import { ConfirmContinueModal } from '../components/modals/ConfirmContinueModal' @@ -311,6 +312,13 @@ export function initializeAccount( data: { ...accountInitObject } }) + // Run one-time migration to clean up denomination settings in background + migrateDenominationSettings(account, syncedSettings).catch( + (error: unknown) => { + console.log('Failed to migrate denomination settings:', error) + } + ) + await dispatch(refreshAccountReferral()) refreshTouchId(account).catch(() => { diff --git a/src/actions/SettingsActions.tsx b/src/actions/SettingsActions.tsx index 04554e2260e..76809e3077c 100644 --- a/src/actions/SettingsActions.tsx +++ b/src/actions/SettingsActions.tsx @@ -418,6 +418,8 @@ export const asSyncedAccountSettings = asObject({ asDenominationSettings, () => ({}) ), + // Flag to track one-time denomination settings cleanup migration + denominationSettingsOptimized: asMaybe(asBoolean, false), securityCheckedWallets: asMaybe( asSecurityCheckedWallets, () => ({}) @@ -575,3 +577,95 @@ const updateCurrencySettings = ( updatedSettings.denominationSettings[pluginId][currencyCode] = denomination return updatedSettings } + +/** + * One-time migration to clean up denomination settings by removing entries + * that match the default values from currencyInfo. This reduces the size of + * the synced settings file and speeds up subsequent logins. + * + * Only runs once per account - tracked via denominationSettingsOptimized flag. + */ +export async function migrateDenominationSettings( + account: EdgeAccount, + syncedSettings: SyncedAccountSettings +): Promise { + const { denominationSettings, denominationSettingsOptimized } = syncedSettings + + // Already migrated or no settings to clean + if (denominationSettingsOptimized) return + if ( + denominationSettings == null || + Object.keys(denominationSettings).length === 0 + ) { + // No denomination settings to clean, just set the flag + await writeSyncedSettings(account, { + ...syncedSettings, + denominationSettingsOptimized: true + }) + return + } + + // Clean up denomination settings by removing entries that match defaults + const cleanedSettings: DenominationSettings = {} + let needsCleanup = false + + for (const pluginId of Object.keys(denominationSettings)) { + const currencyConfig = account.currencyConfig[pluginId] + if (currencyConfig == null) continue + + const { currencyInfo, allTokens } = currencyConfig + const pluginDenoms = denominationSettings[pluginId] + if (pluginDenoms == null) continue + + cleanedSettings[pluginId] = {} + + for (const currencyCode of Object.keys(pluginDenoms)) { + const savedDenom = pluginDenoms[currencyCode] + if (savedDenom == null) continue + + // Find the default denomination for this currency + let defaultDenom: EdgeDenomination | undefined + if (currencyCode === currencyInfo.currencyCode) { + defaultDenom = currencyInfo.denominations[0] + } else { + // Look for token + for (const tokenId of Object.keys(allTokens)) { + const token = allTokens[tokenId] + if (token.currencyCode === currencyCode) { + defaultDenom = token.denominations[0] + break + } + } + } + + // Only keep if different from default + if ( + defaultDenom == null || + savedDenom.multiplier !== defaultDenom.multiplier || + savedDenom.name !== defaultDenom.name + ) { + // @ts-expect-error - DenominationSettings type allows undefined + cleanedSettings[pluginId][currencyCode] = savedDenom + } else { + needsCleanup = true + } + } + + // Remove empty plugin entries + if (Object.keys(cleanedSettings[pluginId] ?? {}).length === 0) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete cleanedSettings[pluginId] + } + } + + // Write cleaned settings with optimization flag + await writeSyncedSettings(account, { + ...syncedSettings, + denominationSettings: cleanedSettings, + denominationSettingsOptimized: true + }) + + if (needsCleanup) { + console.log('Denomination settings cleaned up - removed default values') + } +} From fa389715e8e8d38bb32b35e96e4c331c2fb90c7d Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 26 Nov 2025 13:15:47 -0800 Subject: [PATCH 72/77] perf: simplify denomination settings merge in initializeAccount Remove the expensive nested loop that merged default denomination settings with synced settings. Since default denominations are no longer populated in the LOGIN reducer, we can use synced settings directly. The synced settings contain only user customizations. Default denominations are derived on-demand from currencyInfo via selectors. --- src/actions/LoginActions.tsx | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/actions/LoginActions.tsx b/src/actions/LoginActions.tsx index b5bbda44096..9578de25f17 100644 --- a/src/actions/LoginActions.tsx +++ b/src/actions/LoginActions.tsx @@ -287,25 +287,10 @@ export function initializeAccount( } } - const defaultDenominationSettings = state.ui.settings.denominationSettings - const syncedDenominationSettings = + // Use synced denomination settings directly (user customizations only). + // Default denominations are derived on-demand from currencyInfo via selectors. + accountInitObject.denominationSettings = syncedSettings?.denominationSettings ?? {} - const mergedDenominationSettings: DenominationSettings = {} - - for (const plugin of Object.keys(defaultDenominationSettings)) { - const entries: DenominationSettings[string] = {} - for (const code of Object.keys(entries)) { - entries[code] = { - ...defaultDenominationSettings[plugin]?.[code], - ...syncedDenominationSettings[plugin]?.[code], - name: '', - multiplier: '', - symbol: '' - } - } - mergedDenominationSettings[plugin] = entries - } - accountInitObject.denominationSettings = { ...mergedDenominationSettings } dispatch({ type: 'ACCOUNT_INIT_COMPLETE', From 954bb96f52a2c70be15bc816c2da235a1e85038d Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 5 Dec 2025 14:09:05 -0800 Subject: [PATCH 73/77] perf: remove denomination defaults from LOGIN reducer Remove the expensive loop that populated denomination defaults for all currency plugins and tokens during LOGIN. This loop iterated through every plugin and token to set default denominations in Redux state. Denomination defaults are already available from currencyInfo and can be derived on-demand via selectors (selectDisplayDenom already has fallback logic to currencyConfig). This change eliminates unnecessary work during the login critical path. --- src/reducers/scenes/SettingsReducer.ts | 42 +++++++++++--------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/reducers/scenes/SettingsReducer.ts b/src/reducers/scenes/SettingsReducer.ts index 66f06783588..cad5a520f99 100644 --- a/src/reducers/scenes/SettingsReducer.ts +++ b/src/reducers/scenes/SettingsReducer.ts @@ -2,6 +2,7 @@ import type { EdgeAccount, EdgeTokenId } from 'edge-core-js' import { asSyncedAccountSettings, + type DenominationSettings, type SyncedAccountSettings } from '../../actions/SettingsActions' import type { SortOption } from '../../components/modals/WalletListSortModal' @@ -47,26 +48,10 @@ export const settingsLegacy = ( ): SettingsState => { switch (action.type) { case 'LOGIN': { - const { account, walletSort } = action.data - - // Setup default denominations for settings based on currencyInfo - const newState = { ...state, walletSort } - for (const pluginId of Object.keys(account.currencyConfig)) { - const { currencyInfo } = account.currencyConfig[pluginId] - const { currencyCode } = currencyInfo - if (newState.denominationSettings[pluginId] == null) - state.denominationSettings[pluginId] = {} - // @ts-expect-error - this is because laziness - newState.denominationSettings[pluginId][currencyCode] ??= - currencyInfo.denominations[0] - for (const token of currencyInfo.metaTokens ?? []) { - const tokenCode = token.currencyCode - // @ts-expect-error - this is because laziness - newState.denominationSettings[pluginId][tokenCode] = - token.denominations[0] - } - } - return newState + const { walletSort } = action.data + // Denomination defaults are derived from currencyInfo on-demand via + // selectors, so we don't need to populate them here. + return { ...state, walletsSort: walletSort } } case 'ACCOUNT_INIT_COMPLETE': { @@ -143,13 +128,22 @@ export const settingsLegacy = ( case 'UI/SETTINGS/SET_DENOMINATION_KEY': { const { pluginId, currencyCode, denomination } = action.data - const newDenominationSettings = { ...state.denominationSettings } - // @ts-expect-error - this is because laziness - newDenominationSettings[pluginId][currencyCode] = denomination + + // Ensure pluginId object exists before setting denomination + const newDenominationSettings: DenominationSettings = { + ...state.denominationSettings, + [pluginId]: { + ...state.denominationSettings[pluginId], + [currencyCode]: { + ...denomination, + symbol: denomination.symbol ?? undefined + } + } + } return { ...state, - ...newDenominationSettings + denominationSettings: newDenominationSettings } } From cbab933b131f4788b4b1d95c8ac82519d2da1801 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 5 Dec 2025 14:11:46 -0800 Subject: [PATCH 74/77] refactor: separate new account and existing account flows Separate out the new account workflow to `navigateToNewAccountFlow` in initializeAccount and the existing account flow into `navigateToExistingAccountHome`. --- src/actions/LoginActions.tsx | 243 +++++++++++++++++++---------------- 1 file changed, 135 insertions(+), 108 deletions(-) diff --git a/src/actions/LoginActions.tsx b/src/actions/LoginActions.tsx index 9578de25f17..5b4ce457b85 100644 --- a/src/actions/LoginActions.tsx +++ b/src/actions/LoginActions.tsx @@ -15,9 +15,9 @@ import performance from 'react-native-performance' import { sprintf } from 'sprintf-js' import { - type DenominationSettings, migrateDenominationSettings, - readSyncedSettings + readSyncedSettings, + type SyncedAccountSettings } from '../actions/SettingsActions' import { ConfirmContinueModal } from '../components/modals/ConfirmContinueModal' import { FioCreateHandleModal } from '../components/modals/FioCreateHandleModal' @@ -32,7 +32,7 @@ import { } from '../reducers/scenes/SettingsReducer' import type { WalletCreateItem } from '../selectors/getCreateWalletList' import { config } from '../theme/appConfig' -import type { Dispatch, ThunkAction } from '../types/reduxTypes' +import type { Dispatch, GetState, ThunkAction } from '../types/reduxTypes' import type { EdgeAppSceneProps, NavigationBase } from '../types/routerTypes' import { currencyCodesToEdgeAssets } from '../util/CurrencyInfoHelpers' import { logActivity } from '../util/logger' @@ -83,116 +83,35 @@ export function initializeAccount( account: EdgeAccount ): ThunkAction> { return async (dispatch, getState) => { + const { newAccount } = account const rootNavigation = getRootNavigation(navigation) // Log in as quickly as possible, but we do need the sort order: const syncedSettings = await readSyncedSettings(account) const { walletsSort } = syncedSettings dispatch({ type: 'LOGIN', data: { account, walletSort: walletsSort } }) - const { newAccount } = account const referralPromise = dispatch(loadAccountReferral(account)) // Track whether we showed a non-survey modal or some other interrupting UX. // We don't want to pester the user with too many interrupting flows. let hideSurvey = false + // Account-type specific navigation and setup if (newAccount) { - await referralPromise - let { defaultFiat } = syncedSettings - - const [phoneCurrency] = getCurrencies() - if (typeof phoneCurrency === 'string' && phoneCurrency.length >= 3) { - defaultFiat = phoneCurrency - } - // Ensure the creation reason is available before creating wallets: - const accountReferralCurrencyCodes = - getState().account.accountReferral.currencyCodes - const defaultSelection = - accountReferralCurrencyCodes != null - ? currencyCodesToEdgeAssets(account, accountReferralCurrencyCodes) - : config.defaultWallets - const fiatCurrencyCode = 'iso:' + defaultFiat - - // Ensure we have initialized the account settings first so we can begin - // keeping track of token warnings shown from the initial selected assets - // during account creation - await readLocalAccountSettings(account) - - const newAccountFlow = async ( - navigation: EdgeAppSceneProps< - 'createWalletSelectCrypto' | 'createWalletSelectCryptoNewAccount' - >['navigation'], - items: WalletCreateItem[] - ): Promise => { - navigation.replace('edgeTabs', { screen: 'home' }) - const createWalletsPromise = createCustomWallets( - account, - fiatCurrencyCode, - items, - dispatch - ).catch((error: unknown) => { - showError(error) - }) - - // New user FIO handle registration flow (if env is properly configured) - const { freeRegApiToken = '', freeRegRefCode = '' } = - typeof ENV.FIO_INIT === 'object' ? ENV.FIO_INIT : {} - if (freeRegApiToken !== '' && freeRegRefCode !== '') { - hideSurvey = true - const isCreateHandle = await Airship.show(bridge => ( - - )) - if (isCreateHandle) { - navigation.navigate('fioCreateHandle', { - freeRegApiToken, - freeRegRefCode - }) - } - } - - await createWalletsPromise - dispatch( - logEvent('Signup_Complete', { - numAccounts: getState().core.context.localUsers.length - }) - ) - } - - rootNavigation.replace('edgeApp', { - screen: 'edgeAppStack', - params: { - screen: 'createWalletSelectCryptoNewAccount', - params: { - newAccountFlow, - defaultSelection, - disableLegacy: true - } - } - }) - - performance.mark('loginEnd', { detail: { isNewAccount: newAccount } }) + await navigateToNewAccountFlow( + rootNavigation, + account, + syncedSettings, + referralPromise, + dispatch, + getState + ) } else { - const { defaultScreen } = getDeviceSettings() - rootNavigation.replace('edgeApp', { - screen: 'edgeAppStack', - params: { - screen: 'edgeTabs', - params: - defaultScreen === 'home' - ? { screen: 'home' } - : { screen: 'walletsTab', params: { screen: 'walletList' } } - } - }) - referralPromise.catch(() => { - console.log(`Failed to load account referral info`) - }) - - performance.mark('loginEnd', { detail: { isNewAccount: newAccount } }) + navigateToExistingAccountHome(rootNavigation, referralPromise) } + performance.mark('loginEnd', { detail: { isNewAccount: newAccount } }) + // Show a notice for deprecated electrum server settings const pluginIdsNeedingUserAction: string[] = [] for (const pluginId in account.currencyConfig) { @@ -256,23 +175,19 @@ export function initializeAccount( console.log('Wallet Infos:', filteredWalletInfos) // Merge and prepare settings files: + const walletInfo = newAccount + ? undefined + : getFirstActiveWalletInfo(account) let accountInitObject: AccountInitPayload = { ...initialState, account, - tokenId: null, + tokenId: walletInfo?.tokenId ?? null, pinLoginEnabled: false, - walletId: '', + walletId: walletInfo?.walletId ?? '', walletsSort: 'manual' } try { - if (!newAccount) { - // We have a wallet - const { walletId, tokenId } = getFirstActiveWalletInfo(account) - accountInitObject.walletId = walletId - accountInitObject.tokenId = tokenId - } - accountInitObject = { ...accountInitObject, ...syncedSettings } const loadedLocalSettings = await readLocalAccountSettings(account) @@ -309,6 +224,7 @@ export function initializeAccount( refreshTouchId(account).catch(() => { // We have always failed silently here }) + if ( await showNotificationPermissionReminder({ appName: config.appName, @@ -327,11 +243,11 @@ export function initializeAccount( ) { hideSurvey = true } - } catch (error: any) { + } catch (error: unknown) { showError(error) } - // Post login stuff: + // Post login stuff: Survey modal (existing accounts only) if ( !newAccount && !hideSurvey && @@ -347,6 +263,117 @@ export function initializeAccount( } } +/** + * Navigate to wallet creation flow for new accounts. + */ +async function navigateToNewAccountFlow( + rootNavigation: NavigationBase, + account: EdgeAccount, + syncedSettings: SyncedAccountSettings, + referralPromise: Promise, + dispatch: Dispatch, + getState: GetState +): Promise { + await referralPromise + let { defaultFiat } = syncedSettings + + const [phoneCurrency] = getCurrencies() + if (typeof phoneCurrency === 'string' && phoneCurrency.length >= 3) { + defaultFiat = phoneCurrency + } + + // Ensure the creation reason is available before creating wallets: + const accountReferralCurrencyCodes = + getState().account.accountReferral.currencyCodes + const defaultSelection = + accountReferralCurrencyCodes != null + ? currencyCodesToEdgeAssets(account, accountReferralCurrencyCodes) + : config.defaultWallets + const fiatCurrencyCode = 'iso:' + defaultFiat + + // Ensure we have initialized the account settings first so we can begin + // keeping track of token warnings shown from the initial selected assets + // during account creation + await readLocalAccountSettings(account) + + const newAccountFlow = async ( + navigation: EdgeAppSceneProps< + 'createWalletSelectCrypto' | 'createWalletSelectCryptoNewAccount' + >['navigation'], + items: WalletCreateItem[] + ): Promise => { + navigation.replace('edgeTabs', { screen: 'home' }) + const createWalletsPromise = createCustomWallets( + account, + fiatCurrencyCode, + items, + dispatch + ).catch((error: unknown) => { + showError(error) + }) + + // New user FIO handle registration flow (if env is properly configured) + const { freeRegApiToken = '', freeRegRefCode = '' } = + typeof ENV.FIO_INIT === 'object' ? ENV.FIO_INIT : {} + if (freeRegApiToken !== '' && freeRegRefCode !== '') { + const isCreateHandle = await Airship.show(bridge => ( + + )) + if (isCreateHandle) { + navigation.navigate('fioCreateHandle', { + freeRegApiToken, + freeRegRefCode + }) + } + } + + await createWalletsPromise + dispatch( + logEvent('Signup_Complete', { + numAccounts: getState().core.context.localUsers.length + }) + ) + } + + rootNavigation.replace('edgeApp', { + screen: 'edgeAppStack', + params: { + screen: 'createWalletSelectCryptoNewAccount', + params: { + newAccountFlow, + defaultSelection, + disableLegacy: true + } + } + }) +} + +/** + * Navigate to home screen for existing accounts. + */ +function navigateToExistingAccountHome( + rootNavigation: NavigationBase, + referralPromise: Promise +): void { + const { defaultScreen } = getDeviceSettings() + rootNavigation.replace('edgeApp', { + screen: 'edgeAppStack', + params: { + screen: 'edgeTabs', + params: + defaultScreen === 'home' + ? { screen: 'home' } + : { screen: 'walletsTab', params: { screen: 'walletList' } } + } + }) + referralPromise.catch(() => { + console.log(`Failed to load account referral info`) + }) +} + export function getRootNavigation(navigation: NavigationBase): NavigationBase { while (true) { const parent: NavigationBase = navigation.getParent() From ef36815d477a4cbc2239792396c55cc36f10b917 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Fri, 5 Dec 2025 14:44:31 -0800 Subject: [PATCH 75/77] refactor: consolidate ACCOUNT_INIT_COMPLETE into LOGIN action Eliminate the ACCOUNT_INIT_COMPLETE action by loading all settings upfront and dispatching them in the LOGIN action. This enables immediate navigation after login since all required state is available in Redux right away. - Load synced and local settings in parallel before LOGIN dispatch - Expand LOGIN action payload to include syncedSettings and localSettings - Remove ACCOUNT_INIT_COMPLETE action type and AccountInitPayload - Remove pinLoginEnabled from Redux, load locally in SettingsScene - Remove togglePinLoginEnabled thunk from SettingsActions - Update WalletsReducer to remove ACCOUNT_INIT_COMPLETE handling - Update SpendingLimitsReducer to read from LOGIN payload - Update PasswordReminderReducer to read from LOGIN payload - Remove getFirstActiveWalletInfo (walletId/currencyCode no longer set at login) --- eslint.config.mjs | 2 - .../__snapshots__/RootReducer.test.ts.snap | 1 - src/__tests__/spendingLimits.test.ts | 12 +- src/actions/LoginActions.tsx | 154 ++++++------------ src/actions/SettingsActions.tsx | 34 ---- src/components/scenes/SettingsScene.tsx | 33 ++-- src/reducers/PasswordReminderReducer.ts | 20 +-- src/reducers/SpendingLimitsReducer.ts | 12 +- src/reducers/scenes/SettingsReducer.ts | 49 ++---- src/types/reduxActions.ts | 10 +- 10 files changed, 107 insertions(+), 220 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 957e21a666c..ad9c0204ab9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -491,9 +491,7 @@ export default [ 'src/plugins/stake-plugins/util/builder.ts', 'src/reducers/ExchangeInfoReducer.ts', 'src/reducers/NetworkReducer.ts', - 'src/reducers/PasswordReminderReducer.ts', - 'src/reducers/SpendingLimitsReducer.ts', 'src/selectors/getCreateWalletList.ts', 'src/selectors/SettingsSelectors.ts', 'src/state/createStateProvider.tsx', diff --git a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap index 4e2b61981c9..c159991a518 100644 --- a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap +++ b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap @@ -134,7 +134,6 @@ exports[`initialState 1`] = ` "nonPasswordLoginsLimit": 4, "passwordUseCount": 0, }, - "pinLoginEnabled": false, "preferredSwapPluginId": undefined, "preferredSwapPluginType": undefined, "rampLastCryptoSelection": undefined, diff --git a/src/__tests__/spendingLimits.test.ts b/src/__tests__/spendingLimits.test.ts index f099f917c99..cb09a18808f 100644 --- a/src/__tests__/spendingLimits.test.ts +++ b/src/__tests__/spendingLimits.test.ts @@ -14,12 +14,14 @@ describe('spendingLimits', () => { describe('when logging in', () => { it('should update', () => { const actual = spendingLimits(initialState, { - type: 'ACCOUNT_INIT_COMPLETE', + type: 'LOGIN', data: { - spendingLimits: { - transaction: { - isEnabled: false, - amount: 150 + localSettings: { + spendingLimits: { + transaction: { + isEnabled: false, + amount: 150 + } } } } as any diff --git a/src/actions/LoginActions.tsx b/src/actions/LoginActions.tsx index 5b4ce457b85..a2e1071dd10 100644 --- a/src/actions/LoginActions.tsx +++ b/src/actions/LoginActions.tsx @@ -1,8 +1,4 @@ -import type { - EdgeAccount, - EdgeCreateCurrencyWallet, - EdgeTokenId -} from 'edge-core-js/types' +import type { EdgeAccount, EdgeCreateCurrencyWallet } from 'edge-core-js/types' import { hasSecurityAlerts, refreshTouchId, @@ -26,10 +22,6 @@ import { Airship, showError } from '../components/services/AirshipInstance' import { ENV } from '../env' import { getExperimentConfig } from '../experimentConfig' import { lstrings } from '../locales/strings' -import { - type AccountInitPayload, - initialState -} from '../reducers/scenes/SettingsReducer' import type { WalletCreateItem } from '../selectors/getCreateWalletList' import { config } from '../theme/appConfig' import type { Dispatch, GetState, ThunkAction } from '../types/reduxTypes' @@ -56,28 +48,6 @@ import { const PER_WALLET_TIMEOUT = 5000 const MIN_CREATE_WALLET_TIMEOUT = 20000 -function getFirstActiveWalletInfo(account: EdgeAccount): { - walletId: string - tokenId: EdgeTokenId -} { - // Find the first wallet: - const [walletId] = account.activeWalletIds - const walletKey = account.allKeys.find(key => key.id === walletId) - - // Find the matching currency code: - if (walletKey != null) { - for (const pluginId of Object.keys(account.currencyConfig)) { - const { currencyInfo } = account.currencyConfig[pluginId] - if (currencyInfo.walletType === walletKey.type) { - return { walletId, tokenId: null } - } - } - } - - // The user has no wallets: - return { walletId: '', tokenId: null } -} - export function initializeAccount( navigation: NavigationBase, account: EdgeAccount @@ -86,17 +56,25 @@ export function initializeAccount( const { newAccount } = account const rootNavigation = getRootNavigation(navigation) - // Log in as quickly as possible, but we do need the sort order: - const syncedSettings = await readSyncedSettings(account) - const { walletsSort } = syncedSettings - dispatch({ type: 'LOGIN', data: { account, walletSort: walletsSort } }) - const referralPromise = dispatch(loadAccountReferral(account)) + // Load all settings upfront so we can navigate immediately after LOGIN + const [syncedSettings, localSettings] = await Promise.all([ + readSyncedSettings(account), + readLocalAccountSettings(account) + ]) - // Track whether we showed a non-survey modal or some other interrupting UX. - // We don't want to pester the user with too many interrupting flows. - let hideSurvey = false + // Dispatch LOGIN with all settings - this enables immediate navigation + dispatch({ + type: 'LOGIN', + data: { + account, + syncedSettings, + localSettings + } + }) - // Account-type specific navigation and setup + const referralPromise = dispatch(loadAccountReferral(account)) + + // Navigate immediately - all settings are now in Redux if (newAccount) { await navigateToNewAccountFlow( rootNavigation, @@ -112,6 +90,10 @@ export function initializeAccount( performance.mark('loginEnd', { detail: { isNewAccount: newAccount } }) + // Track whether we showed a non-survey modal or some other interrupting UX. + // We don't want to pester the user with too many interrupting flows. + let hideSurvey = false + // Show a notice for deprecated electrum server settings const pluginIdsNeedingUserAction: string[] = [] for (const pluginId in account.currencyConfig) { @@ -162,9 +144,6 @@ export function initializeAccount( hideSurvey = true } - const state = getState() - const { context } = state.core - // Sign up for push notifications: dispatch(registerNotificationsV2()).catch((error: unknown) => { console.error(error) @@ -174,77 +153,36 @@ export function initializeAccount( const filteredWalletInfos = walletInfos.map(({ keys, id, ...info }) => info) console.log('Wallet Infos:', filteredWalletInfos) - // Merge and prepare settings files: - const walletInfo = newAccount - ? undefined - : getFirstActiveWalletInfo(account) - let accountInitObject: AccountInitPayload = { - ...initialState, - account, - tokenId: walletInfo?.tokenId ?? null, - pinLoginEnabled: false, - walletId: walletInfo?.walletId ?? '', - walletsSort: 'manual' - } - - try { - accountInitObject = { ...accountInitObject, ...syncedSettings } - - const loadedLocalSettings = await readLocalAccountSettings(account) - accountInitObject = { ...accountInitObject, ...loadedLocalSettings } - - for (const userInfo of context.localUsers) { - if ( - userInfo.loginId === account.rootLoginId && - userInfo.pinLoginEnabled - ) { - accountInitObject.pinLoginEnabled = true - } + // Run one-time migration to clean up denomination settings in background + migrateDenominationSettings(account, syncedSettings).catch( + (error: unknown) => { + console.log('Failed to migrate denomination settings:', error) } + ) - // Use synced denomination settings directly (user customizations only). - // Default denominations are derived on-demand from currencyInfo via selectors. - accountInitObject.denominationSettings = - syncedSettings?.denominationSettings ?? {} + await dispatch(refreshAccountReferral()) - dispatch({ - type: 'ACCOUNT_INIT_COMPLETE', - data: { ...accountInitObject } - }) + refreshTouchId(account).catch(() => { + // We have always failed silently here + }) - // Run one-time migration to clean up denomination settings in background - migrateDenominationSettings(account, syncedSettings).catch( - (error: unknown) => { - console.log('Failed to migrate denomination settings:', error) + if ( + await showNotificationPermissionReminder({ + appName: config.appName, + onLogEvent(event, values) { + dispatch(logEvent(event, values)) + }, + onNotificationPermit(info) { + dispatch(updateNotificationSettings(info.notificationOptIns)).catch( + (error: unknown) => { + trackError(error, 'LoginScene:onLogin:setDeviceSettings') + console.error(error) + } + ) } - ) - - await dispatch(refreshAccountReferral()) - - refreshTouchId(account).catch(() => { - // We have always failed silently here }) - - if ( - await showNotificationPermissionReminder({ - appName: config.appName, - onLogEvent(event, values) { - dispatch(logEvent(event, values)) - }, - onNotificationPermit(info) { - dispatch(updateNotificationSettings(info.notificationOptIns)).catch( - (error: unknown) => { - trackError(error, 'LoginScene:onLogin:setDeviceSettings') - console.error(error) - } - ) - } - }) - ) { - hideSurvey = true - } - } catch (error: unknown) { - showError(error) + ) { + hideSurvey = true } // Post login stuff: Survey modal (existing accounts only) diff --git a/src/actions/SettingsActions.tsx b/src/actions/SettingsActions.tsx index 76809e3077c..588abcf9d41 100644 --- a/src/actions/SettingsActions.tsx +++ b/src/actions/SettingsActions.tsx @@ -224,40 +224,6 @@ export function setDenominationKeyRequest( } } -export function togglePinLoginEnabled( - pinLoginEnabled: boolean -): ThunkAction> { - return async (dispatch, getState) => { - const state = getState() - const { context, account } = state.core - - dispatch({ - type: 'UI/SETTINGS/TOGGLE_PIN_LOGIN_ENABLED', - data: { pinLoginEnabled } - }) - return await account - .changePin({ enableLogin: pinLoginEnabled }) - .catch(async (error: unknown) => { - showError(error) - - let pinLoginEnabled = false - for (const userInfo of context.localUsers) { - if ( - userInfo.loginId === account.rootLoginId && - userInfo.pinLoginEnabled - ) { - pinLoginEnabled = true - } - } - - dispatch({ - type: 'UI/SETTINGS/TOGGLE_PIN_LOGIN_ENABLED', - data: { pinLoginEnabled } - }) - }) - } -} - export async function showReEnableOtpModal( account: EdgeAccount ): Promise { diff --git a/src/components/scenes/SettingsScene.tsx b/src/components/scenes/SettingsScene.tsx index f3bab7249b7..9c597e6f861 100644 --- a/src/components/scenes/SettingsScene.tsx +++ b/src/components/scenes/SettingsScene.tsx @@ -28,8 +28,7 @@ import { logoutRequest } from '../../actions/LoginActions' import { setAutoLogoutTimeInSecondsRequest, showReEnableOtpModal, - showUnlockSettingsModal, - togglePinLoginEnabled + showUnlockSettingsModal } from '../../actions/SettingsActions' import { ENV } from '../../env' import { useAsyncEffect } from '../../hooks/useAsyncEffect' @@ -75,9 +74,6 @@ export const SettingsScene: React.FC = props => { state => state.ui.settings.developerModeOn ) const isLocked = useSelector(state => state.ui.settings.changesLocked) - const pinLoginEnabled = useSelector( - state => state.ui.settings.pinLoginEnabled - ) const spamFilterOn = useSelector(state => state.ui.settings.spamFilterOn) const account = useSelector(state => state.core.account) @@ -121,6 +117,15 @@ export const SettingsScene: React.FC = props => { const context = useSelector(state => state.core.context) const logSettings = useWatch(context, 'logSettings') + // Load pin login state locally (not from Redux) and make it mutable + const [pinLoginEnabled, setPinLoginEnabled] = React.useState( + () => + context?.localUsers?.some( + userInfo => + userInfo.loginId === account.rootLoginId && userInfo.pinLoginEnabled + ) ?? false + ) + const [localContactPermissionOn, setLocalContactsPermissionOn] = React.useState(false) const [isDarkTheme, setIsDarkTheme] = React.useState( @@ -222,7 +227,15 @@ export const SettingsScene: React.FC = props => { }) const handleTogglePinLoginEnabled = useHandler(async () => { - await dispatch(togglePinLoginEnabled(!pinLoginEnabled)) + const newValue = !pinLoginEnabled + setPinLoginEnabled(newValue) + try { + await account.changePin({ enableLogin: newValue }) + } catch (error: unknown) { + // Revert on error + setPinLoginEnabled(!newValue) + showError(error) + } }) const handleToggleDarkTheme = useHandler(async () => { @@ -554,22 +567,22 @@ export const SettingsScene: React.FC = props => { onPress={handleDefaultFiat} /> - {isLightAccount ? null : ( + {!isLightAccount ? ( - )} - {supportsTouchId && !isLightAccount && touchIdEnabled != null && ( + ) : null} + {supportsTouchId && !isLightAccount && touchIdEnabled != null ? ( - )} + ) : null} { +): boolean => { switch (action.type) { - case 'ACCOUNT_INIT_COMPLETE': + case 'LOGIN': { + return action.data.localSettings.spendingLimits.transaction.isEnabled + } case 'SPENDING_LIMITS/NEW_SPENDING_LIMITS': { return action.data.spendingLimits.transaction.isEnabled } @@ -27,9 +29,11 @@ export const isEnabled = ( export const amount = ( state: number = initialState.transaction.amount, action: Action -) => { +): number => { switch (action.type) { - case 'ACCOUNT_INIT_COMPLETE': + case 'LOGIN': { + return action.data.localSettings.spendingLimits.transaction.amount + } case 'SPENDING_LIMITS/NEW_SPENDING_LIMITS': { return action.data.spendingLimits.transaction.amount } diff --git a/src/reducers/scenes/SettingsReducer.ts b/src/reducers/scenes/SettingsReducer.ts index cad5a520f99..f75ea3af39d 100644 --- a/src/reducers/scenes/SettingsReducer.ts +++ b/src/reducers/scenes/SettingsReducer.ts @@ -1,11 +1,10 @@ -import type { EdgeAccount, EdgeTokenId } from 'edge-core-js' +import type { EdgeAccount } from 'edge-core-js' import { asSyncedAccountSettings, type DenominationSettings, type SyncedAccountSettings } from '../../actions/SettingsActions' -import type { SortOption } from '../../components/modals/WalletListSortModal' import type { Action } from '../../types/reduxTypes' import { asLocalAccountSettings, @@ -17,7 +16,6 @@ export const initialState: SettingsState = { ...asSyncedAccountSettings({}), ...asLocalAccountSettings({}), changesLocked: true, - pinLoginEnabled: false, settingsLoaded: null, userPausedWalletsSet: null } @@ -26,7 +24,6 @@ export interface SettingsState extends LocalAccountSettings, SyncedAccountSettings { changesLocked: boolean - pinLoginEnabled: boolean settingsLoaded: boolean | null // A copy of `userPausedWallets`, but as a set. @@ -34,12 +31,10 @@ export interface SettingsState userPausedWalletsSet: Set | null } -export interface AccountInitPayload extends SettingsState { +export interface LoginPayload { account: EdgeAccount - tokenId: EdgeTokenId - pinLoginEnabled: boolean - walletId: string - walletsSort: SortOption + syncedSettings: SyncedAccountSettings + localSettings: LocalAccountSettings } export const settingsLegacy = ( @@ -48,36 +43,32 @@ export const settingsLegacy = ( ): SettingsState => { switch (action.type) { case 'LOGIN': { - const { walletSort } = action.data - // Denomination defaults are derived from currencyInfo on-demand via - // selectors, so we don't need to populate them here. - return { ...state, walletsSort: walletSort } - } - - case 'ACCOUNT_INIT_COMPLETE': { + const { syncedSettings, localSettings } = action.data const { autoLogoutTimeInSeconds, - contactsPermissionShown, countryCode, defaultFiat, defaultIsoFiat, denominationSettings, - developerModeOn, - isAccountBalanceVisible, mostRecentWallets, passwordRecoveryRemindersShown, - userPausedWallets, - pinLoginEnabled, preferredSwapPluginId, preferredSwapPluginType, securityCheckedWallets, - spamFilterOn, stateProvinceCode, + userPausedWallets, walletsSort, rampLastFiatCurrencyCode, rampLastCryptoSelection - } = action.data - const newState: SettingsState = { + } = syncedSettings + const { + contactsPermissionShown, + developerModeOn, + isAccountBalanceVisible, + spamFilterOn + } = localSettings + + return { ...state, autoLogoutTimeInSeconds, contactsPermissionShown, @@ -91,7 +82,6 @@ export const settingsLegacy = ( passwordRecoveryRemindersShown, userPausedWallets, userPausedWalletsSet: new Set(userPausedWallets), - pinLoginEnabled, preferredSwapPluginId: preferredSwapPluginId === '' ? undefined : preferredSwapPluginId, preferredSwapPluginType, @@ -103,7 +93,6 @@ export const settingsLegacy = ( rampLastFiatCurrencyCode, rampLastCryptoSelection } - return newState } case 'DEVELOPER_MODE_ON': { return { ...state, developerModeOn: true } @@ -118,14 +107,6 @@ export const settingsLegacy = ( return { ...state, spamFilterOn: false } } - case 'UI/SETTINGS/TOGGLE_PIN_LOGIN_ENABLED': { - const { pinLoginEnabled } = action.data - return { - ...state, - pinLoginEnabled - } - } - case 'UI/SETTINGS/SET_DENOMINATION_KEY': { const { pluginId, currencyCode, denomination } = action.data diff --git a/src/types/reduxActions.ts b/src/types/reduxActions.ts index e4a8bd429bc..724383183b0 100644 --- a/src/types/reduxActions.ts +++ b/src/types/reduxActions.ts @@ -1,6 +1,5 @@ import type { Disklet } from 'disklet' import type { - EdgeAccount, EdgeContext, EdgeCurrencyWallet, EdgeDenomination, @@ -20,7 +19,7 @@ import type { LoanManagerActions } from '../controllers/loan-manager/redux/actio import type { CcWalletMap } from '../reducers/FioReducer' import type { PermissionsState } from '../reducers/PermissionsReducer' import type { - AccountInitPayload, + LoginPayload, SettingsState } from '../reducers/scenes/SettingsReducer' import type { StakingAction } from '../reducers/StakingReducer' @@ -58,7 +57,6 @@ type NoDataActionName = export type Action = | { type: NoDataActionName } // Actions with known payloads: - | { type: 'ACCOUNT_INIT_COMPLETE'; data: AccountInitPayload } | { type: 'ACCOUNT_REFERRAL_LOADED' data: { referral: AccountReferral; cache: ReferralCache } @@ -88,7 +86,7 @@ export type Action = type: 'IS_NOTIFICATION_VIEW_ACTIVE' data: { isNotificationViewActive: boolean } } - | { type: 'LOGIN'; data: { account: EdgeAccount; walletSort: SortOption } } + | { type: 'LOGIN'; data: LoginPayload } | { type: 'MESSAGE_TWEAK_HIDDEN' data: { messageId: string; source: TweakSource } @@ -149,10 +147,6 @@ export type Action = data: { userPausedWallets: string[] } } | { type: 'UI/SETTINGS/SET_WALLETS_SORT'; data: { walletsSort: SortOption } } - | { - type: 'UI/SETTINGS/TOGGLE_PIN_LOGIN_ENABLED' - data: { pinLoginEnabled: boolean } - } | { type: 'UI/SETTINGS/UPDATE_SETTINGS'; data: { settings: SettingsState } } | { type: 'UI/SET_COUNTRY_CODE'; data: { countryCode: string | undefined } } | { From 38f7226add98d6e94175d8932766fb3fc2b0ffa6 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Mon, 22 Dec 2025 18:21:20 -0700 Subject: [PATCH 76/77] Upgrade edge-core-js@^2.38.2 --- ios/Podfile.lock | 4 ++-- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index eb6e3ab5b3b..50af5b14ff2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -15,7 +15,7 @@ PODS: - disklet (0.5.2): - React - DoubleConversion (1.1.6) - - edge-core-js (2.38.1): + - edge-core-js (2.38.2): - React-Core - edge-currency-accountbased (4.68.0): - React-Core @@ -3333,7 +3333,7 @@ SPEC CHECKSUMS: CNIOWindows: 3047f2d8165848a3936a0a755fee27c6b5ee479b disklet: 8a20bf8a568635b6e6bb8f93297dac13ee5cef98 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb - edge-core-js: 2da898293870d04110733ebf3bdc495a7c659065 + edge-core-js: 8b015465c8462879816149c8a5896a854d53e971 edge-currency-accountbased: b526ee12efefad410125c51135222b0c63e42f12 edge-currency-plugins: 6b3341707a6a5c74f837a012768dd2f6c55a691b edge-exchange-plugins: f35930ddcd5a4551a6e45334cb3f4c0295c23acd diff --git a/package.json b/package.json index 55f93b8b5c2..89d0ad2dfd5 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "deprecated-react-native-prop-types": "^5.0.0", "detect-bundler": "^1.1.0", "disklet": "^0.5.2", - "edge-core-js": "^2.38.1", + "edge-core-js": "^2.38.2", "edge-currency-accountbased": "^4.68.0", "edge-currency-monero": "^2.0.1", "edge-currency-plugins": "^3.8.10", diff --git a/yarn.lock b/yarn.lock index 49e1d337419..36c6d2e0fec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9423,10 +9423,10 @@ ed25519@0.0.4: bindings "^1.2.1" nan "^2.0.9" -edge-core-js@^2.38.1: - version "2.38.1" - resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-2.38.1.tgz#983aea8120b32f6b7165c066e4e8b74062989bc7" - integrity sha512-S3DGZJJKIWAOn2AL/MRJWq2RD/jXaDKEqGT0jl8KyWPwbiiNWS9QQXKEWc/UhK79i7y6UOEuq3iVeRflB4yyFQ== +edge-core-js@^2.38.2: + version "2.38.2" + resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-2.38.2.tgz#724835000ec76eb1ae0ea27263ea22eea170d7c4" + integrity sha512-N7lZXB58HcOLZD0gaSFwWbchFkrgO9DEv3/kWTEBAgMfcjND93NjPrKrViNvTdz/NZWMsZrHLXrnwsAO6VvVdQ== dependencies: aes-js "^3.1.0" base-x "^4.0.0" From d102426ac1ab4f9bd0b27da96522b7a036b3f96a Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Fri, 19 Dec 2025 11:56:03 -0800 Subject: [PATCH 77/77] `SceneWrapper` changes from `jon/gift-cards` --- ...reateWalletSelectCryptoScene.test.tsx.snap | 1 + ...FioConnectWalletConfirmScene.test.tsx.snap | 11 +- .../SwapConfirmationScene.test.tsx.snap | 139 +++++++++--------- .../SwapCreateScene.test.tsx.snap | 10 +- .../TransactionDetailsScene.test.tsx.snap | 22 +-- src/components/common/SceneWrapper.tsx | 23 ++- src/components/layout/SceneContainer.tsx | 89 ++++++----- src/components/scenes/DuressPinScene.tsx | 10 +- src/components/scenes/RampPendingScene.tsx | 2 +- .../scenes/Staking/StakeOptionsScene.tsx | 37 +++-- src/components/scenes/SwapCreateScene.tsx | 2 +- 11 files changed, 175 insertions(+), 171 deletions(-) diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap index 5bdfd6eeecf..754a6244f7a 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap @@ -2293,6 +2293,7 @@ exports[`CreateWalletSelectCrypto should render with loading props 1`] = ` ] } nativeID="10" + onLayout={[Function]} style={ [ { diff --git a/src/__tests__/scenes/__snapshots__/FioConnectWalletConfirmScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/FioConnectWalletConfirmScene.test.tsx.snap index 730f9f50dbd..4d716a7ff42 100644 --- a/src/__tests__/scenes/__snapshots__/FioConnectWalletConfirmScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/FioConnectWalletConfirmScene.test.tsx.snap @@ -357,15 +357,10 @@ exports[`FioConnectWalletConfirm should render with loading props 1`] = ` diff --git a/src/__tests__/scenes/__snapshots__/SwapConfirmationScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SwapConfirmationScene.test.tsx.snap index ab3a3f33d1a..6dbf122dcf9 100644 --- a/src/__tests__/scenes/__snapshots__/SwapConfirmationScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SwapConfirmationScene.test.tsx.snap @@ -273,97 +273,92 @@ exports[`SwapConfirmationScene should render with loading props 1`] = ` - - - Exchange - - - + > + Exchange + + + + diff --git a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap index 04ce071b11b..5b860764e76 100644 --- a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap @@ -228,15 +228,10 @@ exports[`TransactionDetailsScene should render 1`] = ` @@ -3121,15 +3116,10 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi diff --git a/src/components/common/SceneWrapper.tsx b/src/components/common/SceneWrapper.tsx index b8b41b68695..a484d35d953 100644 --- a/src/components/common/SceneWrapper.tsx +++ b/src/components/common/SceneWrapper.tsx @@ -171,6 +171,9 @@ function SceneWrapperComponent(props: SceneWrapperProps): React.ReactElement { const navigation = useNavigation() const isIos = Platform.OS === 'ios' + // Track dock height for content padding when dockProps is used + const [dockHeight, setDockHeight] = useState(0) + // We need to track this state in the JS thread because insets are not shared values const [isKeyboardOpen, setIsKeyboardOpen] = useState(false) useKeyboardHandler({ @@ -240,6 +243,12 @@ function SceneWrapperComponent(props: SceneWrapperProps): React.ReactElement { // Ignore inset bottom when keyboard is open because it is rendered behind it const maybeInsetBottom = !isKeyboardOpen || !avoidKeyboard ? safeAreaInsets.bottom : 0 + // Include dock height in bottom inset when dock is visible (not keyboard-only or keyboard is open) + const keyboardVisibleOnlyDock = dockProps?.keyboardVisibleOnly ?? true + const maybeDockHeight = + dockProps != null && (!keyboardVisibleOnlyDock || isKeyboardOpen) + ? dockHeight + : 0 const insets: EdgeInsets = useMemo( () => ({ top: hasHeader ? headerBarHeight : safeAreaInsets.top, @@ -248,13 +257,15 @@ function SceneWrapperComponent(props: SceneWrapperProps): React.ReactElement { maybeInsetBottom + maybeNotificationHeight + maybeTabBarHeight + - footerHeight, + footerHeight + + maybeDockHeight, left: safeAreaInsets.left }), [ footerHeight, hasHeader, headerBarHeight, + maybeDockHeight, maybeInsetBottom, maybeNotificationHeight, maybeTabBarHeight, @@ -328,7 +339,12 @@ function SceneWrapperComponent(props: SceneWrapperProps): React.ReactElement { }, [children, sceneWrapperInfo]) // Build Dock View element - const keyboardVisibleOnlyDoc = dockProps?.keyboardVisibleOnly ?? true + const handleDockLayout = React.useCallback( + (event: { nativeEvent: { layout: { height: number } } }) => { + setDockHeight(event.nativeEvent.layout.height) + }, + [] + ) const dockBaseStyle = useMemo( () => ({ position: 'absolute' as const, @@ -366,9 +382,10 @@ function SceneWrapperComponent(props: SceneWrapperProps): React.ReactElement { return { bottom } }) const shouldShowDock = - dockProps != null && (!keyboardVisibleOnlyDoc || isKeyboardVisibleDock) + dockProps != null && (!keyboardVisibleOnlyDock || isKeyboardVisibleDock) const dockElement = !shouldShowDock ? null : ( = (props: Props) => { - const { children, headerTitle, headerTitleChildren, ...sceneContainerProps } = - props + const { children, headerTitle, headerTitleChildren, undoInsetStyle } = props + + const theme = useTheme() + const styles = getStyles(theme) + + const contentInsets = React.useMemo( + () => ({ + ...undoInsetStyle, + flex: 1, + marginTop: 0, + // Built-in padding if we're not using undoInsetStyle + paddingHorizontal: + undoInsetStyle == null ? theme.rem(DEFAULT_MARGIN_REM) : 0, + paddingBottom: undoInsetStyle == null ? theme.rem(DEFAULT_MARGIN_REM) : 0 + }), + [theme, undoInsetStyle] + ) return ( - + <> {headerTitle != null ? ( - - {headerTitleChildren} - + + + {headerTitle} + {headerTitleChildren} + + + ) : null} - {children} - + {children} + ) } -interface SceneContainerViewProps { - expand?: boolean - undoTop?: boolean - undoRight?: boolean - undoBottom?: boolean - undoLeft?: boolean - undoInsetStyle?: UndoInsetStyle -} -const SceneContainerView = styled(View)( - theme => - ({ expand, undoTop, undoRight, undoBottom, undoLeft, undoInsetStyle }) => ({ - flex: expand === true ? 1 : undefined, - paddingTop: theme.rem(0.5), - paddingRight: theme.rem(0.5), - paddingBottom: theme.rem(0.5), - paddingLeft: theme.rem(0.5), - marginTop: undoTop === true ? undoInsetStyle?.marginTop : undefined, - marginRight: undoRight === true ? undoInsetStyle?.marginRight : undefined, - marginBottom: - undoBottom === true ? undoInsetStyle?.marginBottom : undefined, - marginLeft: undoLeft === true ? undoInsetStyle?.marginLeft : undefined - }) -) +const getStyles = cacheStyles((theme: Theme) => ({ + headerContainer: { + justifyContent: 'center', + overflow: 'visible', + paddingLeft: theme.rem(DEFAULT_MARGIN_REM) + }, + title: { + fontSize: theme.rem(1.2), + fontFamily: theme.fontFaceMedium + }, + titleContainer: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'space-between', + marginHorizontal: theme.rem(DEFAULT_MARGIN_REM), + marginBottom: theme.rem(DEFAULT_MARGIN_REM) + } +})) diff --git a/src/components/scenes/DuressPinScene.tsx b/src/components/scenes/DuressPinScene.tsx index 7280bc45fb1..c41ba4d79a3 100644 --- a/src/components/scenes/DuressPinScene.tsx +++ b/src/components/scenes/DuressPinScene.tsx @@ -19,7 +19,7 @@ import { DigitInput, MAX_PIN_LENGTH } from './inputs/DigitInput' interface Props extends EdgeAppSceneProps<'duressPin'> {} -export const DuressPinScene = (props: Props) => { +export const DuressPinScene: React.FC = (props: Props) => { const { navigation } = props const theme = useTheme() const styles = getStyles(theme) @@ -29,7 +29,7 @@ export const DuressPinScene = (props: Props) => { const [pin, setPin] = React.useState('') const isValidPin = pin.length === MAX_PIN_LENGTH - const handleComplete = () => { + const handleComplete = useHandler(() => { if (!isValidPin) return account .checkPin(pin) @@ -47,10 +47,10 @@ export const DuressPinScene = (props: Props) => { showToast(lstrings.duress_mode_set_pin_success) navigation.navigate('duressModeSetting') }) - .catch(err => { + .catch((err: unknown) => { showError(err) }) - } + }) const handleChangePin = useHandler((newPin: string) => { // Change pin only when input are numbers @@ -66,7 +66,7 @@ export const DuressPinScene = (props: Props) => { return ( - + = props => { return ( - + {error != null ? ( diff --git a/src/components/scenes/Staking/StakeOptionsScene.tsx b/src/components/scenes/Staking/StakeOptionsScene.tsx index 0f4e8558ca2..5c12a7b371d 100644 --- a/src/components/scenes/Staking/StakeOptionsScene.tsx +++ b/src/components/scenes/Staking/StakeOptionsScene.tsx @@ -30,7 +30,6 @@ import { SceneContainer } from '../../layout/SceneContainer' import { Space } from '../../layout/Space' import { useTheme } from '../../services/ThemeContext' import { EdgeText } from '../../themed/EdgeText' -import { SceneHeaderUi4 } from '../../themed/SceneHeaderUi4' interface Props extends EdgeAppSceneProps<'stakeOptions'> { wallet: EdgeCurrencyWallet @@ -41,7 +40,7 @@ export interface StakeOptionsParams { walletId: string } -const StakeOptionsSceneComponent = (props: Props) => { +const StakeOptionsSceneComponent = (props: Props): React.JSX.Element => { const { navigation, route, wallet } = props const { tokenId } = route.params const [stakePlugins = []] = useAsyncValue( @@ -73,7 +72,7 @@ const StakeOptionsSceneComponent = (props: Props) => { // Handlers // - const handleStakeOptionPress = (stakePolicy: StakePolicy) => { + const handleStakeOptionPress = (stakePolicy: StakePolicy): void => { const { stakePolicyId } = stakePolicy const stakePlugin = getPluginFromPolicyId(stakePlugins, stakePolicyId, { pluginId @@ -90,7 +89,11 @@ const StakeOptionsSceneComponent = (props: Props) => { // Renders // - const renderOptions = ({ item: stakePolicy }: { item: StakePolicy }) => { + const renderOptions = ({ + item: stakePolicy + }: { + item: StakePolicy + }): React.JSX.Element => { const primaryText = getPolicyAssetName(stakePolicy, 'stakeAssets') const secondaryText = getPolicyTitleName(stakePolicy, countryCode) const key = [primaryText, secondaryText].join() @@ -143,27 +146,21 @@ const StakeOptionsSceneComponent = (props: Props) => { overrideDots={theme.backgroundDots.assetOverrideDots} > {({ undoInsetStyle, insetStyle }) => ( - + - {/* TODO: Decide if our design language accepts scene headers within - the scroll area of a scene. If so, we must make the SceneContainer - component implement FlatList components. This is a one-off - until then. */} - - - {lstrings.stake_select_options} - - + + {lstrings.stake_select_options} + } keyExtractor={(stakePolicy: StakePolicy) => stakePolicy.stakePolicyId diff --git a/src/components/scenes/SwapCreateScene.tsx b/src/components/scenes/SwapCreateScene.tsx index 13130513eeb..f42ec8d90b1 100644 --- a/src/components/scenes/SwapCreateScene.tsx +++ b/src/components/scenes/SwapCreateScene.tsx @@ -535,7 +535,7 @@ export const SwapCreateScene: React.FC = props => { }} > {({ isKeyboardOpen }) => ( - + {fromWallet == null ? (