From 48bfc1f9875e5cc96e17af38dc5176f1516dc7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 24 Sep 2026 10:37:21 +0200 Subject: [PATCH] refactor(navigation): name the full-document loads the client router cannot serve Nine places assigned window.location.href directly: OAuth connects and sign-in hand-offs that land on API routes answering with a redirect, and the reload after account deletion. The newer Next lint rule reads each as an in-app page change. They now go through one helper that says why a document load is the right tool there. --- src/app/auth/login/page.tsx | 5 +++-- src/components/settings/advanced-section.tsx | 3 ++- .../settings/integrations/fitbit-card.tsx | 3 ++- .../settings/integrations/google-health-card.tsx | 5 +++-- .../settings/integrations/oauth-provider-card.tsx | 3 ++- src/components/settings/integrations/whoop-card.tsx | 3 ++- .../settings/integrations/withings-card.tsx | 3 ++- src/lib/navigation/load-document.ts | 13 +++++++++++++ 8 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 src/lib/navigation/load-document.ts diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 2ece6eabc..b6f739670 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -25,6 +25,7 @@ import { isDashboardSnapshotEnabled } from "@/lib/dashboard/snapshot-flag"; import { prefetchDashboardSnapshot } from "@/lib/queries/use-dashboard-snapshot"; import { clearOfflineCachesForSessionEnd } from "@/lib/pwa/query-persister"; import { sanitizeSameOriginPath } from "@/lib/url-safety"; +import { loadDocument } from "@/lib/navigation/load-document"; export default function LoginPage() { const router = useRouter(); @@ -97,7 +98,7 @@ export default function LoginPage() { function handleOidcLogin() { const params = new URLSearchParams({ next: getRedirectTarget() }); - window.location.href = `/api/auth/oidc/login?${params}`; + loadDocument(`/api/auth/oidc/login?${params}`); } function getRedirectTarget(): string { @@ -126,7 +127,7 @@ export default function LoginPage() { // page always requires a fresh interactive login here (no ambient one-click // pass), which is exactly what the completion route's freshness rule needs. if (isNativeHandoff) { - window.location.href = "/api/auth/native/complete"; + loadDocument("/api/auth/native/complete"); return; } // Start the new session from an empty in-memory cache. The root diff --git a/src/components/settings/advanced-section.tsx b/src/components/settings/advanced-section.tsx index 4c55d52e3..6eef552e2 100644 --- a/src/components/settings/advanced-section.tsx +++ b/src/components/settings/advanced-section.tsx @@ -21,6 +21,7 @@ import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { useTranslations } from "@/lib/i18n/context"; import { apiFetchRaw } from "@/lib/api/api-fetch"; +import { loadDocument } from "@/lib/navigation/load-document"; /** * Settings → Advanced. @@ -225,7 +226,7 @@ function AccountDeleteCard() { // Leave `deleting` set so the confirm button keeps its pending // state through the redirect — the row is gone, nothing to undo. setTimeout(() => { - window.location.href = "/auth/login"; + loadDocument("/auth/login"); }, 1_500); } catch { setMsg(t("settings.deleteAccountFailed")); diff --git a/src/components/settings/integrations/fitbit-card.tsx b/src/components/settings/integrations/fitbit-card.tsx index 2d16a8f7a..8d61cb3ad 100644 --- a/src/components/settings/integrations/fitbit-card.tsx +++ b/src/components/settings/integrations/fitbit-card.tsx @@ -70,6 +70,7 @@ import { IntegrationRedirectGuide, } from "./setup-guide-link"; import type { IntegrationCallbackUrls } from "@/lib/integrations/callback-urls"; +import { loadDocument } from "@/lib/navigation/load-document"; export function FitbitCard({ viewModel, @@ -511,7 +512,7 @@ export function FitbitCard({ className="min-h-11 w-full sm:w-auto" data-testid="fitbit-connect" onClick={() => { - window.location.href = "/api/fitbit/connect"; + loadDocument("/api/fitbit/connect"); }} > diff --git a/src/components/settings/integrations/google-health-card.tsx b/src/components/settings/integrations/google-health-card.tsx index 86ea06dc3..336848694 100644 --- a/src/components/settings/integrations/google-health-card.tsx +++ b/src/components/settings/integrations/google-health-card.tsx @@ -85,6 +85,7 @@ import { IntegrationCardDescription, } from "./setup-guide-link"; import type { IntegrationCallbackUrls } from "@/lib/integrations/callback-urls"; +import { loadDocument } from "@/lib/navigation/load-document"; export function GoogleHealthCard({ viewModel, @@ -443,7 +444,7 @@ export function GoogleHealthCard({ size="sm" variant="outline" onClick={() => { - window.location.href = "/api/google-health/connect"; + loadDocument("/api/google-health/connect"); }} data-testid="google-health-reconnect-button" className="min-h-11" @@ -749,7 +750,7 @@ export function GoogleHealthCard({ className="min-h-11 w-full sm:w-auto" data-testid="googlehealth-connect" onClick={() => { - window.location.href = "/api/google-health/connect"; + loadDocument("/api/google-health/connect"); }} > diff --git a/src/components/settings/integrations/oauth-provider-card.tsx b/src/components/settings/integrations/oauth-provider-card.tsx index 7435f0330..74d4bbad9 100644 --- a/src/components/settings/integrations/oauth-provider-card.tsx +++ b/src/components/settings/integrations/oauth-provider-card.tsx @@ -72,6 +72,7 @@ import { type IntegrationDocsProvider, } from "./setup-guide-link"; import type { IntegrationCallbackUrls } from "@/lib/integrations/callback-urls"; +import { loadDocument } from "@/lib/navigation/load-document"; export interface OAuthProviderStatus { connected: boolean; @@ -265,7 +266,7 @@ export function OAuthProviderCard({ function handleConnect() { setMsg(null); void invalidateKeys(queryClient, measurementDependentKeys); - window.location.href = `/api/${provider}/connect`; + loadDocument(`/api/${provider}/connect`); } return ( diff --git a/src/components/settings/integrations/whoop-card.tsx b/src/components/settings/integrations/whoop-card.tsx index 2b4fad121..ad4c04d8d 100644 --- a/src/components/settings/integrations/whoop-card.tsx +++ b/src/components/settings/integrations/whoop-card.tsx @@ -61,6 +61,7 @@ import { IntegrationRedirectGuide, } from "./setup-guide-link"; import type { IntegrationCallbackUrls } from "@/lib/integrations/callback-urls"; +import { loadDocument } from "@/lib/navigation/load-document"; export function WhoopCard({ viewModel, @@ -493,7 +494,7 @@ export function WhoopCard({ className="min-h-11 w-full sm:w-auto" data-testid="whoop-connect" onClick={() => { - window.location.href = "/api/whoop/connect"; + loadDocument("/api/whoop/connect"); }} > diff --git a/src/components/settings/integrations/withings-card.tsx b/src/components/settings/integrations/withings-card.tsx index ec8d61ce5..57111589a 100644 --- a/src/components/settings/integrations/withings-card.tsx +++ b/src/components/settings/integrations/withings-card.tsx @@ -60,6 +60,7 @@ import { IntegrationRedirectGuide, } from "./setup-guide-link"; import type { IntegrationCallbackUrls } from "@/lib/integrations/callback-urls"; +import { loadDocument } from "@/lib/navigation/load-document"; export function WithingsCard({ viewModel, @@ -542,7 +543,7 @@ export function WithingsCard({ className="min-h-11 w-full sm:w-auto" data-testid="withings-connect" onClick={() => { - window.location.href = "/api/withings/connect"; + loadDocument("/api/withings/connect"); }} > diff --git a/src/lib/navigation/load-document.ts b/src/lib/navigation/load-document.ts new file mode 100644 index 000000000..821d11ab2 --- /dev/null +++ b/src/lib/navigation/load-document.ts @@ -0,0 +1,13 @@ +/** + * Leave the client router and load `path` as a new document. + * + * For destinations the client router cannot serve: an API route that answers + * with a redirect (an OAuth connect, a sign-in hand-off), or a page that has + * to start from a fresh document because the client state of the old one no + * longer belongs to anybody (after the account was deleted). + * + * In-app page changes use `useRouter().push()` instead. + */ +export function loadDocument(path: string): void { + window.location.assign(path); +}