diff --git a/src/app/auth/login/__tests__/page.browser.test.tsx b/src/app/auth/login/__tests__/page.browser.test.tsx index c4b163fde..234cbcf7d 100644 --- a/src/app/auth/login/__tests__/page.browser.test.tsx +++ b/src/app/auth/login/__tests__/page.browser.test.tsx @@ -63,6 +63,21 @@ describe("sign-in page, single sign-on", () => { }); }); +describe("sign-in page, single sign-on with a stuck cache", () => { + it("still leaves for the identity provider when the wipe never settles", async () => { + // WebKit can leave an IndexedDB open hanging after the app returns from + // the background; the button did nothing then. + clearOffline.mockImplementationOnce(() => new Promise(() => {})); + const { screen } = await renderWithApp(, { user: null }); + await screen.getByRole("button", { name: "Example ID" }).click(); + + await expect + .poll(() => loadDocument.mock.calls.length, { timeout: 4000 }) + .toBe(1); + expect(clearOffline).toHaveBeenCalledTimes(1); + }); +}); + describe("sign-in page, password", () => { it("replaces itself with the page asked for", async () => { api.on("POST", "/api/auth/login", { data: { id: "signed-in" } }); diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index bcc84657b..3d5b3e9ed 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -24,6 +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 { settleWithin } from "@/lib/async/settle-within"; import { sanitizeSameOriginPath } from "@/lib/url-safety"; import { localizedSignInError } from "@/lib/api/localized-error"; import { formatDate } from "@/lib/format"; @@ -51,6 +52,9 @@ function deletionPendingMessage(t: Translate, purgeAt: unknown): string { : t("auth.deletionPendingNoDate"); } +/** How long the single-sign-on start waits for the offline copies to go. */ +const SSO_CLEANUP_WAIT_MS = 1_500; + /** The message for the `?error=` a sign-in redirect (the SSO callback) set. */ function signInRedirectError( searchParams: URLSearchParams, @@ -183,8 +187,14 @@ export default function LoginPage() { params.set("client_proto", clientTransport.protocol); params.set("client_host", clientTransport.host); } - // The session starts on the way back; the last account's copies go first. - await clearOfflineCachesForSessionEnd(); + // The session starts on the way back; the last account's copies go + // first. Bounded: WebKit can leave an IndexedDB open hanging after the + // app returns from the background, and the button must still work. + await settleWithin( + clearOfflineCachesForSessionEnd(), + SSO_CLEANUP_WAIT_MS, + undefined, + ); loadDocument(`/api/auth/oidc/login?${params}`); } diff --git a/src/components/auth/claim-profile.tsx b/src/components/auth/claim-profile.tsx index 540fa04ea..3ea40d5e6 100644 --- a/src/components/auth/claim-profile.tsx +++ b/src/components/auth/claim-profile.tsx @@ -23,12 +23,16 @@ import { showErrorToast } from "@/lib/errors/error-toast"; import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { loadDocument } from "@/lib/navigation/load-document"; import { clearOfflineCachesForSessionEnd } from "@/lib/pwa/query-persister"; +import { settleWithin } from "@/lib/async/settle-within"; import { useClaimProfile, useClaimProfileSso, useProfileClaimPreview, } from "@/lib/queries/use-profile-claim"; +/** How long the single-sign-on claim waits for the offline copies to go. */ +const SSO_CLEANUP_WAIT_MS = 1_500; + /** * v1.42 (#959) — the person a managed profile describes takes it over as * their own account (`/auth/claim?token=hlp_…`). @@ -111,8 +115,13 @@ export function ClaimProfile({ next.searchParams.set("client_proto", clientTransport.protocol); next.searchParams.set("client_host", clientTransport.host); } - // The session starts on the way back; the last account's copies go first. - await clearOfflineCachesForSessionEnd(); + // The session starts on the way back; the last account's copies + // go first, waited on no longer than on the sign-in page. + await settleWithin( + clearOfflineCachesForSessionEnd(), + SSO_CLEANUP_WAIT_MS, + undefined, + ); loadDocument(`${next.pathname}${next.search}`); }, },