feat(mobile): register veil:// scheme and universal/app links - #508
Conversation
|
@Olorunfemi20 is attempting to deploy a commit to the miracle656's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@Olorunfemi20 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! 🚀 |
Register the custom scheme and platform universal/app links so payment requests and other external entry points open the right screen from both a cold start and a warm resume. - app.config.ts replaces app.json so the deep-linking surface is derived from the same constants the runtime resolver uses. Declares the veil:// and web+stellar: schemes, iOS associated domains, an autoVerify Android intent filter, and the bundle identifier / package name both need. - lib/deepLinks.ts resolves an inbound URL to an in-app route. Inbound links are untrusted, so it matches a fixed allowlist of routes and copies only the query parameters each route declares; foreign hosts, unknown schemes, and over-long URLs fall back to the home route rather than navigating. Query parsing is hand-rolled because React Native's URLSearchParams accessors throw. - app/+native-intent.ts wires it into expo-router, which calls it for both launch paths, and can never throw during launch. - Placeholder /pay, /send, /receive, and /create-wallet routes give the links somewhere to land; /pay forwards a request into the send form and preserves the raw SEP-7 URI for the handler in backlog Miracle656#38. - apple-app-site-association and assetlinks.json are served by the wallet web app, with next.config.js pinning their Content-Type. Both carry placeholders documented in the mobile README.
167b0e2 to
7a40640
Compare
Miracle656
left a comment
There was a problem hiding this comment.
This is strong work. lib/deepLinks.ts with 246 lines of tests behind it (deepLinks.test.ts + appConfig.test.ts) is exactly the right shape for something that has to behave identically on cold start and warm resume, and +native-intent.ts is the correct expo-router hook for it. Moving app.json → app.config.ts is well-argued in the description, and I agree with the reasoning: the deep-link surface and the resolver that parses links must not drift, and static JSON can't document why it looks the way it does.
Also — thank you for adding the Mobile — typecheck & test job to ci.yml. That gap was ours, and mobile PRs have been merging without any typecheck gate. That's a genuine contribution beyond the issue's scope and it's welcome.
Serving apple-app-site-association and assetlinks.json from frontend/wallet/public/.well-known/ (with the next.config.js headers) is the right call too — universal links don't work without the web side, so that's in scope, not scope creep.
Two things before this can merge.
1. Blocker: package-lock.json conflicts with main
That's the only conflicted file — everything else merges cleanly:
git fetch upstream main
git merge upstream/main
# resolve by regenerating rather than hand-merging:
cd frontend/mobile && rm package-lock.json && npm install
git add package-lock.json2. Needs coordination: route collision with #507
#507 (navigation shell, Closes #434) is open and creates a tab group containing:
frontend/mobile/app/(tabs)/send.tsx
frontend/mobile/app/(tabs)/receive.tsx
This PR creates them at the root instead:
frontend/mobile/app/send.tsx
frontend/mobile/app/receive.tsx
You both also modify app/index.tsx. Whichever lands second will produce two routes that resolve to the same destination, and the deep-link targets in deepLinks.ts would point at whichever expo-router picks.
This isn't a defect in your PR — it's my sequencing problem, two issues handed out with overlapping surface area. I'm going to land #507 first, since it defines the route tree that everything else drops into. Once it's in, please rebase and point your link targets at the (tabs) paths for send and receive. Your pay, create-wallet, and token/[id] routes are unaffected.
No action needed from you on that second point until #507 lands — I'll comment here when it does.
Not your problem
The Wallet E2E Tests failure is pre-existing on main — happy-path.spec.ts is timing out on page.waitForURL in the real passkey flow, and the most recent main run fails the same way. Being fixed separately. Ignore it.
|
Heads-up before you push anything: don't change the app identifier or associated domain yet. This PR declares A bundle identifier is permanent once the app ships to either store, and it has to match both the Everything else in my review still stands and is safe to act on. Just leave the identifiers alone until I confirm. |
Reconcile deep linking with the navigation shell and mobile screens that landed after this branch was opened: - Port main's expo-secure-store and expo-camera plugin entries into app.config.ts before dropping app.json, so SecureStore and the QR scanner keep working. - Drop this branch's root app/send.tsx and app/receive.tsx; /send and /receive are now owned by the (tabs) group. Move the deep-link prefill contract (to, amount, asset, memo) into (tabs)/send.tsx so veil://send and the /pay redirect still land prefilled. - Take main's app/index.tsx redirect into (tabs) and main's lib/theme.ts; port create-wallet.tsx onto the useTheme/ThemeColors API. - Switch the two new suites from vitest to the repo's jest setup, and rewrite the vitest-only two-argument expect() in appConfig.test.ts. tsc --noEmit clean; jest 8 suites / 155 tests passing; expo config resolves both the veil:// and web+stellar: schemes.
`npm ci || npm install` would silently paper over lockfile drift, which is the one thing the job is there to catch. Verified `npm ci` resolves cleanly against frontend/mobile/package-lock.json.
|
Merging. The branch needed reconciling against the mobile work that landed after you opened it (the navigation shell from #507 in particular). What I changed: Config. Routes. Tests. Both new suites imported from CI. Changed the new mobile job from Two notes for follow-ups, neither blocking:
Final state: |
Reconcile the dev-client/EAS setup with the config work that landed since: - app.json no longer exists (Miracle656#508 moved to app.config.ts). Port the expo-dev-client plugin across, and add webcredentials: alongside the existing applinks: entry — iOS will not offer a passkey for a domain that is not claimed as a webcredentials service. - Keep main's xyz.veil.wallet identifier and app.veil.xyz domain rather than this branch's app.veil.mobile / veil.app, so the config stays consistent with the .well-known files already served by the wallet. - Drop react-native-passkey ^3.0.0. main already ships react-native-passkeys ^0.4.1, which is what lib/passkey.ts uses; adding the other package would put two WebAuthn implementations in the same app. - expo-dev-client ~5.2.5 does not exist on the SDK 57 line (npm notarget). Pinned to ~57.0.10. - Replace the personal Apple ID in eas.json with a placeholder alongside the other REPLACE_WITH_* values. Cover the new webcredentials entry in appConfig.test.ts. tsc clean; jest 8 suites / 157 tests; expo lint clean.
The inbound-routing half of Miracle656#466 landed with Miracle656#508: lib/deepLinks.ts already maps web+stellar:pay?... onto /pay with the SEP-7 parameters translated, and app/+native-intent.ts drives it identically on cold start and warm resume. Reduce this branch to the SEP-7 payload work that is genuinely missing: - Drop app.json (main uses app.config.ts), the Linking handler added to app/_layout.tsx (duplicates +native-intent.ts), and the root app/send.tsx (/send is owned by the (tabs) group). - Trim lib/sep7.ts to the parts deepLinks.ts does not cover: parseSep7Uri, parseQrValue, looksLikeStellarAddress and buildSep7PayUri. Remove parseVeilLinkUri/parseDeepLink so there is one owner for inbound routing. - Replace the private destination-only builder in (tabs)/receive.tsx with the shared one, so requesting a specific amount is a parameter away. - Add lib/__tests__/sep7.test.ts (14 cases) covering parsing, the QR bare address path, and a build/parse round-trip including memo encoding. tsc clean; jest 10 suites / 174 tests; expo lint clean.
What
Registers the
veil://custom scheme and the platform universal/app links, and wires expo-router so an inbound link routes to the right screen from both a cold start and a warm resume.closes #498
Approach
app.config.tsreplacesapp.json. Static JSON cannot explain itself, and the deep-linking surface has to agree with the code that resolves links at runtime — the two silently drifting apart is the classic way deep links stop working.Deriving the config from the resolver's constants turned out not to be possible: Expo transpiles
app.config.tson its own and thenrequires it, so a relative import of a sibling.tsmodule fails to resolve at config-load time (npx expo configfails outright). The constants are therefore duplicated, andlib/__tests__/appConfig.test.tsholds the two together instead. It asserts that every natively-registered scheme is one the resolver handles, that the iOS associated domains and the Android intent-filter hosts are the same set, and — the check that matters most — that everypathPrefixclaimed as an app link actually resolves to a route rather than the fallback. A path claimed natively but unrouted is a link that opens the app to a blank screen, which is worse than not claiming it.Resolution itself lives in
lib/deepLinks.tsas a pure function with no React Native or Expo imports, which makes it directly unit-testable and reusable by the SEP-7 handler in backlog #38.app/+native-intent.tsis the expo-router hook that calls it. expo-router invokesredirectSystemPathfor every inbound link on both launch paths —initial: trueon a cold start,initial: falseon a warm resume — so routing the two identically falls out of resolving them through the same function rather than through two separate code paths.Three URL families resolve to the same routes:
veil://pay?to=G…&amount=10/paythen/send, prefilledhttps://app.veil.xyz/receive/receiveweb+stellar:pay?destination=G…&amount=10/pay, raw URI preserved asuri/Security
Inbound links are untrusted input: any installed app, web page, or QR code can send one. The resolver therefore never echoes an arbitrary path back to the router. It matches a fixed allowlist of routes, copies only the query parameters each route declares, and falls back to
/for anything else. Specifically covered by tests:https://evil.example/pay) and look-alike subdomains (app.veil.xyz.evil.example) are rejected.https://app.veil.xyz@evil.example/pay) is rejected — the host is taken after the last@, not before it.javascript:,file:) are rejected.redirectSystemPathruns during launch, so a throw there takes the app down with it.resolveDeepLinkcatches internally and returns the fallback route; the hook wraps it a second time as a backstop.Query strings are parsed by hand rather than with
URLSearchParams, because React Native's built-in implementation is a stub whose accessors throw. Depending on which polyfill happens to be installed at launch time is not a good bet for code on the launch path.SEP-7
Scope here is routing only.
web+stellar:pay?…maps its fields onto the/payroute and forwards the original URI untouched asuri, so the handler in backlog #38 can re-parse and fully validate it. Validation is deliberately not duplicated —sdk/src/sep7.tsalready implements it (address checksums, amount ranges, memo limits, hostile callback rejection) and that is what #38 should reuse.Placeholder screens
/pay,/send,/receive, and/create-walletare added as minimal screens. A deep-link config with nothing to link to cannot be verified, so these exist to give the routes a destination and to own the prefill contract (to,amount,asset,memo) that links depend on. They hold no wallet logic;/payforwards into the send form.Universal / app link files
apple-app-site-associationandassetlinks.jsonare served from the wallet web app, withnext.config.jspinningContent-Type: application/jsonon both — iOS refuses an AASA file served as anything else, and the file has no extension for Next to infer from.Both carry placeholders that must be replaced before a store build, documented in the mobile README:
APPLE_TEAM_ID— the Apple Developer Team ID signingxyz.veil.wallet.ANDROID_RELEASE_CERT_SHA256_FINGERPRINT— the release signing certificate fingerprint, plus the Play App Signing fingerprint if distributed through Play.Until then Android's
autoVerifysimply fails verification and links keep opening in the browser, which is the safe failure mode; theveil://scheme works regardless.Testing
43 unit tests.
lib/__tests__/deepLinks.test.tscovers each scheme, the host checks above, malformed input, and a parity check asserting every link resolves identically withinitial: trueandinitial: false.lib/__tests__/appConfig.test.tscovers the config/resolver agreement described above.Verified locally against a real install:
npm run typecheckandnpm testare clean, andnpx expo config --type public --jsonresolves to the expected scheme array, associated domains, and intent filter.Vitest is added as a mobile dev dependency and a new
mobileCI job runsnpm run typecheckandnpm test. The lockfile change is additive — no existing package was removed or version-changed.Manual device verification, per the README:
Run each twice — once force-quit for the cold start, once backgrounded for the warm resume.
Notes for review
app.veil.xyzserves the two files. The custom scheme is fully testable today.web+stellar:is declared through Expo'sschemearray rather than a hand-written intent filter, so prebuild generates the iOSCFBundleURLTypesentry and the Android intent filter consistently.URLSearchParamsif you would rather rely on Expo's WinterCG polyfill being installed before+native-intentruns. I did not want launch-path code to depend on that ordering, but it would remove about twenty lines./create-walletis claimed as a link target; drop it fromLINKED_PATHSandLINKABLE_ROUTESif onboarding should not be externally reachable.