feat(expo-example): add export wallet on mobile#168
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
57420ef to
086b5cd
Compare
caf3a37 to
07810cd
Compare
There was a problem hiding this comment.
Pull request overview
Adds a mobile-only wallet export flow to the expo-example app using a hardened react-native-webview wrapper around Turnkey’s export.turnkey.com iframe, plus demo “defense-in-depth” measures (biometric gating, screenshot/recording protection, and dependency pinning).
Changes:
- Add
/exportroute group with consent screen + seed phrase/private key reveal screens, backed by a WebView-based Turnkey export bridge. - Add security utilities: biometric-only auth gate, screen-capture prevention hook, and app-state listener for resetting consent/reveal.
- Pin sensitive dependencies (Turnkey packages + webview) and update Expo config/plugins/docs to reflect export behavior.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Lockfile updates for newly added/pinned dependencies (webview, screen capture, local auth) and resolution changes. |
| apps/expo-example/src/lib/biometricAuth.ts | Adds biometric-only authentication helper for reveal/export flows. |
| apps/expo-example/src/hooks/usePreventScreenCapture.ts | Adds a hook to enable/restore screen-capture protection with fail-closed status. |
| apps/expo-example/src/hooks/useAppChangeListener.ts | Adds AppState transition listener hook with “latest callback” ref pattern. |
| apps/expo-example/src/components/export/WarningCard.tsx | Introduces reusable warning UI card for sensitive screens. |
| apps/expo-example/src/components/export/ExportWebView.tsx | Implements Turnkey export wrapper HTML + message bridge + navigation allowlist hardening. |
| apps/expo-example/src/components/export/ExportBox.tsx | Wraps export WebView with loading/error UI and mounts/unmounts for reveal lifecycle. |
| apps/expo-example/src/components/export/CaptureFailedScreen.tsx | Full-screen fail-closed UI when screen-capture prevention can’t be enabled. |
| apps/expo-example/src/app/index.tsx | Adds “Export wallet” link from home screen. |
| apps/expo-example/src/app/export/wallet.web.tsx | Web shim redirect for wallet export route. |
| apps/expo-example/src/app/export/wallet.tsx | Native seed phrase export screen with consent guard, biometrics, and capture protection. |
| apps/expo-example/src/app/export/private-key.web.tsx | Web shim redirect for private key export route. |
| apps/expo-example/src/app/export/private-key.tsx | Native private key export screen with consent guard, biometrics, and capture protection. |
| apps/expo-example/src/app/export/index.web.tsx | Web-only “export disabled” screen. |
| apps/expo-example/src/app/export/index.tsx | Consent screen with checkbox + navigation to reveal routes. |
| apps/expo-example/src/app/export/_layout.tsx | Route-group layout providing consent context and reset-on-background behavior. |
| apps/expo-example/src/app/_layout.tsx | Registers export route under authenticated stack. |
| apps/expo-example/README.md | Documents new wallet export flow and links to the detailed export hardening doc. |
| apps/expo-example/package.json | Adds/pins expo-local-authentication, expo-screen-capture, and react-native-webview; pins Turnkey deps. |
| apps/expo-example/docs/wallet-export.md | Adds detailed design/security rationale and hardening guidance for the export flow. |
| apps/expo-example/app.json | Adds expo-local-authentication plugin configuration (Face ID permission string). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <View | ||
| style={[ | ||
| styles.webViewWrapper, | ||
| stage !== 'ready' && styles.webViewHidden, | ||
| ]} | ||
| pointerEvents={stage === 'ready' ? 'auto' : 'none'} | ||
| > | ||
| <ExportWebView | ||
| kind={kind} | ||
| onReady={() => setStage('ready')} | ||
| onError={(msg) => { | ||
| setStage('error') | ||
| setErrorMsg(msg) | ||
| }} | ||
| style={styles.webView} | ||
| /> | ||
| </View> |
| <ExportWebView | ||
| kind={kind} | ||
| onReady={() => setStage('ready')} | ||
| onError={(msg) => { | ||
| setStage('error') | ||
| setErrorMsg(msg) | ||
| }} | ||
| style={styles.webView} | ||
| /> |
| | Replay / multi-invocation | same file — `handledPubkey` ref + `requestId` matched on `BUNDLE_INJECTED` | A misbehaving iframe can't drive multiple export calls or replay acks | | ||
| | Screenshot / recording (Android) | `src/hooks/usePreventScreenCapture.ts` | `FLAG_SECURE` via `expo-screen-capture`; fail-closed on reject | | ||
| | Biometric gate | `src/lib/biometricAuth.ts` | `disableDeviceFallback: true` — biometric only, no passcode | | ||
| | Consent + reveal reset | `src/app/export/_layout.tsx`, `wallet.native.tsx`, `private-key.native.tsx` | Consent resets on background; reveal resets on inactive | |
07810cd to
0ab2397
Compare
086b5cd to
0991a9d
Compare
0ab2397 to
592314d
Compare
0991a9d to
f2de218
Compare
592314d to
fef2d5c
Compare
f2de218 to
6bfcd21
Compare
6bfcd21 to
fd2ecf9
Compare
fef2d5c to
96c30fd
Compare
96c30fd to
01177b4
Compare
fd2ecf9 to
4bb2387
Compare
4bb2387 to
b748b5e
Compare
01177b4 to
74b376c
Compare
74b376c to
01a54e6
Compare
b748b5e to
0434ba0
Compare
0434ba0 to
81c7db9
Compare
01a54e6 to
8ba0ed9
Compare
81c7db9 to
d55dcb5
Compare
8ba0ed9 to
554f6a8
Compare
d55dcb5 to
7ecc93f
Compare
554f6a8 to
2e3927e
Compare
7ecc93f to
67a3df9
Compare
2e3927e to
8fe6f05
Compare
8fe6f05 to
6378d7d
Compare
OmarBasem
left a comment
There was a problem hiding this comment.
Tested on physical devices on iOS and Android, most auth flows are working except for dynamic links on both platforms, in addition to passkeys and universal links on iOS which needs extra setup (discussed on Slack, yet to be setup and tested out)
Export wallet looks good
Transactions looks good
A small comment on the email input keyboard, should not hide the email input
Closes FS-1979
For the export wallet on mobile, I decided to go with the same approach as the
iframe-stamperon Web. This is theExportWebViewfile. It requests the bundle using thewallet-coreexport functions and passes the bundle to theexport.turnkey.comwebsite via an iframe. The iframe on mobile is rendered in a WebView. TheExportWebViewcomponent injects some JS into the WebView to enable passing the bundle to the iframe itself via aMessageChannel.Other than this the rest of the files are to demo certain further security measures that app developers can take: Biometrics before revealing the key, blocking screen capture both for screenshots and for not showing the keys when switching between apps and protection against supply chain attacks (restricting package versions and network security config)