Magic Link Deep Link Auth Not Completing on Mobile
Metadata
- Date:
2026-05-23 - Status:
resolved - Severity:
high - Related issue/ticket:
N/A - Owner:
Auth Flow
About
Overview: - When a user clicks the "Sign In" button in the magic-link email on mobile, the browser opens the landing web /auth/verify page. The web page verifies the token with the server and gets a handoff code, but then only displays the 6-digit code for the user to type manually. The app never receives the code automatically. The user is left on the browser page with no indication they must open the app and type a code. - The deep link redirect (encache://auth/verify?code=XXXXXX) that should automatically open the app and complete sign-in was never sent from the web page. - Even if the redirect had been sent, the app's verify screen did not read a code URL param from deep links and would not have auto-submitted it.
Technical Questions: - Where should the deep link redirect be triggered? → The landing web page after successful verification. - What URL scheme is the app registered for? → encache:// (declared in app.json scheme and Android intentFilters). - What route does the deep link map to? → encache://auth/verify?code=XXXXXX → expo-router resolves to app/(auth)/verify with code param. - Does the app handle the code param on the verify screen? → No — this was the missing piece on the app side.
Resources: - Web verify page: apps/landing-web/src/routes/auth/verify/+page.svelte - App verify screen: main/app/app/(auth)/verify.tsx - App scheme config: main/app/app.json (scheme: "encache", Android intentFilters) - Server verify endpoint: main/server/api/auth/verify/app.py - Server handoff endpoint: main/server/api/auth/handoff/app.py
Steps to cause failure
flowchart LR
Email["User clicks Sign In in email"] --> Browser["Browser opens\nhttps://encache.ai/auth/verify?email=...&token=...&session=..."]
Browser --> WebVerify["Web page POSTs /auth/verify\n→ gets handoffCode"]
WebVerify --> ShowCode["Web page shows 6-digit code\nto type manually"]
ShowCode --> Stuck["User stuck — no deep link\napp does not open"] System
flowchart TD
AuthStart["POST /auth/start\n→ Cognito CUSTOM_AUTH\n→ SES email with magic link"] --> Email
Email["User opens email on phone"] --> MagicLink["Clicks Sign In button\n→ opens browser at MAGIC_LINK_BASE_URL"]
MagicLink --> WebPage["Landing web /auth/verify\nparses email+token+session from URL"]
WebPage --> ServerVerify["POST /auth/verify\n→ Cognito challenge response\n→ create_handoff_code()"]
ServerVerify --> HandoffCode["Returns handoffCode + expiresIn"]
HandoffCode --> DeepLink["FIXED: window.location.href =\nencache://auth/verify?code=..."]
DeepLink --> AppVerify["App opens at /(auth)/verify\nwith code param"]
AppVerify --> AutoSubmit["FIXED: useEffect reads code param\n→ calls completePasswordlessSignInWithCode"]
AutoSubmit --> Handoff["POST /auth/handoff\n→ returns Cognito tokens"]
Handoff --> Session["Session persisted\nUser logged in"] Reproduction Details
- Request a magic link for any email.
- Open the email on a phone and click the "Sign In" button.
- The browser opens
https://encache.ai/auth/verify?.... - The web page shows a 6-digit code box and says "Enter this code in the Encache app".
- No deep link redirect fires. The app never opens automatically.
- User is unable to complete sign-in without manually switching apps and typing the code.
Reproduction test: main/app/__tests__/verify-screen.test.tsx → test case "auto-submits the handoff code delivered via deep link URL param" fails before the fix.
Root Cause
Two missing pieces, both required for the flow to work end-to-end:
1. Web page never sent the deep link redirect. apps/landing-web/src/routes/auth/verify/+page.svelte called the server, received the handoff code, and immediately set status = "code" to display the manual-entry UI. The line window.location.href = 'encache://...' was never present. The fallback code display (for desktop or app-not-installed cases) was the only path.
2. App verify screen ignored the code URL param. main/app/app/(auth)/verify.tsx declared useLocalSearchParams<{ email?: string }>() — no code key. Even if the OS delivered encache://auth/verify?code=654321 via expo-router, the component would have silently ignored the param and waited for the user to type a code.
Fix Summary
apps/landing-web/src/routes/auth/verify/+page.svelte
After a successful server verification and before setting status = "code", send the deep link redirect:
const deepLink = `encache://auth/verify?code=${encodeURIComponent(handoffCode)}`;
window.location.href = deepLink;
main/app/app/(auth)/verify.tsx
- Extended
useLocalSearchParamstype to includecode?: string. - Added
deepLinkCodememo from thecodeparam. - Added a
useEffectthat fires whendeepLinkCodeis present and is a valid 6-digit number, callinghandleVerify(deepLinkCode)automatically. - Suppressed the keyboard focus
useEffectwhen a deep link code is present (no need to open the keyboard when auto-submitting).
Verification
- Failing test written before fix:
verify-screen.test.tsx→ "auto-submits the handoff code delivered via deep link URL param" - Test confirmed failing before fix: 1 failed, 8 passed
- Test confirmed passing after fix: 9 passed, 0 failed
- All pre-existing verify-screen tests continue to pass