Repository navigation
feat(onboarding): import a wallet from a recovery phrase or secret key - #1068
aristidesstaffieri wants to merge 7 commits into
Conversation
Port of stellar/freighter#3091. Import Wallet takes one input and works out whether it holds a 12 or 24-word recovery phrase or a Stellar secret key. The field keeps a secret key's case. A secret key goes through the new importWalletFromSecretKey store action, which stores one imported account with a secret-key source and no phrase. It checks the key before wiping anything and runs behind the account-mutation lock. If storing fails after the wipe, it wipes again so the user can retry. Set Password names the secret key in its acknowledgement, and both the Set Password and Biometrics steps can finish a secret-key import. Security hides "Show recovery phrase" for a wallet that holds only secret keys. The import is tracked as account_recovery.completed or account_recovery.failed with recovery_method "secret_key".
There was a problem hiding this comment.
🟡 Changes recommended
Critical import-path flaws can leak secret keys to analytics, alter authentication state before validation, and leave sessions or credentials behind after failed-import cleanup.
6 open issues
Clean up keychain entries when key indexing fails · New Use a fixed analytics reason to avoid leaking imported secrets · New Clear biometric credentials when post-wipe registration fails · New Await all storage writes before rollback · New Reset auto-lock only after validating the imported key · New Restore permissive recovery-phrase visibility when a focus read fails · New
What changed in this PR
Extends Freighter Mobile onboarding so users can import a wallet from either a recovery phrase or a Stellar secret key. The key-only flow builds on the existing account-source model and serialized account mutations.
Changes:
- Adds unified input validation and case-preserving secret-key entry.
- Routes password and biometric setup through the appropriate wallet-import action.
- Adjusts recovery-phrase settings for key-only wallets, with translations and unit tests.
| File | Description |
|---|---|
src/i18n/locales/pt/translations.json |
Adds Portuguese key-import copy and errors. |
src/i18n/locales/en/translations.json |
Adds English key-import copy and errors. |
src/hooks/useHasRecoveryPhrase.ts |
Reads phrase availability when settings gains focus. |
src/hooks/useFinishWalletSetup.ts |
Shares setup dispatch for phrase and key imports. |
src/helpers/recoveryPhrase.ts |
Adds case-preserving input normalization. |
src/helpers/parseImportSecret.ts |
Classifies and validates phrases and secret keys. |
src/helpers/hasRecoveryPhrase.ts |
Determines phrase availability from wallet sources. |
src/ducks/loginData.ts |
Holds the onboarding key in memory alongside existing login data. |
src/ducks/auth.ts |
Adds key-only wallet replacement, rollback and error handling. |
src/components/templates/ImportRecoveryPhraseTemplate.tsx |
Passes case-preservation support to the input field. |
src/components/screens/SettingsScreen/SecurityScreen/ShowRecoveryPhraseScreen/ShowRecoveryPhraseScreen.tsx |
Returns from phrase display for key-only wallets. |
src/components/screens/SettingsScreen/SecurityScreen/SecurityScreen.tsx |
Conditionally shows the recovery-phrase option. |
src/components/screens/ImportWalletScreen.tsx |
Accepts either secret type and validates before navigation. |
src/components/screens/ChoosePasswordScreen.tsx |
Supports key imports and their acknowledgement text. |
src/components/screens/BiometricsEnableScreen/BiometricsEnableScreen.tsx |
Handles key imports through Enable and Skip. |
src/components/RecoveryPhraseInput.tsx |
Preserves case during secret-key editing. |
src/components/RecoveryPhraseField.tsx |
Preserves case for pasted keys. |
__tests__/hooks/useFinishWalletSetup.test.tsx |
Tests key-import dispatch and login-data cleanup. |
__tests__/helpers/recoveryPhrase.test.ts |
Tests case-preserving normalization. |
__tests__/helpers/parseImportSecret.test.ts |
Tests phrase and key classification and rejection. |
__tests__/helpers/hasRecoveryPhrase.test.ts |
Tests phrase, key-only and empty source lists. |
__tests__/ducks/auth.test.ts |
Tests key onboarding, failure cleanup and secret reporting. |
__tests__/components/screens/ShowRecoveryPhraseScreen.test.tsx |
Tests the key-only navigation guard. |
__tests__/components/screens/SettingsScreen/SecurityScreen.test.tsx |
Tests recovery-phrase option visibility. |
__tests__/components/screens/ImportWalletScreen.test.tsx |
Tests unified input, validation, refocus and repeated taps. |
__tests__/components/screens/ChoosePasswordScreen.test.tsx |
Tests key-specific acknowledgement and setup routing. |
__tests__/components/screens/BiometricsEnableScreen/BiometricsEnableScreen.test.tsx |
Tests key imports through biometric Enable and Skip. |
Review effort: Balanced
|
iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-d7dd3ed5a49778e0ae83 |
|
Android Emulator preview APK is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-829c893d38f2e7a04869 |
Port of the minimum importing time from stellar/freighter#3091 (835b23a8). runWithWalletSetupOverlay waits at least SETTING_UP_MIN_MS (500ms) before turning ready or fading out, so a secret-key import doesn't flash "Importing your wallet...". The Biometrics screen tests run on fake timers so the wait stays inside each test.
storeKeys removes a key it just wrote if adding it to the index fails, then rethrows the index error. clearAllData also removes every stellarwallet_* keychain entry after the index-based removal, so an entry the index doesn't list can't survive a wipe.
importWalletFromSecretKey and importSecretKey send a fixed reason_code to analytics instead of the scrubbed error text, which can carry the key in a form scrubStrKeys misses (key_S…). Adds the missing trackAccountScreenImportAccountFail to the analytics mock in jest.setup.js.
…lback storeAccount waits for every write it started before it throws, using a new settleAll helper, so nothing lands after a rollback. Failed sign-up, phrase import and key import share rollBackWalletSetup, which also removes BIOMETRIC_PASSWORD. The Biometrics screen saves the password again before each Enable attempt so a retry still works.
signUp, importWallet and importWalletFromSecretKey reset auto-lock themselves, just before the pre-store wipe, instead of in their store actions before validation. An invalid secret key now leaves the timer, the background timestamp and the LOCKED marker as they were. The reset still runs under the account-mutation lock and before the new hash key.
useHasRecoveryPhrase falls back to true when reading the secret sources fails, as its doc says, so a stale false from an earlier key-only read can't hide "Show recovery phrase" after a phrase is added.


