Verify Screen Deep Link Code Not Auto-Filled
Metadata
- Date:
2026-05-23 - Status:
resolved - Severity:
high - Related PRs:
#510,#512 - Owner:
Auth Flow
About
Overview: When a user taps the magic-link email on their phone the app opens to the verify screen, but the 6-digit code input remains empty and authentication does not complete automatically. The user is forced to switch back to the browser, read the code, then return to the app and type it manually.
Technical Questions: - Does the deep link URL carry the code param to the app? Yes — encache://verify?code=XXXXXX is correctly delivered and expo-router populates useLocalSearchParams({ code }). - Does the verify screen call completePasswordlessSignInWithCode? No — see root cause below. - Is the input field populated? No — setCode is never called with the deep link value.
Resources: - App verify screen: main/app/app/(auth)/verify.tsx - Test file: main/app/__tests__/verify-screen.test.tsx - Prior bug (web + app missing redirect): docs/bugs/2026-05-23-magic-link-deep-link-auth-not-completing.md
Steps to cause failure
flowchart LR
Email["User taps magic link in email"] --> App["App opens at /(auth)/verify\nwith code=654321 in params"]
App --> Mount["VerifyScreen mounts\ndeepLinkCode = '654321'"]
Mount --> Effect["deepLinkCode useEffect fires"]
Effect --> OldFix["handleVerify(deepLinkCode) called\n(submission runs invisibly)"]
OldFix --> EmptyInput["Input shows '' — user sees nothing\nbelieves auto-fill did not work"] System
flowchart TD
DeepLink["encache://verify?code=654321\narrives at app"] --> Router["expo-router\nmaps to /(auth)/verify"]
Router --> Params["useLocalSearchParams\nreturns { code: '654321' }"]
Params --> Memo["deepLinkCode = '654321'"]
Memo --> Effect["useEffect fires on deepLinkCode change"]
Effect --> SetCode["setCode('654321')\nFIXED: input now shows the code"]
SetCode --> CodeEffect["code useEffect fires\n→ lastSubmittedRef check\n→ handleVerify('654321')"]
CodeEffect --> API["completePasswordlessSignInWithCode\n→ session persisted"]
API --> Success["'Signed in. Redirecting...'"] Reproduction Details
- Request a magic link for any email address.
- On a phone, open the email and tap "Sign In".
- The browser opens
https://encache.ai/auth/verify?..., which verifies the token and fireswindow.location.href = encache://verify?code=XXXXXX. - The Encache app opens at the verify screen.
- Observation: the 6-digit input field is empty. The user sees no indication that a code was received. No verification attempt appears to run.
Reproduction test (failing before fix): main/app/__tests__/verify-screen.test.tsx → "fills the code input with the deep link code so the user sees visual feedback"
Root Cause
PR #510 added a deepLinkCode useEffect to auto-submit the code from the deep link URL:
useEffect(() => {
if (!deepLinkCode) { return; }
if (!/^\d{6}$/.test(deepLinkCode)) { return; }
handleVerify(deepLinkCode); // <-- submission bypasses setCode()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [deepLinkCode]);
handleVerify accepts an overrideCode parameter and submits the code directly, bypassing the code state entirely. As a result:
- The input field (
value={code}) always shows""— the user sees an empty box and believes nothing is happening. - The submission runs invisibly in the background with no visible loading indicator in the input.
- If verification succeeds the "Signed in. Redirecting..." message eventually appears, but users who see the empty box first often type the code themselves, causing a race between the two submissions (the dedup guard via
lastSubmittedRefprevents the manual entry from firing, leaving the user confused when typing 6 digits does nothing).
The fix is to call setCode(deepLinkCode) instead. The existing code-watching useEffect then drives the submission and the dedup ref (lastSubmittedRef) prevents double-submission. The input field shows the code immediately so the user knows the deep link was received.
Fix Summary
main/app/app/(auth)/verify.tsx
Replace handleVerify(deepLinkCode) with setCode(deepLinkCode) in the deepLinkCode effect.
useEffect(() => {
if (!deepLinkCode) { return; }
if (!/^\d{6}$/.test(deepLinkCode)) { return; }
- handleVerify(deepLinkCode);
+ // Populate the input field so the user sees visual feedback that the
+ // code was received. The code-watching effect then drives the actual
+ // submission; lastSubmittedRef guards against double-submission.
+ setCode(deepLinkCode);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [deepLinkCode]);
The code-watching effect already handles 6-digit validation, deduplication via lastSubmittedRef, and the submitting/success guards — so routing through setCode is strictly safer than calling handleVerify directly.
Verification
- Failing test written before fix:
verify-screen.test.tsx→ "fills the code input with the deep link code so the user sees visual feedback" - Test confirmed failing before fix: 1 failed, 9 passed
- Fix applied:
setCode(deepLinkCode)in the deepLinkCode useEffect - Test confirmed passing after fix: 10 passed, 0 failed
- All pre-existing verify-screen tests continue to pass
- Reverted fix and confirmed test fails again: 1 failed, 9 passed