Android OAuth cold-start doesn't complete sign-in (in-memory waiter lost when app is killed during browser handoff) #300
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#300
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?
Follow-up to #297 / PR #298.
PR #298 fixes the warm-path Android OAuth return (URL polyfill +
+native-intent), so sign-in completes and the user never sees thecarol:///404. But on a true cold start — Android kills the app while the system browser is in the foreground (e.g. low memory, or "Don't keep activities" enabled) — sign-in still doesn't finish.Why
The native sign-in flow sets up an in-memory waiter when the user taps "Sign in" (
awaitOauthDeepLinkinapps/client/lib/auth/oauthNativeFlow.ts, armed fromapps/client/app/login.tsx'sonPress). When the OS kills the app, that promise/waiter is gone. On relaunch, the return deep linkcarol://auth/oauth/complete?init=…&code=…arrives viaLinking.getInitialURL()and is pushed onto the queue inapps/client/lib/auth/oauthDeepLink.ts, but nothing drains it — there's no live waiter — so thePOST /api/auth/tokenexchange never runs. The user lands cleanly on/login(thanks to #298's native-intent redirect) but is not signed in and must retry.Suggested fix (drain the queue independently of the in-memory waiter)
Make completion survive a process restart, e.g.:
apps/client/app/_layout.tsxthat, on launch, checks the pending deep-link queue (oauthDeepLink.ts) and runs the token exchange if an unconsumed{init, code}payload is present — independent of whether alogin.tsxwaiter exists; orauth/oauth/completescreen) that readsuseLocalSearchParams()and performs the exchange itself, with+native-intentredirecting the deep link there instead of/login.Either way the
inittoken is single-use server-side, so guard against double-consumption (warm waiter + bootstrap both firing).Verify
Enable Android developer option "Don't keep activities", start OAuth sign-in, authenticate in the browser. On return the app should complete the exchange and land signed in on
/notes— not sit unauthenticated on/login. Re-confirm the warm path (activities kept) still works and isn't double-exchanged.