AuthReset was declared in the AuthEvent union and handled in
auth-machine.idle.on with `assign(() => initialState)`, but
nothing in the codebase actually dispatches it anymore.
History of dispatch sites (now all gone):
- sidebar-screen.tsx previously dispatched AuthReset post-logout
alongside router.replace(ROUTES.chat) — that was simplified to
just dispatch AuthLogoutSubmitted, the state machine's
loggingOut state now does the context reset itself in onDone
via the same `assign(() => initialState)` action.
- chat-screen / splash / auth screens never dispatched it.
- The /auth screen's pendingRedirect-based redirect loop doesn't
need it (the redirect is one-shot, not a state that needs
resetting).
So the event handler is unreachable. Remove the type member
from auth-events.ts and the handler from auth-machine.ts.
No behavior change: the only path that 'reset auth state' was
already covered by loggingOut.onDone.
Verified via `grep -rn 'AuthReset' src/`: only 2 hits remain
after this commit — both are the type declaration and the
machine handler, which is now also gone (zero hits).
The three defensive Zod helpers (stringOrEmpty, numberOrZero,
booleanOrFalse) used by user.ts are general-purpose Zod utilities
that any schema may need — they're not user-specific. The original
inline declaration in user.ts had two downsides:
1. Any new schema (chat, auth, metrics, etc.) that needs the same
'null | undefined → default scalar' pattern would have to
duplicate the helper.
2. The long defensive-parse comment block (11 lines) lived inside
user.ts and didn't explain the helpers' general purpose.
Move them to src/data/schemas/nullable-defaults.ts (top-level
sibling of auth/, chat/, metrics/, user/ subdirs) so any schema
can import them. Keep the original explanation verbatim at the
top of the new file.
Bundled in this commit:
- user.ts imports the helpers from the new file (drops 11 lines
of comment + 3 const declarations)
- 3 auto-generated barrel index files regenerated by barrelsby
to pick up the new exports (chat/, subscription/, stores/user/)
Previously, the Skip button (guest login entry) relied on the auth
state machine's `pendingRedirect` flag to trigger the navigation
to /chat after guest login completed. That introduced a
non-obvious coupling: the button dispatched an event, the machine
ran an actor, the auth screen (or splash useEffect) saw the flag
and redirected.
This refactor moves the redirect into the splash screen itself:
- `SplashButton` now takes an `onSkip` prop and no longer knows
about auth dispatch. Pure presentation component.
- `SplashScreen` provides `handleSkip` that dispatches the
`AuthGuestLoginSubmitted` event AND immediately calls
`router.replace(/chat)` for snappier perceived navigation.
- `auth-machine.ts`: `loadingGuestLogin.onDone` no longer sets
`pendingRedirect: true` (splash already navigated). Sets
it to `false` explicitly so other screens (auth, sidebar) that
also react to `pendingRedirect` don't double-navigate if the
user triggers guest login from a non-splash surface in the future.
No behavior change for the happy path: Skip → /chat works the same.
The refactor is purely about responsibility allocation and
component decoupling.
The AuthState.isLoading derivation in auth-context.tsx covered the
OAuth redirect phase (loadingOAuth) but NOT the backend-sync phase
that runs after the OAuth callback returns
(syncingGoogleBackend / syncingFacebookBackend). Any UI that
read isLoading would incorrectly report "not loading" during
the backend token-exchange round-trip.
Add the two missing state.matches() lines so isLoading is true
for the entire OAuth login window (button click → OAuth redirect
→ callback → backend sync).
Note: in the current OAuth flow the user is on /chat during the
sync phase (NextAuth redirected them there), so the splash/auth
button is not actually visible to reflect the new true value.
The fix is still correct because:
- Conceptually aligned: any in-flight login op is "loading"
- Defense in depth: future UI on /chat that consumes isLoading
will get accurate feedback
- Non-regression: only makes isLoading more permissive (true more
often), never false-fires
A visible /chat overlay during the sync phase would be a separate,
larger change — noted in the planning doc but not in this commit.
The previous event name implied "re-check status", but the actual
semantics is just "read storage on app start and sync loginStatus into
the machine". It was being dispatched from three sites:
1. AuthStatusChecker mount useEffect (the legit one-time init)
2. AuthStatusChecker loginStatus-watching useEffect (dead code — the
machine's onDone already writes loginStatus, re-reading storage
just returns the same value as a no-op)
3. Sidebar post-logout effect (also dead code — AuthReset directly
sets loginStatus to initialState's "notLoggedIn", and
userLogoutActor already cleared storage, so the values are
already aligned)
This commit:
- Renames event AuthStatusCheckSubmitted → AuthInit in:
* auth-events.ts (type union)
* auth-machine.ts (handler + transition target: checkingAuthStatus →
initializing, mirroring UserInit / initializing in user-machine)
* auth-status-checker.tsx (single dispatch site)
* root-providers.tsx (one comment)
- Simplifies auth-status-checker.tsx from 74 → ~40 lines:
* Removes useEffect ② (loginStatus-watching re-check)
* Removes prevLoginStatusRef + skipNextChangeRef + useAuthState
(dead loop-guard machinery no longer needed)
- Removes the post-logout re-check dispatch from sidebar-screen.tsx:
* AuthReset alone is sufficient — userLogoutActor cleared storage
and AuthReset writes back initialState, so they match by
construction. No re-verification needed.
Net: -44 lines, no behavior change for the happy paths (startup,
login, logout), one fewer source of false re-checks.
Root cause: backend `/auth/logout` returns `{ success: true, data: null }`
(no business payload for a fire-and-forget endpoint). The previous
`AuthApi.logout` ran `unwrap(env)` (which passes null through, since
null is defined) and then `LogoutResponse.fromJson(null)`, which
Zod-parses as `z.object(...)` and throws "Invalid input: expected
object, received null".
The error was caught in `AuthRepository.logout` with
`console.warn(... clearing local anyway)` — logout actually still
worked (local clear ran), but a noisy red ZodError hit the console on
every logout click.
Fix: align `AuthApi.logout` with the existing fire-and-forget pattern
used by `register` and `sendCode` — call `httpClient` and ignore the
response body. Drop the now-unused `LogoutResponse` import.
`AuthRepository.logout` already discards the return value, so the
`Promise<LogoutResponse> → Promise<void>` signature change is safe.
Bundled in this commit (unrelated):
- chat-machine.actors.ts / user-machine.ts: removed stale design-doc
comment blocks (IDE/linter cleanup)
- user-machine.actors.ts: removed redundant `userStorage.clearUserData()`
from `userLogoutActor` — `authRepo.logout` already clears local
user data, so this was a no-op duplicate.
- sidebar-screen.tsx: removed one stale comment line.