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);
+}