Skip to content

feat(onboarding): import a wallet from a recovery phrase or secret key - #1068

Open
aristidesstaffieri wants to merge 7 commits into
feature/import-additional-recovery-phrasefrom
feature/onboard-secret-key
Open

aristidesstaffieri wants to merge 7 commits into
feature/import-additional-recovery-phrasefrom
feature/onboard-secret-key

Conversation

@aristidesstaffieri

@aristidesstaffieri aristidesstaffieri commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

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 importWalletFromSecretKey store 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: importWalletFromSecretKey in ducks/auth.ts, then ImportWalletScreen and runWalletSetup in hooks/useFinishWalletSetup.ts.

Auth

  • importWalletFromSecretKey (ducks/auth.ts, new): validates the key and derives its source id, then wipes and stores under the lock. Tracks account_recovery.completed and account_recovery.failed with recovery_method: "secret_key", then links the source.
  • authStore.error.invalidSecretKey (new) is on the user-facing error allowlist.
  • useLoginDataStore (ducks/loginData.ts): holds an onboarding secretKey beside mnemonicPhrase. Setting either one clears the other, so setup never has to choose.

Onboarding

  • ImportWalletScreen: Next classifies the input with parseImportSecret (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.
  • RecoveryPhraseField and RecoveryPhraseInput: new preserveCase option. 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.
  • runWalletSetup and hasWalletSetupSecret (hooks/useFinishWalletSetup.ts, new): pick sign-up, phrase import or key import. useFinishWalletSetup and BiometricsEnableScreen both go through them, so Enable and Skip handle a key.

Settings

  • SecurityScreen hides 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.
  • ShowRecoveryPhraseScreen goes back on mount for a wallet with no phrase.
  • Strings added and updated in en and pt.

Not ported from #3091: the recovery-lifecycle helper extraction, since mobile's recovery paths already share clearAllData, 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

  • Stacked on feat(accounts): import an additional recovery phrase without wiping the wallet #1067. Retarget once that merges.
  • No Maestro flow for a secret-key import yet. It needs a new CI secret and clipboard setup in the e2e scripts. The existing ImportWallet.yaml uses the same test IDs.
  • Invalid input is now rejected on Next before the keyboard closes, without the spinner the old phrase check showed. The check no longer derives the seed, so it doesn't block the JS thread.
  • Not yet tested by hand on Android.

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]

Before After

Test plan

  • auth.test.ts covers 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.ts covers 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.ts and recoveryPhrase.test.ts cover the new helpers.
  • Screen tests cover Import Wallet routing a phrase and a key, the error on invalid input, starting over on refocus and a double tap. They also cover the Set Password acknowledgement, Biometrics and useFinishWalletSetup importing a key, Security hiding the row, and Show recovery phrase going back.
  • yarn test (5591 passed, 14 skipped) and tsc --noEmit pass.
  • Manual check on iOS: import a key, a phrase and invalid input, then lock, unlock and open Security.
  • Manual check on Android.
  • CI green.

Checklist

PR structure

  • This PR does not mix refactoring changes with feature changes (break it down into smaller PRs if not).
  • This PR has reasonably narrow scope (break it down into smaller PRs if not).
  • This PR includes relevant before and after screenshots/videos highlighting these changes.
  • I took the time to review my own PR.

Testing

  • These changes have been tested and confirmed to work as intended on Android.
  • These changes have been tested and confirmed to work as intended on iOS.
  • These changes have been tested and confirmed to work as intended on small iOS screens.
  • These changes have been tested and confirmed to work as intended on small Android screens.
  • I have tried to break these changes while extensively testing them.
  • This PR adds tests for the new functionality or fixes.

Release

  • This is not a breaking change.
  • This PR updates existing JSDocs when applicable.
  • This PR adds JSDocs to new functionalities.
  • I've checked with the product team if we should add metrics to these changes.
  • I've shared relevant before and after screenshots/videos highlighting these changes with the design team and they've approved the changes.

  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".

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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
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

Comment thread src/ducks/auth.ts Resolved
Comment thread src/ducks/auth.ts Resolved
Comment thread src/ducks/auth.ts Resolved
Comment thread src/ducks/auth.ts Resolved
Comment thread src/ducks/auth.ts Resolved
Comment thread src/hooks/useHasRecoveryPhrase.ts Resolved
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-d7dd3ed5a49778e0ae83
Backend: sandbox (aristidesstaffieri). SDF collaborators only — install instructions in the release description.

@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Android Emulator preview APK is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-829c893d38f2e7a04869
Backend: sandbox (aristidesstaffieri). SDF collaborators only — install instructions in the release description.

  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.
@aristidesstaffieri
aristidesstaffieri marked this pull request as ready for review November 13, 2026 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants