From e0cfd77e5c57ab8f0a64833118242fe00cc2cff5 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 14 Sep 2026 13:29:51 -0700 Subject: [PATCH 1/3] test(ui): reproduce openSignIn withSignUp OAuth sign-up landing on an unmounted create route --- integration/tests/oauth-flows.test.ts | 34 +++++++++++++++++++ .../__tests__/SignInSocialButtons.test.tsx | 31 +++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/integration/tests/oauth-flows.test.ts b/integration/tests/oauth-flows.test.ts index 0dc9e526ff5..8c4bbdb7178 100644 --- a/integration/tests/oauth-flows.test.ts +++ b/integration/tests/oauth-flows.test.ts @@ -19,6 +19,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo test.describe.configure({ mode: 'serial' }); let fakeUser: FakeUser; + let modalSignUpUser: FakeUser; test.beforeAll(async () => { // Create a clerkClient for the OAuth provider instance. @@ -30,16 +31,22 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo fakeUser = users.createFakeUser(test, { withUsername: true, }); + modalSignUpUser = users.createFakeUser(test, { + withUsername: true, + }); // Create the user on the OAuth provider instance so we do not need to sign up twice. await users.createBapiUser(fakeUser); + await users.createBapiUser(modalSignUpUser); }); test.afterAll(async () => { const u = createTestUtils({ app }); // Delete the user on the OAuth provider instance. await fakeUser.deleteIfExists(); + await modalSignUpUser.deleteIfExists(); // Delete the user on the app instance. await u.services.users.deleteIfExists({ email: fakeUser.email }); + await u.services.users.deleteIfExists({ email: modalSignUpUser.email }); await app.teardown(); }); @@ -103,6 +110,33 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo await u.po.expect.toBeSignedIn(); }); + test('openSignIn with withSignUp signs up a new user with custom oauth provider', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + await u.page.goToRelative('/buttons'); + await u.page.waitForClerkJsLoaded(); + await u.po.expect.toBeSignedOut(); + + await u.page.evaluate(() => { + (window as any).Clerk.openSignIn({ + withSignUp: true, + forceRedirectUrl: '/protected', + signUpForceRedirectUrl: '/protected', + }); + }); + await u.po.signIn.waitForModal(); + await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); + await u.page.getByText('Sign in to oauth-provider').waitFor(); + + await u.po.signIn.setIdentifier(modalSignUpUser.email); + await u.po.signIn.continue(); + await u.po.signIn.enterTestOtpCode(); + await grantOAuthConsent(u.page); + + await u.page.waitForAppUrl('/protected'); + await u.po.expect.toBeSignedIn(); + }); + test('openSignIn OAuth uses ClerkProvider.signInUrl for sso-callback', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); diff --git a/packages/ui/src/components/SignIn/__tests__/SignInSocialButtons.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInSocialButtons.test.tsx index 832458dd4b3..49857e17837 100644 --- a/packages/ui/src/components/SignIn/__tests__/SignInSocialButtons.test.tsx +++ b/packages/ui/src/components/SignIn/__tests__/SignInSocialButtons.test.tsx @@ -58,6 +58,37 @@ describe('SignInSocialButtons', () => { openSpy.mockRestore(); }); + it('with virtual routing and withSignUp, redirects back to the sign-in sso-callback route', async () => { + const { wrapper, fixtures, props } = await createFixtures(f => { + f.withSocialProvider({ provider: 'google' }); + }); + props.setProps({ routing: 'virtual', withSignUp: true, oauthFlow: 'redirect' } as any); + fixtures.signIn.authenticateWithRedirect.mockResolvedValue(undefined as any); + + const { userEvent } = render( + + + , + { wrapper }, + ); + + await userEvent.click(screen.getByText('Continue with Google')); + + await waitFor(() => { + expect(fixtures.signIn.authenticateWithRedirect).toHaveBeenCalled(); + }); + expect(fixtures.signIn.authenticateWithRedirect).toHaveBeenCalledWith( + expect.objectContaining({ + strategy: 'oauth_google', + redirectUrl: 'https://dashboard.clerk.com/sign-in#/sso-callback', + }), + ); + }); + it('with a transport registered, clears loading when authenticateWithRedirect rejects', async () => { const { wrapper, fixtures, props } = await createFixtures(f => { f.withSocialProvider({ provider: 'google' }); From f35f48c984e5f61a128a84506674283887a1782d Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 14 Sep 2026 13:33:43 -0700 Subject: [PATCH 2/3] fix(ui): route modal SSO callbacks to the sign-in sso-callback route --- .changeset/modal-with-signup-sso-callback.md | 5 +++++ integration/tests/oauth-flows.test.ts | 9 +++++---- packages/ui/src/contexts/components/SignIn.ts | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 .changeset/modal-with-signup-sso-callback.md diff --git a/.changeset/modal-with-signup-sso-callback.md b/.changeset/modal-with-signup-sso-callback.md new file mode 100644 index 00000000000..4437de2b78c --- /dev/null +++ b/.changeset/modal-with-signup-sso-callback.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Fixed OAuth and Enterprise SSO sign-ups started from `openSignIn({ withSignUp: true })` or ``. The callback now returns to the sign-in page's `sso-callback` route, which completes the sign-up transfer. Previously it targeted a `create/sso-callback` route that only exists when that page mounts ``, so new users landed on the sign-in form with an "External Account was not found" error instead of being signed up. diff --git a/integration/tests/oauth-flows.test.ts b/integration/tests/oauth-flows.test.ts index 8c4bbdb7178..daec351ec98 100644 --- a/integration/tests/oauth-flows.test.ts +++ b/integration/tests/oauth-flows.test.ts @@ -273,7 +273,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpFlow] })('oauth await app.teardown(); }); - test('openSignIn OAuth in combined flow targets /sign-in#/create/sso-callback', async ({ page, context }) => { + test('openSignIn OAuth in combined flow targets /sign-in#/sso-callback', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.page.goToRelative('/buttons'); @@ -297,13 +297,14 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpFlow] })('oauth expect(redirectUrl).toBeTruthy(); // Combined flow (CLERK_SIGN_UP_URL is unset in this env): the sso-callback must anchor to - // ClerkProvider.signInUrl and carry the combined-flow /create segment, since the - // create/sso-callback route is mounted under the SignIn tree — not SignUp. + // ClerkProvider.signInUrl. The modal cannot know whether the SignIn mounted at that URL has the + // combined-flow `create/*` routes, so it targets `sso-callback`, which every SignIn mounts and + // whose handler transfers new users into a sign-up itself. const parsed = new URL(redirectUrl!); const appOrigin = new URL(app.serverUrl).origin; expect(parsed.origin).toBe(appOrigin); expect(parsed.pathname).toBe('/sign-in'); - expect(parsed.hash).toMatch(/^#\/create\/sso-callback/); + expect(parsed.hash).toMatch(/^#\/sso-callback/); }); }); diff --git a/packages/ui/src/contexts/components/SignIn.ts b/packages/ui/src/contexts/components/SignIn.ts index 2d44b0bb838..3be9d63fac6 100644 --- a/packages/ui/src/contexts/components/SignIn.ts +++ b/packages/ui/src/contexts/components/SignIn.ts @@ -119,7 +119,8 @@ export const useSignInContext = (): SignInContextType => { baseUrl: signInUrl, authQueryString, path: ctx.path, - endpoint: isCombinedFlow ? '/create' + SSO_CALLBACK_PATH_ROUTE : SSO_CALLBACK_PATH_ROUTE, + endpoint: + isCombinedFlow && ctx.routing !== 'virtual' ? '/create' + SSO_CALLBACK_PATH_ROUTE : SSO_CALLBACK_PATH_ROUTE, }); if (isCombinedFlow) { From fc7fe29a588b75db72cd90987923793c0e140780 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Mon, 14 Sep 2026 14:38:54 -0700 Subject: [PATCH 3/3] chore: update changeset --- .changeset/modal-with-signup-sso-callback.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/modal-with-signup-sso-callback.md b/.changeset/modal-with-signup-sso-callback.md index 4437de2b78c..18927dc06ed 100644 --- a/.changeset/modal-with-signup-sso-callback.md +++ b/.changeset/modal-with-signup-sso-callback.md @@ -2,4 +2,4 @@ '@clerk/ui': patch --- -Fixed OAuth and Enterprise SSO sign-ups started from `openSignIn({ withSignUp: true })` or ``. The callback now returns to the sign-in page's `sso-callback` route, which completes the sign-up transfer. Previously it targeted a `create/sso-callback` route that only exists when that page mounts ``, so new users landed on the sign-in form with an "External Account was not found" error instead of being signed up. +Fixed OAuth sign-ups from `openSignIn({ withSignUp: true })` landing on the sign-in page with an "External Account was not found" error instead of creating the account.