From 6d38bec99d341ef4d90bea7e12e4a4f7d64a61a7 Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Mon, 27 Jul 2026 13:08:14 -0500
Subject: [PATCH 1/2] fix(ui): remove ticket parameters on sign up continue
---
.changeset/quiet-tickets-continue.md | 5 ++
.../src/components/SignUp/SignUpContinue.tsx | 10 ++-
.../SignUp/__tests__/SignUpStart.test.tsx | 72 +++++++++++++++++++
3 files changed, 84 insertions(+), 3 deletions(-)
create mode 100644 .changeset/quiet-tickets-continue.md
diff --git a/.changeset/quiet-tickets-continue.md b/.changeset/quiet-tickets-continue.md
new file mode 100644
index 00000000000..5e8cd27ec63
--- /dev/null
+++ b/.changeset/quiet-tickets-continue.md
@@ -0,0 +1,5 @@
+---
+'@clerk/ui': patch
+---
+
+Fix an issue where Clerk's ticket query parameters were not removed from the URL when completing a sign-up that was missing requirements.
diff --git a/packages/ui/src/components/SignUp/SignUpContinue.tsx b/packages/ui/src/components/SignUp/SignUpContinue.tsx
index 2777d63324b..93cfb3f63ef 100644
--- a/packages/ui/src/components/SignUp/SignUpContinue.tsx
+++ b/packages/ui/src/components/SignUp/SignUpContinue.tsx
@@ -1,3 +1,4 @@
+import { removeClerkQueryParam } from '@clerk/shared/internal/clerk-js/queryParams';
import { useClerk } from '@clerk/shared/react';
import React, { useEffect, useMemo } from 'react';
@@ -180,13 +181,16 @@ function SignUpContinueInternal() {
verifyEmailPath: './verify-email-address',
verifyPhonePath: './verify-phone-number',
protectCheckPath: '../protect-check',
- handleComplete: () =>
- clerk.setActive({
+ handleComplete: () => {
+ removeClerkQueryParam('__clerk_ticket');
+ removeClerkQueryParam('__clerk_invitation_token');
+ return clerk.setActive({
session: res.createdSessionId,
navigate: async ({ session, decorateUrl }) => {
await navigateOnSetActive({ session, redirectUrl: afterSignUpUrl, decorateUrl });
},
- }),
+ });
+ },
navigate,
oidcPrompt: ctx.oidcPrompt,
}),
diff --git a/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx b/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx
index 73807b7968d..eb5fb3f4739 100644
--- a/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx
+++ b/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx
@@ -10,6 +10,7 @@ import { CardStateProvider } from '@/ui/elements/contexts';
import { OptionsProvider } from '../../../contexts';
import { AppearanceProvider } from '../../../customizables';
+import { SignUpContinue } from '../SignUpContinue';
import { SignUpStart } from '../SignUpStart';
const { createFixtures } = bindCreateFixtures('SignUp');
@@ -497,6 +498,77 @@ describe('SignUpStart', () => {
);
});
+ it('removes the ticket before setting the session active after continuing the sign up', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withEmailAddress({ required: true });
+ f.withPassword({ required: true });
+ f.startSignUpWithEmailAddress({ emailVerificationStatus: 'verified' });
+ });
+ let currentUrl = new URL('http://localhost/sign-up?__clerk_ticket=test_ticket');
+ let ticketAtSetActive: string | null | undefined;
+
+ Object.defineProperty(window, 'location', {
+ configurable: true,
+ value: {
+ get href() {
+ return currentUrl.href;
+ },
+ get search() {
+ return currentUrl.search;
+ },
+ },
+ });
+ Object.defineProperty(window, 'history', {
+ configurable: true,
+ value: {
+ state: undefined,
+ replaceState: vi.fn((_state, _title, url) => {
+ currentUrl = new URL(url, currentUrl);
+ }),
+ },
+ });
+
+ fixtures.signUp.create.mockResolvedValueOnce(fixtures.signUp as SignUpResource);
+ fixtures.router.navigate.mockImplementation((to, options) => {
+ currentUrl = new URL(`/sign-up/${to}`, currentUrl);
+ currentUrl.search = options?.searchParams?.toString() || '';
+ return Promise.resolve(true);
+ });
+ fixtures.signUp.update.mockImplementationOnce(() => {
+ fixtures.signUp.status = 'complete';
+ fixtures.signUp.createdSessionId = 'sess_ticket';
+ return Promise.resolve(fixtures.signUp);
+ });
+ fixtures.clerk.setActive.mockImplementationOnce(() => {
+ ticketAtSetActive = new URL(window.location.href).searchParams.get('__clerk_ticket');
+ return Promise.resolve();
+ });
+
+ const { rerender, userEvent } = render(, { wrapper });
+
+ await waitFor(() =>
+ expect(fixtures.signUp.create).toHaveBeenCalledWith({
+ strategy: 'ticket',
+ ticket: 'test_ticket',
+ unsafeMetadata: undefined,
+ }),
+ );
+ await waitFor(() =>
+ expect(fixtures.router.navigate).toHaveBeenCalledWith('continue', {
+ searchParams: new URLSearchParams('__clerk_ticket=test_ticket'),
+ }),
+ );
+ expect(currentUrl.href).toBe('http://localhost/sign-up/continue?__clerk_ticket=test_ticket');
+
+ rerender();
+ await userEvent.type(screen.getByLabelText('Password'), 'a-secure-password');
+ await userEvent.click(screen.getByText('Continue'));
+
+ await waitFor(() => expect(fixtures.signUp.update).toHaveBeenCalled());
+ await waitFor(() => expect(fixtures.clerk.setActive).toHaveBeenCalled());
+ expect(ticketAtSetActive).toBeNull();
+ });
+
it('should show the sign up form when ticket detected and mode is restricted', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withEmailAddress();
From b6716cf0773dbdcc852c97c2f66aed132ad02a71 Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Mon, 27 Jul 2026 13:36:48 -0500
Subject: [PATCH 2/2] fix: remove ticket parameters in handleComplete
---
.../__tests__/completeSignUpFlow.test.ts | 46 +++++++++++++++++++
.../internal/clerk-js/completeSignUpFlow.ts | 4 +-
.../ui/src/components/SignUp/SignUpStart.tsx | 4 +-
3 files changed, 50 insertions(+), 4 deletions(-)
diff --git a/packages/shared/src/internal/clerk-js/__tests__/completeSignUpFlow.test.ts b/packages/shared/src/internal/clerk-js/__tests__/completeSignUpFlow.test.ts
index d13513a7c04..df15c09b583 100644
--- a/packages/shared/src/internal/clerk-js/__tests__/completeSignUpFlow.test.ts
+++ b/packages/shared/src/internal/clerk-js/__tests__/completeSignUpFlow.test.ts
@@ -31,6 +31,52 @@ describe('completeSignUpFlow', () => {
expect(mockNavigate).not.toHaveBeenCalled();
});
+ it('removes ticket query parameters before calling handleComplete', async () => {
+ const mockSignUp = {
+ status: 'complete',
+ missingFields: [] as SignUpField[],
+ } as SignUpResource;
+ let currentUrl = new URL(
+ 'http://localhost/sign-up/continue?__clerk_ticket=test_ticket&__clerk_invitation_token=test_invitation',
+ );
+ let ticketAtHandleComplete: string | null | undefined;
+ let invitationAtHandleComplete: string | null | undefined;
+
+ Object.defineProperty(window, 'location', {
+ configurable: true,
+ value: {
+ get href() {
+ return currentUrl.href;
+ },
+ get search() {
+ return currentUrl.search;
+ },
+ },
+ });
+ Object.defineProperty(window, 'history', {
+ configurable: true,
+ value: {
+ state: undefined,
+ replaceState: vi.fn((_state, _title, url) => {
+ currentUrl = new URL(url, currentUrl);
+ }),
+ },
+ });
+ mockHandleComplete.mockImplementationOnce(() => {
+ ticketAtHandleComplete = currentUrl.searchParams.get('__clerk_ticket');
+ invitationAtHandleComplete = currentUrl.searchParams.get('__clerk_invitation_token');
+ });
+
+ await completeSignUpFlow({
+ signUp: mockSignUp,
+ handleComplete: mockHandleComplete,
+ navigate: mockNavigate,
+ });
+
+ expect(ticketAtHandleComplete).toBeNull();
+ expect(invitationAtHandleComplete).toBeNull();
+ });
+
it('navigates to verify email page if email still unverified', async () => {
const mockSignUp = {
status: 'missing_requirements',
diff --git a/packages/shared/src/internal/clerk-js/completeSignUpFlow.ts b/packages/shared/src/internal/clerk-js/completeSignUpFlow.ts
index bc019ed150a..d2bae881512 100644
--- a/packages/shared/src/internal/clerk-js/completeSignUpFlow.ts
+++ b/packages/shared/src/internal/clerk-js/completeSignUpFlow.ts
@@ -1,5 +1,5 @@
import type { SignUpResource } from '../../types';
-import { forwardClerkQueryParams } from './queryParams';
+import { forwardClerkQueryParams, removeClerkQueryParam } from './queryParams';
type CompleteSignUpFlowProps = {
signUp: SignUpResource;
@@ -27,6 +27,8 @@ export const completeSignUpFlow = ({
oidcPrompt,
}: CompleteSignUpFlowProps): Promise | undefined => {
if (signUp.status === 'complete') {
+ removeClerkQueryParam('__clerk_ticket');
+ removeClerkQueryParam('__clerk_invitation_token');
return handleComplete && handleComplete();
} else if (signUp.status === 'missing_requirements') {
if (signUp.missingFields.some(mf => mf === 'enterprise_sso')) {
diff --git a/packages/ui/src/components/SignUp/SignUpStart.tsx b/packages/ui/src/components/SignUp/SignUpStart.tsx
index 8eba3452453..3b2e4a125c6 100644
--- a/packages/ui/src/components/SignUp/SignUpStart.tsx
+++ b/packages/ui/src/components/SignUp/SignUpStart.tsx
@@ -1,7 +1,7 @@
import { getAlternativePhoneCodeProviderData } from '@clerk/shared/alternativePhoneCode';
import { isClerkAPIResponseError } from '@clerk/shared/error';
import { ERROR_CODES, SIGN_UP_MODES } from '@clerk/shared/internal/clerk-js/constants';
-import { getClerkQueryParam, removeClerkQueryParam } from '@clerk/shared/internal/clerk-js/queryParams';
+import { getClerkQueryParam } from '@clerk/shared/internal/clerk-js/queryParams';
import { useClerk } from '@clerk/shared/react';
import type { PhoneCodeChannel, PhoneCodeChannelData, SignUpResource } from '@clerk/shared/types';
import React from 'react';
@@ -170,8 +170,6 @@ function SignUpStartInternal(): JSX.Element {
protectCheckPath: 'protect-check',
continuePath: 'continue',
handleComplete: () => {
- removeClerkQueryParam('__clerk_ticket');
- removeClerkQueryParam('__clerk_invitation_token');
return setActive({
session: signUp.createdSessionId,
navigate: async ({ session, decorateUrl }) => {