From b4d7dacc40cd8fab2d36021fa082f8058931dd82 Mon Sep 17 00:00:00 2001 From: Amr Gaber Date: Tue, 26 May 2026 23:27:12 -0500 Subject: [PATCH] feat: client-side navigation for auth state transitions (#43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `` 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. --- src/components/navbar.tsx | 4 +- .../oauth-associate-complete-page.tsx | 7 ++- src/pages/callback/oauth-complete-page.tsx | 51 ++++++++++++------- src/pages/login/login-page.tsx | 20 ++++++-- src/pages/profile/profile-page.tsx | 6 ++- src/pages/register/register-page.tsx | 10 +++- 6 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 1a386ab..e0ed4d5 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -1,4 +1,5 @@ import { ChevronDown, ExternalLink, LogOut } from "lucide-react" +import { useNavigate } from "@tanstack/react-router" import { BrandLockup } from "@/components/brand-lockup" import { DropdownMenu, @@ -13,6 +14,7 @@ import { useAuth } from "@/lib/auth" export function Navbar() { const auth = useAuth() + const navigate = useNavigate() return (