Skip to content

feat: client-side navigation for auth state transitions#45

Merged
amrtgaber merged 1 commit into
mainfrom
feat/client-side-auth-nav
May 27, 2026
Merged

feat: client-side navigation for auth state transitions#45
amrtgaber merged 1 commit into
mainfrom
feat/client-side-auth-nav

Conversation

@amrtgaber

Copy link
Copy Markdown
Contributor

Closes #43. Replaces 6 `window.location.href` calls with TanStack Router's `useNavigate` across login, register, logout, OAuth callback, profile delete, and associate-complete. No more white flash between auth state changes.

The race that wasn't

`login-page.tsx:36-37` previously documented:

Hard redirect — reloads the page so AuthProvider picks up the new cookie cleanly. Client-side navigate has a race condition with the onUnauthorized handler.

Live E2E testing shows the race does not manifest with `await auth.checkAuth() + await navigate()`. React 18's batching plus TanStack Router's context propagation (via `<RouterProvider context={{ auth }} />` in `main.tsx:105`) means the updated context is visible by the time `beforeLoad` runs. No `router.invalidate()` needed. The previous comment was likely about pre-React-18 batching or a different flow.

The pattern by call site

Call site Pattern Why
Login / register `await auth.checkAuth() → await navigate(target)` Refresh AuthContext with the new cookie's identity before the destination's `beforeLoad` checks `isAuthenticated`
Logout (navbar) / delete (profile) `await auth.logout() → await navigate("/login")` Clear local state so /login's "redirect authenticated → /profile" guard doesn't bounce the user back
OAuth-complete Direct `api.get("auth/me")` for closure-local routing + `await auth.checkAuth()` for context refresh + `await navigate(target)` Need user data for the accept-terms / profile routing decision AND a fresh AuthContext for the destination
Associate-complete Just `await navigate({ to: "/profile", search: { linked } })` No auth state change — user is already logged in

Preserved hard-nav cases

  • Cross-origin redirects (consumer apps like `hera-streamer-…`) still use `window.location.href` since useNavigate is for in-app routes only. `login-page.tsx` and `oauth-complete-page.tsx` both check `target.startsWith("http")` and fall back.
  • Outbound to provider OAuth (Google / Steam authorize) keeps direct navigation — that's the fix from fix: navigate directly to Google authorize endpoints #44.

Test plan

Unit

  • `pnpm test:run` — 45 tests pass.
  • `pnpm exec tsc -b` clean.

Live E2E (Playwright against local API + Postgres on non-default ports)

Flow Result Backend log
Register → /profile ✅ no bounce, no flash register/login/accept-tos/me/connections all 2xx
Logout (navbar) → /login ✅ clean jwt/logout → 204
Login → /profile ✅ no bounce, no flash jwt/login → 204, me → 200
Delete account → /login ✅ no bounce-back to deleted /profile DELETE me → 204, jwt/logout → 204

Not covered E2E

OAuth callback complete paths (Google / Steam). Would need real OAuth client creds in the local API; the conversion mirrors login (await checkAuth + await navigate) which is verified.

Reviewer checklist

  • `pnpm test:run`
  • Locally: log in via email → /profile should appear instantly without a full reload (compare to current main where there's a ~200ms blank-page flash).
  • Logout from the navbar → /login should appear instantly.
  • Delete an account → /login (the trickiest case — pre-fix this could bounce back to /profile if the auth state hadn't cleared in time).

Replaces 6 `window.location.href` calls with TanStack Router's
`useNavigate` across login, register, logout, oauth callback, profile
delete, and associate-complete. No more white flash between auth state
changes.

## The race that wasn't

`login-page.tsx:36-37` previously documented:
> Hard redirect — reloads the page so AuthProvider picks up the new
> cookie cleanly. Client-side navigate has a race condition with the
> onUnauthorized handler.

E2E testing (Playwright against real API + Postgres) shows the race
does NOT manifest with `await auth.checkAuth() + await navigate()`:
React 18's batching plus TanStack Router's context propagation (via
`<RouterProvider context={{ auth }} />` in `main.tsx:105`) means the
updated context is visible by the time `beforeLoad` runs. No
`router.invalidate()` needed.

The pattern by call site:

- **Login / register**: `await auth.checkAuth() → await navigate(...)`.
  Refreshes the AuthContext with the new cookie's identity before the
  destination's beforeLoad checks `isAuthenticated`.
- **Logout / delete**: `await auth.logout() → await navigate("/login")`.
  Clears local state before navigating so /login's "redirect
  authenticated users to /profile" guard doesn't bounce the user back.
- **OAuth-complete**: keeps a direct `api.get("auth/me")` (so the
  routing decision sees current data without waiting for a render)
  PLUS `await auth.checkAuth()` (so the AuthContext is fresh for the
  destination route).
- **Associate-complete**: pure in-app nav with no auth state change.

## Preserved hard-nav cases

- Cross-origin redirects (consumer apps like hera-streamer-…) still use
  `window.location.href` — useNavigate is for in-app routes only.
- Outbound to provider OAuth (Google / Steam authorize) keeps direct
  navigation (CSP + the dev/prod JSON-vs-302 split, fixed in #44).

## Test plan

- [x] `pnpm test:run` — 45 tests pass.
- [x] `pnpm exec tsc -b` clean.
- [x] Live E2E: register → /profile, logout → /login, login → /profile,
  delete → /login. No bounce, no white flash. Backend log confirms
  request chain (register/login/accept-tos/me/connections; logout;
  delete/logout).

Closes #43.
@netlify

netlify Bot commented May 27, 2026

Copy link
Copy Markdown

Deploy Preview for criticalbit-auth-web ready!

Name Link
🔨 Latest commit b4d7dac
🔍 Latest deploy log https://app.netlify.com/projects/criticalbit-auth-web/deploys/6a1672ca81e2bb00072bb513
😎 Deploy Preview https://deploy-preview-45--criticalbit-auth-web.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@amrtgaber
amrtgaber merged commit 13ccaf7 into main May 27, 2026
6 checks passed
@amrtgaber
amrtgaber deleted the feat/client-side-auth-nav branch May 27, 2026 04:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace hard-reload auth navigation with client-side transitions

1 participant