diff --git a/CHANGELOG.md b/CHANGELOG.md
index 308691258ec..aa4a9d32c6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,12 @@
## Unreleased (develop)
+- added: Zcash buy/sell support with Banxa
+
## 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/eslint.config.mjs b/eslint.config.mjs
index 8295493276d..ad9c0204ab9 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()'
+ }
+ ]
}
},
@@ -81,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',
@@ -241,7 +249,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',
@@ -288,10 +296,9 @@ 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',
'src/components/scenes/SpendingLimitsScene.tsx',
'src/components/scenes/Staking/EarnScene.tsx',
'src/components/scenes/Staking/StakeOptionsScene.tsx',
@@ -308,7 +315,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',
@@ -467,7 +474,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',
@@ -484,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',
@@ -505,7 +510,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/ios/Podfile.lock b/ios/Podfile.lock
index ce646ad0e81..50af5b14ff2 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -15,13 +15,13 @@ PODS:
- disklet (0.5.2):
- React
- DoubleConversion (1.1.6)
- - edge-core-js (2.37.0):
+ - edge-core-js (2.38.2):
- React-Core
- - edge-currency-accountbased (4.67.0):
+ - 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.1):
+ - edge-exchange-plugins (2.40.2):
- React-Core
- edge-login-ui-rn (3.34.6):
- React-Core
@@ -3333,10 +3333,10 @@ SPEC CHECKSUMS:
CNIOWindows: 3047f2d8165848a3936a0a755fee27c6b5ee479b
disklet: 8a20bf8a568635b6e6bb8f93297dac13ee5cef98
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
- edge-core-js: 248f7d28942a5ea6c9835eca6f9f16969c89476c
- edge-currency-accountbased: 993920e46f000e04df92d0a49eabb57973096d1c
- edge-currency-plugins: 0d8a1a8da63672342cbc9bd5055feb4b397544e7
- edge-exchange-plugins: b92baace286dd8ed8a7a6672e1d0172f04a91357
+ edge-core-js: 8b015465c8462879816149c8a5896a854d53e971
+ edge-currency-accountbased: b526ee12efefad410125c51135222b0c63e42f12
+ edge-currency-plugins: 6b3341707a6a5c74f837a012768dd2f6c55a691b
+ edge-exchange-plugins: f35930ddcd5a4551a6e45334cb3f4c0295c23acd
edge-login-ui-rn: c9648a772533c092f4526a189cd4da9d6f729639
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
Expo: 43d9e0c3108cc3a1c2739743e9b51086144ee4b0
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/01-accounts/C000006-light-account.yaml b/maestro/01-accounts/C000006-light-account.yaml
index e06b4697561..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:
@@ -136,20 +148,27 @@ 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
+
+ # 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:
@@ -187,7 +206,9 @@ tags:
- tapOn: Get Started
- waitForAnimationToEnd:
timeout: 10000
- - assertVisible: Choose Username
+ - extendedWaitUntil:
+ visible: Choose Username
+ timeout: 10000
- assertVisible: Next
- tapOn:
id: "headerLeftButton"
@@ -213,6 +234,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:
@@ -233,7 +256,9 @@ tags:
optional: true
- waitForAnimationToEnd:
timeout: 5000
- - assertVisible: Choose Username
+ - extendedWaitUntil:
+ visible: Choose Username
+ timeout: 10000
- assertVisible: Next
# Complete backup of account
@@ -336,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
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
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/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
diff --git a/maestro/07-wallets/C000033-pause-wallets.yaml b/maestro/07-wallets/C000033-pause-wallets.yaml
new file mode 100644
index 00000000000..3f2ba44b4ee
--- /dev/null
+++ b/maestro/07-wallets/C000033-pause-wallets.yaml
@@ -0,0 +1,102 @@
+# 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"
+# 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
+# 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
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/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"
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
diff --git a/maestro/07-wallets/C000037-split-wallets.yaml b/maestro/07-wallets/C000037-split-wallets.yaml
new file mode 100644
index 00000000000..afe956fe4b2
--- /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: 60000
+ optional: true
+ label: "⏰ Waiting 60 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
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/07-wallets/C000044-create-all-wallet-types.yaml b/maestro/07-wallets/C000044-create-all-wallet-types.yaml
new file mode 100644
index 00000000000..95cb1e13473
--- /dev/null
+++ b/maestro/07-wallets/C000044-create-all-wallet-types.yaml
@@ -0,0 +1,65 @@
+# 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
+ env:
+ NEW_WALLETS: ${'["Bitcoin"]'}
+- 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: 5
+
+ - evalScript: ${index++}
\ No newline at end of file
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..1c863e05cae
--- /dev/null
+++ b/maestro/07-wallets/C000045-add-edit-tokens.yaml
@@ -0,0 +1,182 @@
+
+## 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:
+ 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: ETH
+ direction: DOWN
+ centerElement: true
+- longPressOn: ETH
+- 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
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
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
+
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..f31dcbf205b
--- /dev/null
+++ b/maestro/07-wallets/C000048-split-all-evm.yaml
@@ -0,0 +1,111 @@
+# 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 by appearance)
+- 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:
+ # # 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: 60000
+
+- runFlow:
+ file: ../common/no-errors.yaml
+
+
+- stopApp
+
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
diff --git a/maestro/12-notifications/C000015-ip-validation-reminder.yaml b/maestro/12-notifications/C000015-ip-validation-reminder.yaml
index 41731c5face..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:
@@ -20,6 +22,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
@@ -36,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
@@ -50,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/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/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
diff --git a/maestro/common/create-account.yaml b/maestro/common/create-account.yaml
index 83bdd59afe4..d3fbb6b4032 100644
--- a/maestro/common/create-account.yaml
+++ b/maestro/common/create-account.yaml
@@ -9,20 +9,45 @@ 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 || ""}
+ 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"
@@ -30,6 +55,8 @@ env:
- waitForAnimationToEnd:
timeout: 10000
- assertVisible: Username available
+
+# Continue
- assertVisible: Next
- pressKey:
key: Enter
@@ -47,11 +74,13 @@ env:
- pressKey:
key: Enter
- inputText: ${NEW_ACCOUNT_PASSWORD}
+- evalScript: ${output.newAccountPassword = NEW_ACCOUNT_PASSWORD}
- pressKey:
key: Enter
# Set PIN
# Using UTXO creds for now
+- assertVisible: Set PIN
- inputText: ${MAESTRO_EDGE_NEW_ACCOUNT_PIN}
- tapOn: Next
@@ -60,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}
@@ -87,22 +125,36 @@ 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 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: ${filteredDefaults.length}
+ commands:
+ - tapOn: ${".*" + filteredDefaults[defIndex]}
+ - evalScript: ${defIndex++}
+ # Add new wallets
+ - evalScript: ${var newIndex = 0}
+ - repeat:
+ times: ${filteredWallets.length}
+ commands:
+ - tapOn: Search Wallets
+ - tapOn:
+ id: "undefined.clearIcon"
+ - inputText: ${filteredWallets[newIndex]}
+ - tapOn:
+ text: ${".*" + filteredWallets[newIndex]}
+ index: 1
+ - evalScript: ${newIndex++}
+
- 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/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:
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:
diff --git a/maestro/common/import-wallets.yaml b/maestro/common/import-wallets.yaml
new file mode 100644
index 00000000000..0d6025a3f9a
--- /dev/null
+++ b/maestro/common/import-wallets.yaml
@@ -0,0 +1,101 @@
+# 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: "Enter your.*"
+# Drop keyboard - iOS
+- runFlow:
+ when:
+ platform: iOS
+ commands:
+ - tapOn: "Private Key or Private Seed"
+
+- 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}
+# 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
+# skip if NO_COMPLETE is true (for autodetect tokens flow test)
+- repeat:
+ while:
+ visible: Next
+ true: ${!NO_COMPLETE}
+ commands:
+ - tapOn:
+ text: Next
+ optional: true
\ 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
diff --git a/maestro/common/relogin-pin.yaml b/maestro/common/relogin-pin.yaml
index c6ea72e3ff9..5906626856a 100644
--- a/maestro/common/relogin-pin.yaml
+++ b/maestro/common/relogin-pin.yaml
@@ -6,10 +6,10 @@ 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
+- extendedWaitUntil:
+ visible: Exit PIN
+ timeout: 35000
- tapOn: ${PIN_RELOGIN}
- tapOn: ${PIN_RELOGIN}
- tapOn: ${PIN_RELOGIN}
diff --git a/package.json b/package.json
index c02dbfe3a9f..89d0ad2dfd5 100644
--- a/package.json
+++ b/package.json
@@ -105,11 +105,11 @@
"deprecated-react-native-prop-types": "^5.0.0",
"detect-bundler": "^1.1.0",
"disklet": "^0.5.2",
- "edge-core-js": "^2.37.0",
- "edge-currency-accountbased": "^4.67.0",
+ "edge-core-js": "^2.38.2",
+ "edge-currency-accountbased": "^4.68.0",
"edge-currency-monero": "^2.0.1",
- "edge-currency-plugins": "^3.8.9",
- "edge-exchange-plugins": "^2.40.1",
+ "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",
"ethers": "^5.7.2",
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',
diff --git a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap
index 3caf6936468..c159991a518 100644
--- a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap
+++ b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap
@@ -113,10 +113,9 @@ exports[`initialState 1`] = `
"defaultFiat": "USD",
"defaultIsoFiat": "iso:USD",
"denominationSettings": {},
+ "denominationSettingsOptimized": false,
"developerModeOn": false,
"isAccountBalanceVisible": true,
- "isTouchEnabled": false,
- "isTouchSupported": false,
"mostRecentWallets": [],
"notifState": {},
"passwordRecoveryRemindersShown": {
@@ -135,7 +134,6 @@ exports[`initialState 1`] = `
"nonPasswordLoginsLimit": 4,
"passwordUseCount": 0,
},
- "pinLoginEnabled": false,
"preferredSwapPluginId": undefined,
"preferredSwapPluginType": undefined,
"rampLastCryptoSelection": undefined,
diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap
index 071b593a98c..754a6244f7a 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/__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/__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/__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/actions/LocalSettingsActions.ts b/src/actions/LocalSettingsActions.ts
index b7b9ac2e199..e8b0986ad91 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), [])
@@ -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
}
}
diff --git a/src/actions/LoginActions.tsx b/src/actions/LoginActions.tsx
index 033ea132aea..a2e1071dd10 100644
--- a/src/actions/LoginActions.tsx
+++ b/src/actions/LoginActions.tsx
@@ -1,12 +1,6 @@
-import type {
- EdgeAccount,
- EdgeCreateCurrencyWallet,
- EdgeTokenId
-} from 'edge-core-js/types'
+import type { EdgeAccount, EdgeCreateCurrencyWallet } from 'edge-core-js/types'
import {
- getSupportedBiometryType,
hasSecurityAlerts,
- isTouchEnabled,
refreshTouchId,
showNotificationPermissionReminder
} from 'edge-login-ui-rn'
@@ -16,7 +10,11 @@ import { getCurrencies } from 'react-native-localize'
import performance from 'react-native-performance'
import { sprintf } from 'sprintf-js'
-import { readSyncedSettings } from '../actions/SettingsActions'
+import {
+ migrateDenominationSettings,
+ readSyncedSettings,
+ type SyncedAccountSettings
+} from '../actions/SettingsActions'
import { ConfirmContinueModal } from '../components/modals/ConfirmContinueModal'
import { FioCreateHandleModal } from '../components/modals/FioCreateHandleModal'
import { SurveyModal } from '../components/modals/SurveyModal'
@@ -24,13 +22,9 @@ 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, 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'
@@ -50,148 +44,56 @@ import {
registerNotificationsV2,
updateNotificationSettings
} from './NotificationActions'
-import { showScamWarningModal } from './ScamWarningActions'
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
): ThunkAction> {
return async (dispatch, getState) => {
- 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
-
- 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[]
- ) => {
- navigation.replace('edgeTabs', { screen: 'home' })
- const createWalletsPromise = createCustomWallets(
- account,
- fiatCurrencyCode,
- items,
- dispatch
- ).catch(error => {
- 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
- })
- }
- }
+ const rootNavigation = getRootNavigation(navigation)
- await createWalletsPromise
- dispatch(
- logEvent('Signup_Complete', {
- numAccounts: getState().core.context.localUsers.length
- })
- )
+ // Load all settings upfront so we can navigate immediately after LOGIN
+ const [syncedSettings, localSettings] = await Promise.all([
+ readSyncedSettings(account),
+ readLocalAccountSettings(account)
+ ])
+
+ // Dispatch LOGIN with all settings - this enables immediate navigation
+ dispatch({
+ type: 'LOGIN',
+ data: {
+ account,
+ syncedSettings,
+ localSettings
}
+ })
- rootNavigation.replace('edgeApp', {
- screen: 'edgeAppStack',
- params: {
- screen: 'createWalletSelectCryptoNewAccount',
- params: {
- newAccountFlow,
- defaultSelection,
- disableLegacy: true
- }
- }
- })
+ const referralPromise = dispatch(loadAccountReferral(account))
- performance.mark('loginEnd', { detail: { isNewAccount: newAccount } })
+ // Navigate immediately - all settings are now in Redux
+ if (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 } })
+
+ // 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) {
@@ -231,118 +133,59 @@ export function initializeAccount(
})
}
})
- .catch(err => {
- showError(err)
+ .catch((error: unknown) => {
+ showError(error)
})
}
- // Show the scam warning modal if needed
- if (await showScamWarningModal('firstLogin')) hideSurvey = true
-
// Check for security alerts:
if (hasSecurityAlerts(account)) {
navigation.push('securityAlerts')
hideSurvey = true
}
- const state = getState()
- 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
const filteredWalletInfos = walletInfos.map(({ keys, id, ...info }) => info)
console.log('Wallet Infos:', filteredWalletInfos)
- // Merge and prepare settings files:
- let accountInitObject: AccountInitPayload = {
- ...initialState,
- account,
- tokenId: null,
- pinLoginEnabled: false,
- isTouchEnabled: await isTouchEnabled(account),
- isTouchSupported: (await getSupportedBiometryType()) !== false,
- walletId: '',
- walletsSort: 'manual'
- }
- try {
- if (!newAccount) {
- // We have a wallet
- const { walletId, tokenId } = getFirstActiveWalletInfo(account)
- accountInitObject.walletId = walletId
- accountInitObject.tokenId = tokenId
+ // 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)
}
+ )
- accountInitObject = { ...accountInitObject, ...syncedSettings }
-
- const loadedLocalSettings = await readLocalAccountSettings(account)
- accountInitObject = { ...accountInitObject, ...loadedLocalSettings }
+ await dispatch(refreshAccountReferral())
- for (const userInfo of context.localUsers) {
- if (
- userInfo.loginId === account.rootLoginId &&
- userInfo.pinLoginEnabled
- ) {
- accountInitObject.pinLoginEnabled = true
- }
- }
+ refreshTouchId(account).catch(() => {
+ // We have always failed silently here
+ })
- const defaultDenominationSettings = state.ui.settings.denominationSettings
- const syncedDenominationSettings =
- syncedSettings?.denominationSettings ?? {}
- const mergedDenominationSettings = {}
-
- 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] ?? {})
- }
+ 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)
+ }
+ )
}
- }
- accountInitObject.denominationSettings = { ...mergedDenominationSettings }
-
- dispatch({
- type: 'ACCOUNT_INIT_COMPLETE',
- data: { ...accountInitObject }
})
-
- 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 => {
- trackError(error, 'LoginScene:onLogin:setDeviceSettings')
- console.error(error)
- }
- )
- }
- })
- ) {
- hideSurvey = true
- }
- } catch (error: any) {
- showError(error)
+ ) {
+ hideSurvey = true
}
- // Post login stuff:
+ // Post login stuff: Survey modal (existing accounts only)
if (
!newAccount &&
!hideSurvey &&
@@ -358,6 +201,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()
@@ -434,7 +388,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/actions/SettingsActions.tsx b/src/actions/SettingsActions.tsx
index 21f4ae9425c..588abcf9d41 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,59 +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> {
- 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 {
@@ -438,6 +384,8 @@ export const asSyncedAccountSettings = asObject({
asDenominationSettings,
() => ({})
),
+ // Flag to track one-time denomination settings cleanup migration
+ denominationSettingsOptimized: asMaybe(asBoolean, false),
securityCheckedWallets: asMaybe(
asSecurityCheckedWallets,
() => ({})
@@ -565,10 +513,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
}
}
@@ -596,3 +543,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')
+ }
+}
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/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 (
()
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/CreateWalletSelectCryptoScene.tsx b/src/components/scenes/CreateWalletSelectCryptoScene.tsx
index b7a8b1356d3..d875034fc3e 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}
)}
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 => {
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,52 +176,111 @@ export const GettingStartedScene: React.FC = props => {
}
})
- const footerButtons = (
- <>
-
-
-
-
-
-
-
-
-
-
-
- {lstrings.getting_started_already_have_an_account}
- {lstrings.getting_started_sign_in}
-
-
- >
+ // ---------------------------------------------------------------------------
+ // 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 => {
-
{parseMarkedText(lstrings.getting_started_welcome_title)}
-
+
-
+
{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)
+ scrollIndex={scrollIndex}
+ />
+ ))}
+
+
+
+
-
-
- ))}
-
-
-
- {sections.map((section, index) => {
- return (
-
-
-
- {parseMarkedText(section.title)}
-
-
- {section.message}
-
- {section.footnote == null ? null : (
-
- {lstrings.getting_started_slide_1_footnote}
-
- )}
-
-
- )
- })}
-
- {footerButtons}
-
-
+
+
+
+
+
+
+
+
+
+
+ {lstrings.getting_started_already_have_an_account}
+
+ {lstrings.getting_started_sign_in}
+
+
+
+
+
)
}
// -----------------------------------------------------------------------------
-// Local Components
+// Styles
// -----------------------------------------------------------------------------
-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,
+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 TertiaryText = styled(EdgeText)(theme => props => ({
- color: theme.textInputTextColorDisabled
}))
-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 [
- {
- 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 => {
- // 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'
- }
-})
+ return (
+
+
+
+ {parseMarkedText(section.title)}
+
+
+ {section.message}
+
+ {section.footnote == null ? null : (
+
+ {section.footnote}
+
+ )}
+
+
+ )
+}
diff --git a/src/components/scenes/RampPendingScene.tsx b/src/components/scenes/RampPendingScene.tsx
index 179e525d984..1afc076ecab 100644
--- a/src/components/scenes/RampPendingScene.tsx
+++ b/src/components/scenes/RampPendingScene.tsx
@@ -108,7 +108,7 @@ export const RampPendingScene: React.FC = props => {
return (
-
+
{error != null ? (
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/SettingsScene.tsx b/src/components/scenes/SettingsScene.tsx
index d5c9339d762..9c597e6f861 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'
@@ -22,9 +28,7 @@ import { logoutRequest } from '../../actions/LoginActions'
import {
setAutoLogoutTimeInSecondsRequest,
showReEnableOtpModal,
- showUnlockSettingsModal,
- togglePinLoginEnabled,
- updateTouchIdEnabled
+ showUnlockSettingsModal
} from '../../actions/SettingsActions'
import { ENV } from '../../env'
import { useAsyncEffect } from '../../hooks/useAsyncEffect'
@@ -57,7 +61,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()
@@ -70,16 +74,40 @@ export const SettingsScene = (props: 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 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 =
@@ -89,6 +117,15 @@ export const SettingsScene = (props: 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(
@@ -135,10 +172,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 +185,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({
@@ -161,7 +202,20 @@ export const SettingsScene = (props: 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 () => {
@@ -173,7 +227,15 @@ export const SettingsScene = (props: 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 () => {
@@ -289,8 +351,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 +403,7 @@ export const SettingsScene = (props: Props) => {
defaultLogLevel: newDefaultLogLevel,
sources: {}
})
- .catch(error => {
+ .catch((error: unknown) => {
showError(error)
})
})
@@ -355,58 +417,51 @@ export const SettingsScene = (props: Props) => {
await writeForceLightAccountCreate(!forceLightAccountCreate)
})
- const loadBiometryType = async () => {
+ 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)
}
- }
+ }, [biometricState])
- useAsyncEffect(
- async () => {
- const currentContactsPermission = await check(permissionNames.contacts)
- setLocalContactsPermissionOn(currentContactsPermission === 'granted')
- },
- [],
- 'SettingsScene'
- )
-
- // Load biometry type on mount
+ // Watch for logSettings changes
React.useEffect(() => {
- if (!supportsTouchId) return
-
- loadBiometryType().catch(error => {
- 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) cleanup()
+ if (cleanup != null) cleanup()
}
- }, [context, supportsTouchId])
+ }, [context])
// Show a modal if we have a pending OTP resent when we enter the scene:
React.useEffect(() => {
return navigation.addListener('focus', () => {
if (account.otpResetDate != null) {
- showReEnableOtpModal(account).catch(error => {
+ showReEnableOtpModal(account).catch((error: unknown) => {
showError(error)
})
}
@@ -512,22 +567,22 @@ export const SettingsScene = (props: Props) => {
onPress={handleDefaultFiat}
/>
- {isLightAccount ? null : (
+ {!isLightAccount ? (
- )}
- {supportsTouchId && !isLightAccount && (
+ ) : null}
+ {supportsTouchId && !isLightAccount && touchIdEnabled != null ? (
- )}
+ ) : null}
{
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 ? (
{}
-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)
})
})
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 {
diff --git a/src/components/services/SortedWalletList.ts b/src/components/services/SortedWalletList.ts
index 331bb1b073f..e0a49639d62 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,14 @@ 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+/)
+ .map(term => normalizeForSearch(term))
+ .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 +286,38 @@ export function searchWalletList(
// Grab wallet and token information:
const { currencyCode, displayName } = token ?? wallet.currencyInfo
- const name = getWalletName(wallet)
-
- const contractAddress = token?.networkLocation?.contractAddress ?? ''
+ const { assetDisplayName, chainDisplayName } = wallet.currencyInfo
+
+ // 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 ?? ''
+ )
- return (
- normalizeForSearch(currencyCode).includes(target) ||
- normalizeForSearch(displayName).includes(target) ||
- normalizeForSearch(name).includes(target) ||
- normalizeForSearch(contractAddress).includes(target)
+ // 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 =>
+ // 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/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}
+
+ )}
)
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,
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/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",
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/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",
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",
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",
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",
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",
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",
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",
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",
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",
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",
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",
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({
diff --git a/src/plugins/ramps/paybis/paybisRampPlugin.ts b/src/plugins/ramps/paybis/paybisRampPlugin.ts
index 562d3d007ff..743dc966860 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
@@ -1215,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
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)
diff --git a/src/reducers/PasswordReminderReducer.ts b/src/reducers/PasswordReminderReducer.ts
index 30e82a43c6c..88b592269f1 100644
--- a/src/reducers/PasswordReminderReducer.ts
+++ b/src/reducers/PasswordReminderReducer.ts
@@ -200,30 +200,22 @@ export const untranslatedReducer: Reducer<
}
function translateAction(action: Action): PasswordReminderReducerAction {
- if (
- action.type === 'ACCOUNT_INIT_COMPLETE' &&
- action.data.account.newAccount
- ) {
+ if (action.type === 'LOGIN' && action.data.account.newAccount) {
const now = Date.now()
return {
type: 'NEW_ACCOUNT_LOGIN',
data: {
- lastLoginDate: now,
- // @ts-expect-error
- lastPasswordUseDate: now
+ lastLoginDate: now
}
}
}
- if (
- action.type === 'ACCOUNT_INIT_COMPLETE' &&
- action.data.account.passwordLogin
- ) {
+ if (action.type === 'LOGIN' && action.data.account.passwordLogin) {
const now = Date.now()
return {
type: 'PASSWORD_LOGIN',
data: {
- ...action.data.passwordReminder,
+ ...action.data.localSettings.passwordReminder,
lastLoginDate: now,
lastPasswordUseDate: now
}
@@ -231,7 +223,7 @@ function translateAction(action: Action): PasswordReminderReducerAction {
}
if (
- action.type === 'ACCOUNT_INIT_COMPLETE' &&
+ action.type === 'LOGIN' &&
!action.data.account.passwordLogin &&
!action.data.account.newAccount &&
action.data.account.username != null
@@ -239,7 +231,7 @@ function translateAction(action: Action): PasswordReminderReducerAction {
return {
type: 'NON_PASSWORD_LOGIN',
data: {
- ...action.data.passwordReminder,
+ ...action.data.localSettings.passwordReminder,
lastLoginDate: Date.now()
}
}
diff --git a/src/reducers/SpendingLimitsReducer.ts b/src/reducers/SpendingLimitsReducer.ts
index d65fff7ef12..af413565685 100644
--- a/src/reducers/SpendingLimitsReducer.ts
+++ b/src/reducers/SpendingLimitsReducer.ts
@@ -13,9 +13,11 @@ export const initialState: SpendingLimits = {
export const isEnabled = (
state: boolean = initialState.transaction.isEnabled,
action: Action
-) => {
+): 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 3df018553ae..f75ea3af39d 100644
--- a/src/reducers/scenes/SettingsReducer.ts
+++ b/src/reducers/scenes/SettingsReducer.ts
@@ -1,10 +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,
@@ -16,9 +16,6 @@ export const initialState: SettingsState = {
...asSyncedAccountSettings({}),
...asLocalAccountSettings({}),
changesLocked: true,
- isTouchEnabled: false,
- isTouchSupported: false,
- pinLoginEnabled: false,
settingsLoaded: null,
userPausedWalletsSet: null
}
@@ -27,9 +24,6 @@ export interface SettingsState
extends LocalAccountSettings,
SyncedAccountSettings {
changesLocked: boolean
- isTouchEnabled: boolean
- isTouchSupported: boolean
- pinLoginEnabled: boolean
settingsLoaded: boolean | null
// A copy of `userPausedWallets`, but as a set.
@@ -37,14 +31,10 @@ export interface SettingsState
userPausedWalletsSet: Set | null
}
-export interface AccountInitPayload extends SettingsState {
+export interface LoginPayload {
account: EdgeAccount
- tokenId: EdgeTokenId
- pinLoginEnabled: boolean
- isTouchEnabled: boolean
- isTouchSupported: boolean
- walletId: string
- walletsSort: SortOption
+ syncedSettings: SyncedAccountSettings
+ localSettings: LocalAccountSettings
}
export const settingsLegacy = (
@@ -53,54 +43,32 @@ 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
- }
-
- case 'ACCOUNT_INIT_COMPLETE': {
+ const { syncedSettings, localSettings } = action.data
const {
autoLogoutTimeInSeconds,
- contactsPermissionShown,
countryCode,
defaultFiat,
defaultIsoFiat,
denominationSettings,
- developerModeOn,
- isAccountBalanceVisible,
- isTouchEnabled,
- isTouchSupported,
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,
@@ -110,13 +78,10 @@ export const settingsLegacy = (
denominationSettings,
developerModeOn,
isAccountBalanceVisible,
- isTouchEnabled,
- isTouchSupported,
mostRecentWallets,
passwordRecoveryRemindersShown,
userPausedWallets,
userPausedWalletsSet: new Set(userPausedWallets),
- pinLoginEnabled,
preferredSwapPluginId:
preferredSwapPluginId === '' ? undefined : preferredSwapPluginId,
preferredSwapPluginType,
@@ -128,7 +93,6 @@ export const settingsLegacy = (
rampLastFiatCurrencyCode,
rampLastCryptoSelection
}
- return newState
}
case 'DEVELOPER_MODE_ON': {
return { ...state, developerModeOn: true }
@@ -143,23 +107,24 @@ 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
- 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
}
}
@@ -210,13 +175,6 @@ export const settingsLegacy = (
}
}
- case 'UI/SETTINGS/CHANGE_TOUCH_ID_SETTINGS': {
- return {
- ...state,
- isTouchEnabled: action.data.isTouchEnabled
- }
- }
-
case 'UI/SETTINGS/SET_MOST_RECENT_WALLETS': {
return {
...state,
diff --git a/src/selectors/getCreateWalletList.ts b/src/selectors/getCreateWalletList.ts
index a441776030d..59a60a967ec 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: {},
@@ -215,44 +219,69 @@ 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 {
+ assetDisplayName,
currencyCode,
displayName,
networkLocation = {},
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 => {
+ // Asset identification fields use startsWith to avoid partial matches
+ // (e.g., "eth" matches "Ethereum" but not "Tether")
if (
- typeof value === 'string' &&
- normalizeForSearch(value).includes(searchTarget)
+ normalizeForSearch(currencyCode).startsWith(term) ||
+ normalizeForSearch(displayName).startsWith(term)
) {
- out.push(item)
- break
+ return true
}
+ // Search assetDisplayName for mainnet create items (also uses startsWith)
+ if (
+ walletType != null &&
+ assetDisplayName != null &&
+ normalizeForSearch(assetDisplayName).startsWith(term)
+ ) {
+ return true
+ }
+ // Search pluginId for mainnet create items (uses includes for discovery)
+ if (walletType != null && normalizeForSearch(pluginId).includes(term)) {
+ return true
+ }
+ // Search networkLocation values ie. contractAddress (uses includes)
+ 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
diff --git a/src/types/reduxActions.ts b/src/types/reduxActions.ts
index d96b778d82b..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 }
@@ -109,10 +107,6 @@ export type Action =
type: 'UPDATE_FIO_WALLETS'
data: { fioWallets: EdgeCurrencyWallet[] }
}
- | {
- type: 'UI/SETTINGS/CHANGE_TOUCH_ID_SETTINGS'
- data: { isTouchEnabled: boolean }
- }
| {
type: 'UI/SETTINGS/SET_ACCOUNT_BALANCE_VISIBILITY'
data: { isAccountBalanceVisible: boolean }
@@ -153,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 } }
| {
diff --git a/src/util/fake/FakeProviders.tsx b/src/util/fake/FakeProviders.tsx
index ed15493363c..acd24a3850d 100644
--- a/src/util/fake/FakeProviders.tsx
+++ b/src/util/fake/FakeProviders.tsx
@@ -1,4 +1,5 @@
import { NavigationContext } from '@react-navigation/native'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import * as React from 'react'
import { type Metrics, SafeAreaProvider } from 'react-native-safe-area-context'
import { Provider } from 'react-redux'
@@ -9,6 +10,14 @@ import { rootReducer, type RootState } from '../../reducers/RootReducer'
import { renderStateProviders } from '../../state/renderStateProviders'
import { fakeNavigation } from './fakeSceneProps'
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false
+ }
+ }
+})
+
type DeepPartial = 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}
+
+ )}
+
+
)
}
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'
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index f6633e01b76..36c6d2e0fec 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==
@@ -9436,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.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"
@@ -9461,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"
@@ -9533,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"
@@ -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"