Skip to content

feat(onboarding): add passkey wallet restoration - #509

Closed
adeboladee wants to merge 5 commits into
Miracle656:mainfrom
adeboladee:feat/454-restore-wallet
Closed

feat(onboarding): add passkey wallet restoration#509
adeboladee wants to merge 5 commits into
Miracle656:mainfrom
adeboladee:feat/454-restore-wallet

Conversation

@adeboladee

Copy link
Copy Markdown

Resolves #454. Adds the restore.tsx screen to allow returning users to regain access by authenticating with an existing passkey, re-deriving their wallet contract address, and setting the session.

@adeboladee
adeboladee requested a review from Miracle656 as a code owner July 27, 2026 19:37
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

@openhands-agent is attempting to deploy a commit to the miracle656's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jul 27, 2026

Copy link
Copy Markdown

@adeboladee Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for taking #454 on. The screen structure in (onboarding)/restore.tsx is in the right place and the error states you handle (no passkey → no wallet → account mismatch) are the right three cases. Credit also for updating sdk/src/__tests__/__snapshots__/api-surface.test.ts.snap when you added the webAuthnProvider export — contributors miss that constantly.

That said, this can't run on a device as written. The issue says "Port/reference: frontend/wallet/lib/passkeyAuth.ts", and the port has carried the browser APIs across unchanged — but React Native has none of them.

🚨 Blockers

1. localStorage does not exist in React Native

app/lib/passkeyAuth.ts:8 and (onboarding)/restore.tsx:31:

const keyId = localStorage.getItem("invisible_wallet_key_id");

This throws ReferenceError: localStorage is not defined on both iOS and Android — the restore screen crashes the moment it's opened. A credential ID is a security-relevant identifier, so it should go in expo-secure-store rather than plain async storage:

import * as SecureStore from 'expo-secure-store';
const keyId = await SecureStore.getItemAsync('invisible_wallet_key_id');

Note this makes the call async, which changes the shape of requirePasskey and the restore handler.

2. crypto.getRandomValues is not available either

app/lib/passkeyAuth.ts:12:

const challengeBytes = crypto.getRandomValues(new Uint8Array(32));

There's no global crypto in the RN runtime. Use expo-crypto's getRandomBytes(32), or pull in react-native-get-random-values as a polyfill imported before any SDK code.

3. rpId: "localhost" is hardcoded

app/lib/passkeyAuth.ts:20:

rpId: "localhost",

The WebAuthn relying-party ID has to match the app's associated domain, or authentication fails against any real credential — and a passkey registered under the production RP ID will never verify against localhost. This needs to come from config, not a literal. #508 is currently landing the associated-domains / app-links setup; that's where the value should come from.

Also needs fixing

4. app/lib/ is inside the router directory

app/lib/passkeyAuth.ts and app/lib/wallet.ts are under app/, which expo-router scans as the route tree — these become navigable routes /lib/passkeyAuth and /lib/wallet. main already has a top-level frontend/mobile/lib/ (see lib/soroswap.ts, lib/bulkPayout.ts). Please move both there.

5. Out-of-scope files

  • frontend/mobile/assets/expo.icon/icon.json (21+/26-) — unrelated to wallet restoration.
  • frontend/mobile/eslint.config.js (+15) — new lint config, unrelated. Worth having, but as its own PR.

Please revert both here:

git checkout upstream/main -- frontend/mobile/assets/expo.icon/icon.json
git rm frontend/mobile/eslint.config.js

6. package-lock.json churn

5474+/1807- is far more movement than the two dependencies in package.json justify. Please regenerate against current main (rm package-lock.json && npm install) so the diff reflects only what you actually added.

7. tsconfig.json

The 3+/12- diff is pure reformatting — same content, collapsed onto single lines. Harmless, but please revert it so the diff stays focused.

Question on the SDK export

export { webAuthnProvider } from './webauthn'; widens the SDK's public API. I'm not opposed, but I'd like to understand the need first — is there a reason the restore flow can't go through the existing useInvisibleWallet surface, the way the web wallet's passkey path does? If webAuthnProvider genuinely needs to be public, that's fine, but it should be a deliberate API decision rather than a side effect of this screen.

One design note

requirePasskey generates a challenge client-side and only checks that an assertion came back — the assertion is never verified, and the challenge isn't bound to anything. As a local UX gate that's defensible, but it isn't authentication, and the naming implies it is. Worth a comment saying so, so nobody later mistakes it for a security boundary.


Happy to re-review as soon as the three blockers are addressed — the overall approach is sound, it just needs to be ported to the RN runtime rather than the browser's.