What
Ports stellar/freighter#3091 to mobile. Onboarding's Import Wallet takes one input and works out whether it holds a 12 or 24-word recovery phrase or a Stellar secret key before asking for a password. A secret key onboards the wallet with one imported account under its own secret-key source and no phrase. The phrase path keeps its store action and its order.
The new
importWalletFromSecretKeystore action checks the key before wiping anything, so an invalid key leaves the device as it was. It then wipes, stores the account and registers the source, all under the account-mutation lock from #1066. If the store or the source registration fails after the wipe, it wipes again and leaves a fresh install the user can retry from. Nothing about a failure is logged or tracked with the error text, since that could carry the key.Suggested reading order:
importWalletFromSecretKeyinducks/auth.ts, thenImportWalletScreenandrunWalletSetupinhooks/useFinishWalletSetup.ts.Auth
importWalletFromSecretKey(ducks/auth.ts, new): validates the key and derives its source id, then wipes and stores under the lock. Tracksaccount_recovery.completedandaccount_recovery.failedwithrecovery_method: "secret_key", then links the source.authStore.error.invalidSecretKey(new) is on the user-facing error allowlist.useLoginDataStore(ducks/loginData.ts): holds an onboardingsecretKeybesidemnemonicPhrase. Setting either one clears the other, so setup never has to choose.Onboarding
ImportWalletScreen: Next classifies the input withparseImportSecret(helpers/parseImportSecret.ts, new) and keeps the phrase or key in memory for Set Password. Invalid input shows "Enter a valid recovery phrase or secret key" under the field and leaves the keyboard up.RecoveryPhraseFieldandRecoveryPhraseInput: newpreserveCaseoption. The field lowercased every keystroke and paste, which breaks a secret key. Only Import Wallet turns it on, so the other phrase screens are unchanged.ChoosePasswordScreen: accepts a held key as well as a phrase, and the acknowledgement names the secret key when that is what's being imported.runWalletSetupandhasWalletSetupSecret(hooks/useFinishWalletSetup.ts, new): pick sign-up, phrase import or key import.useFinishWalletSetupandBiometricsEnableScreenboth go through them, so Enable and Skip handle a key.Settings
SecurityScreenhides Show recovery phrase when the wallet's sources hold no phrase (useHasRecoveryPhrase,helpers/hasRecoveryPhrase.ts, new). An empty or unreadable source list keeps the row.ShowRecoveryPhraseScreengoes back on mount for a wallet with no phrase.Not ported from #3091: the
recovery-lifecyclehelper extraction, since mobile's recovery paths already shareclearAllData, and the extension's "Importing your wallet" and "Your wallet is ready!" screens, which mobile's wallet setup overlay already covers.Why
Onboarding could only import a recovery phrase, and Import Secret Key under Add wallet needs an existing wallet, so someone with only a secret key had no way to set up the app. The designs fold both imports into one screen, as in the extension.
Known limitations
ImportWallet.yamluses the same test IDs.Screenshots / video
[TODO: Import Wallet with a masked key and with the error, Set Password with the secret-key acknowledgement, Security without Show recovery phrase]
Test plan
auth.test.tscovers a key import replacing the wallet with one imported account, a secret-key source and pin, and no phrase in the session. It also covers an invalid key leaving storage untouched, a failed key store or source registration leaving a fresh install, and the key never reaching logs or analytics.parseImportSecret.test.tscovers keys (trimmed, case kept, a lowercased key and a bad checksum refused, a public key refused) and 12 and 24-word phrases with case and spacing normalized.hasRecoveryPhrase.test.tsandrecoveryPhrase.test.tscover the new helpers.useFinishWalletSetupimporting a key, Security hiding the row, and Show recovery phrase going back.yarn test(5591 passed, 14 skipped) andtsc --noEmitpass.Checklist
PR structure
Testing
Release