From dbe8d4491838a427fc1c95287722834ae601186d Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 30 Oct 2026 17:38:06 +0530 Subject: [PATCH 1/2] fix(claim-db-worker): start one sign-in per claim The claim button opened sign-in in a new tab and was enabled again right after, so a second click started a second sign-in. For a user who has already consented, both tabs return to the callback at once and both run the transfer: one moves the project, the other finds it gone and shows Transfer Failed for a claim that worked. Production shows 7 such pairs of 404 and 204 transfers in the last 7 days. Sign in in the same tab, and keep the button disabled until the page navigates away; it is enabled again only if getting the sign-in URL fails. --- apps/claim-db-worker/__tests__/claim.test.tsx | 42 ++++++++++++------- apps/claim-db-worker/app/claim/page.tsx | 11 +++-- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/apps/claim-db-worker/__tests__/claim.test.tsx b/apps/claim-db-worker/__tests__/claim.test.tsx index 1fe4760..d525690 100644 --- a/apps/claim-db-worker/__tests__/claim.test.tsx +++ b/apps/claim-db-worker/__tests__/claim.test.tsx @@ -5,16 +5,16 @@ import { useSearchParams, useRouter, usePathname } from "next/navigation"; import ClaimPage from "../app/claim/page"; // This test does not check the actual claim flow, but rather the claim page functionality and claim flow logic. -// It mocks the fetch API and window.open to simulate the claim flow, +// It mocks the fetch API and window.location to simulate the claim flow, // so long as the Management API is functioning properly, this should be as well. const mockFetch = vi.fn(); global.fetch = mockFetch; -const mockWindowOpen = vi.fn(); -Object.defineProperty(window, "open", { +const mockAssign = vi.fn(); +Object.defineProperty(window, "location", { writable: true, - value: mockWindowOpen, + value: { ...window.location, origin: "http://localhost:3000", assign: mockAssign }, }); vi.mock("next/navigation", () => ({ @@ -55,7 +55,7 @@ describe("claim page", () => { }); describe("functionality", () => { - it("completes full claim flow when claim button is clicked", async () => { + it("sends the browser to sign in and keeps the button disabled", async () => { mockSearchParams.set("projectID", "test-project-123"); render(); @@ -80,15 +80,26 @@ describe("claim page", () => { }); await waitFor(() => { - expect(mockWindowOpen).toHaveBeenCalledWith( - "https://auth.example.com/callback?code=test", - "_blank" + expect(mockAssign).toHaveBeenCalledWith( + "https://auth.example.com/callback?code=test" ); }); - await waitFor(() => { - expect(claimButton).not.toBeDisabled(); - }); + expect(claimButton).toBeDisabled(); + }); + + it("starts one claim however often the button is clicked", async () => { + mockSearchParams.set("projectID", "test-project-123"); + render(); + + const claimButton = screen.getByRole("button", { name: "Claim database" }); + fireEvent.click(claimButton); + await waitFor(() => expect(mockAssign).toHaveBeenCalledTimes(1)); + fireEvent.click(claimButton); + fireEvent.click(claimButton); + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockAssign).toHaveBeenCalledTimes(1); }); it("handles auth endpoint errors gracefully", async () => { @@ -111,7 +122,7 @@ describe("claim page", () => { expect(consoleSpy).toHaveBeenCalledWith("Error:", expect.any(Error)); }); - expect(mockWindowOpen).not.toHaveBeenCalled(); + expect(mockAssign).not.toHaveBeenCalled(); await waitFor(() => { expect(claimButton).not.toBeDisabled(); @@ -155,13 +166,12 @@ describe("claim page", () => { }); await waitFor(() => { - expect(mockWindowOpen).toHaveBeenCalledWith( - expect.stringContaining("test-project-123"), - "_blank" + expect(mockAssign).toHaveBeenCalledWith( + expect.stringContaining("test-project-123") ); }); - expect(claimButton).not.toBeDisabled(); + expect(claimButton).toBeDisabled(); }); }); }); diff --git a/apps/claim-db-worker/app/claim/page.tsx b/apps/claim-db-worker/app/claim/page.tsx index fea7e3a..4026df9 100644 --- a/apps/claim-db-worker/app/claim/page.tsx +++ b/apps/claim-db-worker/app/claim/page.tsx @@ -47,13 +47,16 @@ function ClaimContent() { throw new Error("Failed to get auth URL"); } - const data = (await response.json()) as { authUrl: string }; - if (data.authUrl) { - window.open(data.authUrl, "_blank"); + const data = (await response.json()) as { authUrl?: string }; + if (!data.authUrl) { + throw new Error("Missing auth URL"); } + // Sign in in this tab, with the button left disabled: two sign-ins for + // one database run two transfers at once, and the one that loses shows + // "Transfer Failed" for a claim that worked. + window.location.assign(data.authUrl); } catch (error) { console.error("Error:", error); - } finally { setIsLoading(false); } }; From 631cca659eaf8a746f8dbf22c4df3a181f89644e Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sat, 31 Oct 2026 22:21:01 +0530 Subject: [PATCH 2/2] fix(claim-db-worker): enable the claim button again after back from sign-in --- apps/claim-db-worker/__tests__/claim.test.tsx | 13 +++++++++++++ apps/claim-db-worker/app/claim/page.tsx | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/apps/claim-db-worker/__tests__/claim.test.tsx b/apps/claim-db-worker/__tests__/claim.test.tsx index d525690..c1e3eff 100644 --- a/apps/claim-db-worker/__tests__/claim.test.tsx +++ b/apps/claim-db-worker/__tests__/claim.test.tsx @@ -102,6 +102,19 @@ describe("claim page", () => { expect(mockAssign).toHaveBeenCalledTimes(1); }); + it("enables the button again when the browser comes back from sign-in", async () => { + mockSearchParams.set("projectID", "test-project-123"); + render(); + + const claimButton = screen.getByRole("button", { name: "Claim database" }); + fireEvent.click(claimButton); + await waitFor(() => expect(claimButton).toBeDisabled()); + + fireEvent(window, new PageTransitionEvent("pageshow", { persisted: true })); + + await waitFor(() => expect(claimButton).not.toBeDisabled()); + }); + it("handles auth endpoint errors gracefully", async () => { const consoleSpy = vi .spyOn(console, "error") diff --git a/apps/claim-db-worker/app/claim/page.tsx b/apps/claim-db-worker/app/claim/page.tsx index 4026df9..d558a73 100644 --- a/apps/claim-db-worker/app/claim/page.tsx +++ b/apps/claim-db-worker/app/claim/page.tsx @@ -21,6 +21,18 @@ function ClaimContent() { } }, [pathname, projectID, router]); + // Back from sign-in restores this page from the bfcache with the button + // still disabled. + useEffect(() => { + const handlePageShow = (event: PageTransitionEvent) => { + if (event.persisted) { + setIsLoading(false); + } + }; + window.addEventListener("pageshow", handlePageShow); + return () => window.removeEventListener("pageshow", handlePageShow); + }, []); + if (!projectID && !pathname.includes("/test/")) { return null; }