Mobile one-tap test sign-in
Status summary#
Implemented, awaiting preview CI (the rewritten mobile spec runs in the preview lane). Stacked on #2485.
Ask#
Mobile preview sign-in took 7 taps. Decision from discussion: keep the consent screen ("the mobile app is effectively a userland app/client") and the unavoidable native ASWebAuthenticationSession dialog; kill everything else. 7 → 3 (or "2 that count": Continue + Allow access).
| Old tap | Fate | How |
|---|---|---|
| 1 Continue | stays | — |
| 2 native dialog | stays | iOS requirement of ASWebAuthenticationSession |
| 3 Continue with email | gone | browser routed through /test-login?email=…&return_to=<authorize URL> |
| 4 OTP submit | gone | same |
| 5 project-access Accept | gone | page auto-selects all + continues for test users (fixed-OTP deployments only) |
| 6 consent Authorize | kept deliberately | mobile stays an untrusted dynamically-registered client |
| 7 in-app project picker | gone | auto-open when the account has exactly one project (general UX win, not test-only) |
Checklist#
- mobile: route test-identity sign-ins through
/test-login— apps/mobile/src/lib/auth.ts:makeAuthUrlAsync+promptAsync(discovery, {url}); gated on the hint matching+test@nustom.com; fallback (stale deployment without the endpoint) is the old prefilled login page since login_hint still rides the authorize URL - auth: project-access auto-continue for test users — _apps/auth/src/routes/auth/project-access.tsx: gated on build-time
fixedTestOtpEnabled+shouldUseTestOtp(session email)+ OAuth flow with projects; selects every project, stores selection,oauth2.continue; holds the loading skeleton while in flight; falls back to the interactive page on error - mobile: auto-open single project after sign-in — apps/mobile/src/app/index.tsx login mutation: list projects post-signin, exactly one → backfill + setLastProject + straight to
/project/[projectId] - spec: pin the new flow — specs/mobile/expected-backend-signin.spec.ts: popup's first interactive page is consent ("Allow Iterate (iOS)?"), Allow access → app lands on the project chat screen ("New chat"), no login/OTP/selection/picker anywhere
Implementation log#
-
Misha's phone test exposed two QR-flow gaps (the QR confirm screen has its own sign-in path, separate from the sign-in screen): its
applyPlanlanded on the boot path with noautoOpen, so the picker still demanded a tap; and the post-switch re-entry waited for a second "Continue" even though the Switch tap already said everything. Fixed:applyPlannavigates to the picker withautoOpenfor sign-in plans, and the Switch tap persists a one-shot marker (AsyncStorage — the reload wipes JS state) that the re-opened screen consumes to continue by itself once freshness and phone-state settle. Bare rescans of the current channel set no marker and keep the reassurance screen, so the untick affordance and existing specs stay intact. QR flow is now: Switch → native dialog → Allow access. -
expo-auth-session supports the wrap cleanly:
AuthRequest.makeAuthUrlAsyncbuilds the real authorize URL (PKCE intact),promptAsync(discovery, {url})opens the wrapper instead./test-loginalready accepts same-origin absolutereturn_to(the authorize endpoint is on the auth origin), so no endpoint changes were needed. -
project-access auto-continue uses a gated
useQuery(no useEffect) that performs the same store-selection + continue the manual button does, and never resolves (redirect keeps it pending) — mirroring the page'sredirectAndStayPendingconvention. -
Kept the protocol shape identical for test users: selection row → consentReferenceId narrowing → project-scoped token. Only the tap is gone.
-
Auto-open single project is deliberately NOT test-gated: a one-item picker is a pointless tap for real users too. The picker remains reachable (Back, and multi-project accounts land on it as before).
-
First CI round: four older mobile specs still tapped "Continue" on project-access and the project in the picker — removed both (that's the feature). Auto-open initially lived in the sign-in mutation with a single attempt; the first project list rides a cold itx WebSocket (~20-30s on preview slots) so it sometimes fell back to the picker. Moved it into the picker's own retrying query, keyed by an
autoOpenparam only the sign-in navigation sets — deterministic, and Back-to-picker never auto-bounces. Verified against preview-5 locally: 8 passed / 1 skipped, approvals green.