From ae005f020b7cdf0992946291b2f98e43e734c9aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 12 Nov 2026 14:19:47 +0100 Subject: [PATCH] refactor(auth): navigate for single sign-on at the call sites again Moving the navigation into the shared helper took both sign-in hand-offs out of what the headerless-transport guard walks, which only reads the component and page trees. The helper now only clears the offline copies with the capped wait, and each page navigates itself. The device registration test checks the end of the no-device rest inside the test that already pairs an iPhone instead of a copy of it. --- src/app/api/devices/__tests__/route.test.ts | 15 +-------------- src/app/auth/login/page.tsx | 6 ++++-- src/components/auth/claim-profile.tsx | 7 +++++-- src/lib/auth/leave-for-single-sign-on.ts | 14 -------------- src/lib/pwa/clear-before-leaving.ts | 12 ++++++++++++ 5 files changed, 22 insertions(+), 32 deletions(-) delete mode 100644 src/lib/auth/leave-for-single-sign-on.ts create mode 100644 src/lib/pwa/clear-before-leaving.ts diff --git a/src/app/api/devices/__tests__/route.test.ts b/src/app/api/devices/__tests__/route.test.ts index d82648c3b..220b605c9 100644 --- a/src/app/api/devices/__tests__/route.test.ts +++ b/src/app/api/devices/__tests__/route.test.ts @@ -221,21 +221,8 @@ describe("POST /api/devices", () => { }; expect(args.data.apnsToken).toBe("deadbeef".repeat(8)); expect(args.data.apnsEnvironment).toBe("sandbox"); - }); - - it("ends a pause that was waiting for a paired iPhone", async () => { // A reminder with no iPhone paired rests the APNs channel for an hour; - // pairing one must not leave the next reminder waiting it out. - vi.mocked(getSession).mockResolvedValue(SESSION_OK as never); - const res = await POST( - req({ - token: "abcd1234efgh5678", - bundleId: "io.healthlog.app", - apnsToken: "deadbeef".repeat(8), - apnsEnvironment: "sandbox", - }), - ); - expect(res.status).toBe(201); + // pairing one ends that rest, so the next reminder does not wait it out. expect(prisma.notificationChannel.updateMany).toHaveBeenCalledWith({ where: { userId: "user-1", diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 89eae5a58..579209b0d 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -24,7 +24,7 @@ import { OIDC_MFA_COOKIE, OIDC_MFA_COOKIE_PATH } from "@/lib/auth/oidc-cookie"; import { isDashboardSnapshotEnabled } from "@/lib/dashboard/snapshot-flag"; import { prefetchDashboardSnapshot } from "@/lib/queries/use-dashboard-snapshot"; import { clearOfflineCachesForSessionEnd } from "@/lib/pwa/query-persister"; -import { leaveForSingleSignOn } from "@/lib/auth/leave-for-single-sign-on"; +import { clearOfflineCachesBeforeLeaving } from "@/lib/pwa/clear-before-leaving"; import { sanitizeSameOriginPath } from "@/lib/url-safety"; import { localizedSignInError } from "@/lib/api/localized-error"; import { formatDate } from "@/lib/format"; @@ -184,7 +184,9 @@ export default function LoginPage() { params.set("client_proto", clientTransport.protocol); params.set("client_host", clientTransport.host); } - await leaveForSingleSignOn(`/api/auth/oidc/login?${params}`); + // The session starts on the way back; the last account's copies go first. + await clearOfflineCachesBeforeLeaving(); + loadDocument(`/api/auth/oidc/login?${params}`); } function getRedirectTarget(): string { diff --git a/src/components/auth/claim-profile.tsx b/src/components/auth/claim-profile.tsx index 695bf2ef5..360d5a1db 100644 --- a/src/components/auth/claim-profile.tsx +++ b/src/components/auth/claim-profile.tsx @@ -21,7 +21,8 @@ import { import { ApiError } from "@/lib/api/api-fetch"; import { showErrorToast } from "@/lib/errors/error-toast"; import { useFormatters, useTranslations } from "@/lib/i18n/context"; -import { leaveForSingleSignOn } from "@/lib/auth/leave-for-single-sign-on"; +import { loadDocument } from "@/lib/navigation/load-document"; +import { clearOfflineCachesBeforeLeaving } from "@/lib/pwa/clear-before-leaving"; import { useClaimProfile, useClaimProfileSso, @@ -110,7 +111,9 @@ export function ClaimProfile({ next.searchParams.set("client_proto", clientTransport.protocol); next.searchParams.set("client_host", clientTransport.host); } - await leaveForSingleSignOn(`${next.pathname}${next.search}`); + // The session starts on the way back; the last account's copies go first. + await clearOfflineCachesBeforeLeaving(); + loadDocument(`${next.pathname}${next.search}`); }, }, ); diff --git a/src/lib/auth/leave-for-single-sign-on.ts b/src/lib/auth/leave-for-single-sign-on.ts deleted file mode 100644 index 07b2bf363..000000000 --- a/src/lib/auth/leave-for-single-sign-on.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { settleWithin } from "@/lib/async/settle-within"; -import { loadDocument } from "@/lib/navigation/load-document"; -import { clearOfflineCachesForSessionEnd } from "@/lib/pwa/query-persister"; - -/** - * Leave for the identity provider. The session starts on the way back, so - * the last account's offline copies go first. The wait for them is capped: - * WebKit can leave an IndexedDB open hanging after the installed app returns - * from the background, and the button must still work then. - */ -export async function leaveForSingleSignOn(url: string): Promise { - await settleWithin(clearOfflineCachesForSessionEnd(), 1_500, undefined); - loadDocument(url); -} diff --git a/src/lib/pwa/clear-before-leaving.ts b/src/lib/pwa/clear-before-leaving.ts new file mode 100644 index 000000000..460114f1c --- /dev/null +++ b/src/lib/pwa/clear-before-leaving.ts @@ -0,0 +1,12 @@ +import { settleWithin } from "@/lib/async/settle-within"; +import { clearOfflineCachesForSessionEnd } from "@/lib/pwa/query-persister"; + +/** + * The session-end wipe before the page leaves for the identity provider, + * where the next session starts. The wait is capped: WebKit can leave an + * IndexedDB open hanging after the installed app returns from the + * background, and the button must still work then. + */ +export function clearOfflineCachesBeforeLeaving(): Promise { + return settleWithin(clearOfflineCachesForSessionEnd(), 1_500, undefined); +}