Skip to content

fix: hydrate wallet state from SecureStore on cold start (fixes #34) - #58

Open
mac-dubem wants to merge 1 commit into
Orbit-Wal:mainfrom
mac-dubem:fix/onboarding-hydration-34
Open

fix: hydrate wallet state from SecureStore on cold start (fixes #34)#58
mac-dubem wants to merge 1 commit into
Orbit-Wal:mainfrom
mac-dubem:fix/onboarding-hydration-34

Conversation

@mac-dubem

@mac-dubem mac-dubem commented Jul 29, 2026

Copy link
Copy Markdown

Root cause

useWalletStore is a plain zustand store with no persistence middleware — isOnboarded defaults to false on every cold start. app/index.tsx reads that in-memory flag synchronously to decide the initial route, so even when secureStorage.ts holds a valid secret from a previous session, the app always redirects to /auth/welcome.

Design decision

Instead of adding persistence middleware (which would either store the secret in plaintext via AsyncStorage or require a serialization layer), the fix adds an explicit hydrate() action that runs once on mount:

  1. Read the secret from SecureStore (hardware-backed, encrypted-at-rest)
  2. If it exists, derive the publicKey via StellarSdk.Keypair.fromSecret() and set isOnboarded: true
  3. If it doesn't exist, mark hydration as complete with defaults

The secret never enters the zustand store — only the derived public key and the boolean flag are stored in memory. Plaintext persistence outside SecureStore is avoided entirely.

Definition of done

  • Check SecureStore on cold start: walletStore.hydrate() reads from SecureStore via getSecretKey() and restores state before routing
  • Restore publicKey/isOnboarded accordingly: When a secret is found, publicKey is derived and isOnboarded set to true; otherwise defaults remain
  • No plaintext persistence outside SecureStore: Only publicKey (not the secret) is stored in the zustand store; the secret stays in SecureStore
  • Regression test: src/store/__tests__/walletStore.test.ts covers both paths — secret present (onboarded restored) and secret absent (stays not onboarded)

Evidence

All 16 tests pass (4 suites):

PASS src/store/__tests__/walletStore.test.ts
PASS src/hooks/useScreenCaptureProtection.test.ts
PASS src/store/__tests__/guardianStore.test.ts
PASS src/services/__tests__/guardianRecovery.test.ts

Test Suites: 4 passed, 4 total
Tests:       16 passed, 16 total

Adjacent behavior verified

  • Create wallet flow (app/auth/create.tsx): unchanged — still saves secret to SecureStore and calls setOnboarded(true) / setPublicKey() directly
  • Import wallet flow (app/auth/import.tsx): unchanged — same pattern as create
  • Guardian store hydration pattern is preserved and independent
  • Routes (/auth/welcome, /tabs/home) unchanged; only the decision point in app/index.tsx now waits for hydration

Closes #34

…-Wal#34)

- Add hydrate() action to walletStore that reads secret from SecureStore
  and restores publicKey/isOnboarded before routing decisions
- Update app/index.tsx to await hydration before rendering redirect,
  showing a loading spinner during the async check
- No plaintext secret stored outside SecureStore — only the derived
  publicKey enters the zustand store
- Regression test: verifies cold start with pre-existing SecureStore
  entry restores onboarded state; verifies no secret leaves state as
  not onboarded
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.

isOnboarded state is in-memory only — cold app restart loses onboarding state despite the secret still existing in SecureStore

1 participant