@adeboladee

Copy link
Copy Markdown
Author

Why useInvisibleWallet cannot replace webAuthnProvider.authenticate() in the restore UX gate

1. No existing method fits the Biometric UX Gate

The restore flow requires a biometric presence check before restoring. None of the existing useInvisibleWallet methods are suitable:

  • login(): Performs an on-chain check only; does not prompt for biometrics.
  • signAuthEntry(): Prompts for biometrics, but conflates transaction signing with identity verification. It sets unwanted hook states (isPending/error) and reads from the hook's storage adapter instead of SecureStore.
  • register(): Prompts for biometrics, but creates a new passkey rather than authenticating an existing one.
  • Contract operations (deploy(), addSigner(), etc.): Transactional operations with no biometric prompt.

2. Storage Mismatch

  • requirePasskey specifically reads from SecureStore.
  • useInvisibleWallet uses its own StorageAdapter (which defaults to localStorage).

Switching the gate to the hook would tie identity verification to the hook's storage adapter, defeating the purpose of migrating to expo-secure-store.

@Miracle656

Copy link
Copy Markdown
Owner

Thanks for taking #454. Closing this one — the screen it adds can't run on a device, and the dependency changes would take several working features off main with it.

1. restore.tsx is a web component, not a React Native one

frontend/mobile/app/(onboarding)/restore.tsx is built out of DOM primitives. React Native has no DOM, so none of this renders on iOS or Android:

  • six <div> elements with className and CSS-string styles (padding: "2rem 1.25rem", maxWidth: 400, gap: "2.5rem") — RN needs <View>/<Text> and StyleSheet with unitless numbers
  • sessionStorage.getItem / .setItem / .clear at three points in handleRestoresessionStorage is a browser global and is undefined in RN, so the first call throws ReferenceError the moment a restore is attempted
  • className="wallet-shell", a class from the web wallet's stylesheet

This looks like frontend/wallet's restore page copied across rather than ported. The acceptance criterion — existing passkey → recovers contract address → session set — can't be met, because the handler throws before it reaches the session step.

For session state on mobile, lib/walletStore.ts and expo-secure-store are what the rest of the app uses.

2. Dependencies removed that main depends on

frontend/mobile/package.json drops five packages that are in active use:

Removed Used by
expo-document-picker lib/backupFile.ts
expo-file-system lib/backupFile.ts
expo-sharing backup export + the receive screen's share action
jest the mobile test suite
jest-expo the mobile test suite

Dropping jest and jest-expo deletes the test setup outright — main currently runs 8 suites / 155 tests through jest-expo, and the Mobile — typecheck & test CI job runs npm test.

Also, eslint-config-expo is pinned to ~57.0.0. That's the Expo SDK version, not this package's — it's on 10.x, and #506 already added it at ~10.0.0 along with eslint.config.js, which this branch adds a second copy of.

3. tsconfig.json drops a path alias that's now in use

The reformat removes "@/components/*": ["./components/*"]. Since #507 landed, @/components/ScreenScaffold and @/components/TabIcon are imported across the (tabs) group and every route stub, so typecheck fails without it.

4. Out-of-scope lockfile churn

package-lock.json at the repo root is +20063/-648, from adding "invisible-wallet-sdk": "file:../../sdk" to the mobile package. A file: link to a workspace sibling pulls the SDK's entire tree into the root lockfile. Wiring the SDK into mobile is its own piece of work — #594 is scoped to exactly that (feat(mobile): wire SDK and port WalletProvider with secure session persistence), so this should build on that rather than doing it inline.

5. Smaller notes

  • restore.tsx hardcodes factoryAddress, rpcUrl and networkPassphrase inline. lib/network.ts already resolves these from EXPO_PUBLIC_* so testnet/mainnet switching keeps working.
  • lib/passkeyAuth.ts defaults RP_ID to "localhost", which won't match any real relying party on a device. lib/passkey.ts on main already handles the device passkey path.

Suggested path forward

#454 is still open and worth doing. Once #594 lands the SDK wiring and WalletProvider, a restore screen becomes a fairly small piece: build it with View/Text/Pressable and StyleSheet (app/(tabs)/send.tsx is a good template), get the wallet from the provider instead of calling useInvisibleWallet directly, persist through lib/walletStore.ts, and read network config from lib/network.ts. Branch off current main and keep package.json untouched. Happy to review that.

@Miracle656 Miracle656 closed this Jul 30, 2026
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.

26. Restore / import existing wallet

3 participants