Auth¶
frontend/src/features/Auth/ — the pre-auth screens mounted by
AuthNavigator while authStatus === 'anonymous', plus the ReauthSheet
overlay shown in 'reauth-required' (frontend/src/App.tsx:113-127,184-189).
The session state machine itself lives in
frontend/src/context/AuthContext.tsx (see
Navigation — auth gate and
ADR 0004).
Screens¶
| Screen | Route | Responsibility |
|---|---|---|
GetStartedScreen |
GetStarted |
pre-auth landing; its buy CTA opens GUMROAD_PRODUCT_URL (frontend/src/config.ts:64-69) |
LoginScreen |
Login |
email/password login via useAuth().login |
SignupScreen |
Signup |
signup with the required Gumroad license key — the Signup action type takes it as a required third argument so "the paid-content gate is only real if the type system refuses a call that forgets it" (AuthContext.tsx:35-40); route param licenseKey seeds the form (App.tsx:47-49) |
ForgotPasswordScreen |
ForgotPassword |
auth.requestPasswordReset — always-202 anti-enumeration |
ResetPasswordScreen |
ResetPassword |
trades the emailed token via confirmPasswordReset (AuthContext.tsx:631-640) |
CancelResetScreen |
CancelReset |
the "this wasn't me" landing (auth.cancelPasswordReset) |
ReauthSheet |
(overlay, not a route) | in-place re-authentication that keeps RootStack mounted (BUG-NAV-001, App.tsx:184-189) |
Shared building blocks: AuthScreenContainer, AuthBrandBand,
SocialAuthButtons, the components/ fields (EmailField,
PasswordField, LicenseKeyField), and auth.styles.ts.
Hooks and helpers¶
useAuthSubmit(fn, { fallback })— the shared submit state machine every auth form uses: oneerror/submittingpair, failures mapped throughformatApiError, a stable-identityrunwith an in-flight guard so a double submit is a no-op (frontend/src/features/Auth/useAuthSubmit.ts:6-58).useSignupForm,passwordValidation.ts,licenseKeyValidation.ts,canonicalizeEmail.ts,signupErrorRouting.ts— form state and validation helpers.- Social sign-in:
useGoogleAuth/useAppleAuthobtain provider ID tokens (config inoauthConfig.ts),socialFlow.tssequences the exchange. The context exchange returns aSocialSignInResultvalue union —success | needs_license | error— instead of throwing, becauseneeds_licenseis a normal branch that opens the inline license step (AuthContext.tsx:67-92,474-499). Only the collapsed409 needs_licenserefusal routes there; any other 409 is a plain error so the user isn't stranded re-entering a key that was never the problem (AuthContext.tsx:483-499). resetToken.ts— parses the recovery deep-link token.
Session-machine behaviors this module leans on¶
- Signup attaches the device IANA timezone and converts the backend's
user_id == 0duplicate-email sentinel into a client-sideApiError(409, 'email_in_use')(AuthContext.tsx:438-472). - Apple's
full_nameis a one-shot gift: it rides every leg of the flow including the license retry, "because that retry is the request that creates the account — and Apple will never offer the name again" (AuthContext.tsx:531-559). - Persistence-first ordering (BUG-AUTH-005):
applyAuthResponseawaitssaveTokenbefore mutating React state, shared by login / signup / password-reset (AuthContext.tsx:414-436). - Logout and the sheet's "sign out instead" both run
tearDownSession, which clears the token (arming a retry marker if the delete fails), wipes every store and per-user storage key, and lands'anonymous'(BUG-FE-STATE-001,AuthContext.tsx:580-596).
Stores: useWelcomeStore (the Welcome gate reads first-run state around
these screens). API: the auth namespace only.
Grounded in adepthood@55eef11, 2026-07-31.