From 31e46ed8c8af4d50d59b62de9fa9f56812a2f129 Mon Sep 17 00:00:00 2001 From: Theo Zourzouvillys Date: Mon, 31 Aug 2026 05:51:40 -0800 Subject: [PATCH] fix(ui): clear the invitation ticket when a sign-in completes on a challenge A ticket sign-in that would have completed on the start page completes on the challenge instead, which did not clear the ticket from the URL. Co-authored-by: Claude Opus 5 (1M context) --- .changeset/clear-ticket-after-challenge.md | 5 ++++ .../components/SignIn/SignInProtectCheck.tsx | 4 ++++ .../__tests__/SignInProtectCheck.test.tsx | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 .changeset/clear-ticket-after-challenge.md diff --git a/.changeset/clear-ticket-after-challenge.md b/.changeset/clear-ticket-after-challenge.md new file mode 100644 index 00000000000..dcd7bd64d48 --- /dev/null +++ b/.changeset/clear-ticket-after-challenge.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Clear the invitation ticket from the URL when a sign-in completes on a verification challenge. diff --git a/packages/ui/src/components/SignIn/SignInProtectCheck.tsx b/packages/ui/src/components/SignIn/SignInProtectCheck.tsx index 2c826535ddd..f43276252a2 100644 --- a/packages/ui/src/components/SignIn/SignInProtectCheck.tsx +++ b/packages/ui/src/components/SignIn/SignInProtectCheck.tsx @@ -1,3 +1,4 @@ +import { removeClerkQueryParam } from '@clerk/shared/internal/clerk-js/queryParams'; import { useClerk } from '@clerk/shared/react'; import type { SignInResource } from '@clerk/shared/types'; import { useEffect, useRef, useState } from 'react'; @@ -72,6 +73,9 @@ function SignInProtectCheckInternal(): JSX.Element | null { return; } if (updatedSignIn.status === 'complete' && updatedSignIn.createdSessionId) { + // A ticket sign-in that would have completed on the start page is completing here + // instead, so the ticket has to be cleared here too — otherwise it stays in the URL. + removeClerkQueryParam('__clerk_ticket'); await setActive({ session: updatedSignIn.createdSessionId, navigate: async ({ session, decorateUrl }) => { diff --git a/packages/ui/src/components/SignIn/__tests__/SignInProtectCheck.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInProtectCheck.test.tsx index 6367f77b1f2..645d5ea645b 100644 --- a/packages/ui/src/components/SignIn/__tests__/SignInProtectCheck.test.tsx +++ b/packages/ui/src/components/SignIn/__tests__/SignInProtectCheck.test.tsx @@ -23,6 +23,29 @@ beforeEach(() => { }); describe('SignInProtectCheck', () => { + it('clears the invitation ticket when the sign-in completes on the challenge', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.startSignInWithProtectCheck(); + }); + const url = new URL(window.location.href); + url.searchParams.set('__clerk_ticket', 'tkt_abc'); + window.history.replaceState({}, '', url.toString()); + + mockExecute.mockResolvedValue('proof-abc'); + fixtures.signIn.submitProtectCheck.mockResolvedValue({ + status: 'complete', + protectCheck: null, + createdSessionId: 'sess_1', + } as unknown as SignInResource); + + render(, { wrapper }); + + await waitFor(() => { + expect(fixtures.clerk.setActive).toHaveBeenCalled(); + }); + expect(new URL(window.location.href).searchParams.get('__clerk_ticket')).toBeNull(); + }); + describe('enterprise SSO', () => { const enterpriseSSOSignIn = (supportedFirstFactors: unknown[]) => ({