feat(onboarding): add passkey wallet restoration - #509
Conversation
|
@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. |
|
@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! 🚀 |
Miracle656
left a comment
There was a problem hiding this comment.
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.js6. 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.
Why
|
|
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 1.
|
| 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.tsxhardcodesfactoryAddress,rpcUrlandnetworkPassphraseinline.lib/network.tsalready resolves these fromEXPO_PUBLIC_*so testnet/mainnet switching keeps working.lib/passkeyAuth.tsdefaultsRP_IDto"localhost", which won't match any real relying party on a device.lib/passkey.tsonmainalready 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.
Resolves #454. Adds the
restore.tsxscreen to allow returning users to regain access by authenticating with an existing passkey, re-deriving their wallet contract address, and setting the session.