fix(auth): leave for single sign-on even when the cache wipe hangs

The sign-in page and the profile take-over page waited for the offline
copies to be cleared before leaving for the identity provider, with no
limit. WebKit can leave an IndexedDB open unsettled after the installed
app returns from the background, and the button then did nothing. The
wait is now capped at one and a half seconds.
This commit is contained in:
Marc-André Bombeck committed 2026-11-12 14:03:46 +01:00
1 parent 444e1eaca8
commit 12a23bf779
3 files changed
+38 -4

No files matched your search

@@ -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(<LoginPage />, { 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" } });
+12 -2
View File
@@ -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}`);
}
+11 -2
View File
@@ -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}`);
},
},