Android OAuth sign-in/link fails: native URL drops deep-link path → carol:/// Unmatched 404 #297
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol#297
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
After completing OAuth in the system browser on Android, the app returns to a bare deep link
carol:///and Expo Router shows its built-in "Unmatched Route" 404 (see attached screenshot). Sign-in/link never completes.Root cause
React Native's built-in global
URL(react-native/Libraries/Blob/URL.js, installed bysetUpXHR.js) deriveshost/pathnamewith regexes hardcoded tohttps?://(lines 131, 158). Noreact-native-url-polyfillis installed and Expo SDK 56's winter runtime is absent, so RN's implementation is authoritative. Therefore on-device:That single defect breaks both consumers of the return deep link:
parseOauthCompleteDeepLink/parseLinkCompleteDeepLinkgate on`${parsed.host}${parsed.pathname}`(apps/client/lib/auth/oauthDeepLinkParse.ts:32-33). On-device that's"" + "/"→""≠"auth/oauth/complete"→ parser returns null → the awaiting native sign-in flow never receives{init, code}, soPOST /api/auth/tokennever happens.host + pathname→ empty → rendersUnmatched, whose label iscreateURL("/")=carol:///(the screenshot).Masked in CI because vitest runs under Node's spec-compliant
URL.Fix
react-native-url-polyfilland install a WHATWG-compliantURLon native (not web) at the top ofapps/client/app/_layout.tsx, before any deep-link code runs. Fixes the parsers + router extraction.apps/client/app/+native-intent.ts(redirectSystemPath) to redirect the non-routable return deep links to real screens (carol://auth/oauth/complete…→/login,carol://account/identities…→/account), matched on the raw URL string. TheLinkinglistener still drains the queue and completes the exchange.Known follow-up (separate ticket)
True Android cold-start completion (app killed during the browser handoff) still won't finish the exchange — the in-memory waiter is gone, so the queued
{init, code}is never drained. The user lands cleanly on/loginbut must retry. Making cold-start self-healing (drain the queue from a bootstrap effect / dedicated route) is out of scope for this fix.Verify
Fresh Android build (not a JS reload): sign in with GitHub → returns to the app, completes, lands on
/notessigned in — nocarol:///. Repeat for account linking →/account.