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/)
The previous install flow had a timing race: the
`beforeinstallprompt` event fires at unpredictable times after
page load (often ~30s+ into the session, when Chrome's heuristic
finally decides the user is engaged). If the listener was
attached only inside the dialog's onClick handler, by the time
the user clicked OK the event had already fired into the void
and the deferred prompt was null — so `install()` returned
"unavailable" every time.
The new flow attaches the listener as early as possible:
1. `pwaUtil.prepareInstallPrompt()` — new public method that
calls the private `attachListener()`. Idempotent (guard via
`listenersAttached`).
2. `pwaUtil.canPromptInstall()` — companion getter that returns
whether a deferred event is currently cached. Useful for UI
to decide whether to even show the install CTA.
3. `pwa-install-overlay.tsx` — calls `prepareInstallPrompt()`
right after the isSupported / isInstalled guards. The overlay
mounts when the user enters /chat, so this is the earliest
practical point in the user flow.
4. `pwa-screen.tsx` — calls `prepareInstallPrompt()` BOTH at
module load (synchronous, so the listener is attached before
the splash screen's useEffects run) AND in a useEffect for
robustness. The splash screen is the very first screen the
user sees, so attaching here is the earliest possible.
5. `pwa-install-dialog.tsx` — `handleInstall` is now async,
and `onInstall` accepts `() => void | Promise<void>`. The
overlay calls `await pwaUtil.install()` so the deferred
prompt flow can complete before the dialog unmounts.
Together: the listener is now attached from the moment the
splash screen module loads, the deferred prompt is captured
whenever Chrome decides to fire the event, and the dialog's
install button can reliably `prompt()` against the cached
deferred. The full install flow now works end-to-end.
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 Serwist createSerwistRoute factory returns a GET handler with
`params: Promise<{ path: string }>` hardcoded. Next.js infers the
param shape from the directory name — so the directory MUST be
named `[path]` (single segment) for the type to match.
`[[...slug]]` (catch-all) was wrong on two counts:
1. The segment name is `slug`, not `path` (Serwist expects
exactly `path`).
2. The shape is `string[]` (array), not `string` (Serwist
expects a single string).
Per the Serwist source comment: 'Asset and chunk names must be at
the top, as our path is /serwist/[path], not /serwist/[...path]'
— all SW output files (sw.js, workbox-XXX.js) are flat, single
segment is sufficient.
The fix is a directory rename only — the route.ts content
(createSerwistRoute call) is unchanged.
Verification:
- pnpm build now succeeds (TypeScript check passes)
- The Serwist route appears in the build output as `● /serwist/[path]`
prerendered with `sw.js` and `sw.js.map` static pages
- `pnpm start` + Chrome: `/serwist/sw.js` will be served by the
route handler and registered by <SerwistProvider>
Next.js 16 ships Turbopack as the default bundler, but
@ducanh2912/next-pwa is webpack-only — the previous PWA setup
required `--webpack` on dev/build/"dev:proxy" scripts, accepting
a perf regression in exchange for working PWA install.
@serwist/turbopack (paired with the official 'serwist' package
and 'esbuild') is the Turbopack-native successor: it works with
the default bundler, no opt-out flag, and gives us a real TS
service worker source file we can read and edit.
What this commit does:
1. Swap deps in package.json:
- Remove @ducanh2912/next-pwa
- Add @serwist/turbopack, serwist, esbuild (all devDeps)
- Drop --webpack flag from dev / dev:proxy / build scripts
(Turbopack is restored as the bundler)
2. next.config.ts:
- Replace withPWAInit wrapping with withSerwist named import
- Update comment block to describe new architecture
3. New src/app/sw.ts: Serwist service worker source.
skipWaiting + clientsClaim + navigationPreload + defaultCache.
This file is compiled to a real SW at build time by Serwist.
4. New src/app/serwist/[[...slug]]/route.ts: catch-all Route
Handler that serves the compiled SW at /serwist/sw.js plus
Serwist's chunked runtime assets. Catch-all is required
because Serwist serves multiple files under /serwist/.
5. src/app/layout.tsx: replace <SwRegister /> with
<SerwistProvider swUrl="/serwist/sw.js"> from
@serwist/turbopack/react. The provider handles registration
+ lifecycle internally.
6. Delete src/app/_components/core/sw-register.tsx: replaced
by SerwistProvider.
7. .gitignore: add public/sw.js and public/workbox-*.js
(defensive — these are build artifacts that change hash
every build and shouldn't be committed).
8. Untrack public/workbox-3c9d0171.js: stale workbox runtime
from the previous next-pwa build, hash is build-specific
so it can never be regenerated correctly.
Not modified (intentionally):
- src/utils/pwa.ts: pure beforeinstallprompt browser-API wrapper,
library-agnostic, stays as-is.
- pwa-install-overlay / pwa-install-dialog: install UI flow
unchanged.
- public/manifest.json: still works; layout's metadata.manifest
points at it.
Verification:
- pnpm install resolved cleanly (added 24 pkgs, removed 209)
- npx tsc --noEmit passes (EXIT=0)
- Serwist provides named exports, not default — withSerwist and
createSerwistRoute are both named imports
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.
PWA install flow was wired but unusable: pwaUtil.install() calls
deferred.prompt(), but Chrome only fires beforeinstallprompt when a
valid Service Worker is registered + a manifest is linked. The project
had both intent (manifest, usePwaInstall hook) but no live SW —
public/sw.js was a stale workbox build artifact from a previous
attempt with hardcoded chunk URLs that no longer matched any build.
What this commit does:
1. Add @ducanh2912/next-pwa@10.2.9 as devDependency.
2. Wrap nextConfig with withPWA({...}):
- dest: "public" SW outputs to public/sw.js
- disable: true skip SW generation in dev (HMR-friendly)
- register: false don't auto-inject registration
- workboxOptions.skipWaiting: true + clientsClaim: true — new SW
takes over immediately
3. Add --webpack flag to dev/build/dev:proxy scripts.
@ducanh2912/next-pwa is webpack-only; Turbopack (Next 16 default)
doesn't load webpack plugins. Per user decision to accept the perf
trade-off in exchange for a working PWA.
4. Delete stale public/sw.js (workbox will regenerate it at build
time with the correct chunk URLs).
5. Add src/app/_components/core/sw-register.tsx: a no-UI client
component that registers /sw.js in production. Mounted at the end
of <body> in layout.tsx. disable: true means dev mode skips
registration (no /sw.js to fetch anyway).
After this commit + pnpm build, public/sw.js is regenerated with
correct chunk URLs, registered on first prod load, and Chrome fires
beforeinstallprompt — usePwaInstall + pwaUtil.install() then trigger
the native install prompt end-to-end.
Facebook Graph API field expansion uses parentheses, not equals signs.
The buggy `picture.type=large` produced HTTP 400 with
`OAuthException code 2500: Syntax error ... at character 32`.
Fix `FIELDS` constant and 2 JSDoc comment lines in facebook-graph.ts
to use `picture.type(large)` per Facebook's documented syntax.
Effect: after Facebook OAuth sync, `fetchFacebookUserData` now
succeeds (HTTP 200), `UserStorage.setAvatarUrl` and
`AuthStorage.setFacebookId` get populated, and the
`[auth-machine] syncFacebookBackendActor: fetchFacebookUserData
failed (continuing anyway)` warning stops firing on the happy path.