From 9243dc67ea9896dd34dc09cdf0e9ae8f95ff6275 Mon Sep 17 00:00:00 2001 From: akshitkrnagpal Date: Wed, 9 Sep 2026 14:47:13 +0400 Subject: [PATCH] fix(ui): reconnect Google One Tap accounts through Google OAuth - Preserve session reverification when reconnecting in UserProfile. --- .changeset/early-crabs-obey.md | 5 +++ .../UserProfile/ConnectedAccountsSection.tsx | 5 ++- .../ConnectedAccountsSection.test.tsx | 44 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .changeset/early-crabs-obey.md diff --git a/.changeset/early-crabs-obey.md b/.changeset/early-crabs-obey.md new file mode 100644 index 00000000000..3ec11c9ace5 --- /dev/null +++ b/.changeset/early-crabs-obey.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Fix reconnecting disconnected Google One Tap accounts in UserProfile to use Google OAuth while preserving session reverification. diff --git a/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx b/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx index 6b3f60e3400..0f8eb828cb1 100644 --- a/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx +++ b/packages/ui/src/components/UserProfile/ConnectedAccountsSection.tsx @@ -109,7 +109,10 @@ const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) => const reauthorizationRequired = additionalScopes.length > 0 && account.approvedScopes != ''; const shouldDisplayReconnect = errorCodesForReconnect.includes(account.verification?.error?.code || '') || reauthorizationRequired; - const strategy = (account.verification?.strategy || `oauth_${account.provider}`) as OAuthStrategy; + const verificationStrategy = account.verification?.strategy; + const strategy = ( + verificationStrategy === 'google_one_tap' ? 'oauth_google' : verificationStrategy || `oauth_${account.provider}` + ) as OAuthStrategy; const createExternalAccount = useReverification((redirectUrl: string) => user?.createExternalAccount({ diff --git a/packages/ui/src/components/UserProfile/__tests__/ConnectedAccountsSection.test.tsx b/packages/ui/src/components/UserProfile/__tests__/ConnectedAccountsSection.test.tsx index b8b242cecbc..8b8b3ac0469 100644 --- a/packages/ui/src/components/UserProfile/__tests__/ConnectedAccountsSection.test.tsx +++ b/packages/ui/src/components/UserProfile/__tests__/ConnectedAccountsSection.test.tsx @@ -1,3 +1,4 @@ +import { ClerkAPIResponseError } from '@clerk/shared/error'; import { CLERK_MODAL_STATE } from '@clerk/shared/internal/clerk-js/constants'; import type { ExternalAccountResource } from '@clerk/shared/types'; import { act, waitFor } from '@testing-library/react'; @@ -242,6 +243,49 @@ describe('ConnectedAccountsSection ', () => { expect(await screen.findByText(/OAuth flow did not receive a verification URL./i)).toBeInTheDocument(); }); + it.each(['google_one_tap', 'oauth_google'] as const)( + 'reconnects a %s account using Google OAuth after reverification', + async strategy => { + const { wrapper, fixtures } = await createFixtures(withReconnectableConnection); + fixtures.clerk.user!.externalAccounts[0].verification!.strategy = strategy; + const createExternalAccount = fixtures.clerk.user!.createExternalAccount; + createExternalAccount + .mockRejectedValueOnce( + new ClerkAPIResponseError('Reverification required', { + status: 403, + data: [{ code: 'session_reverification_required', message: 'Reverification required' }], + }), + ) + .mockResolvedValueOnce({ + verification: { externalVerificationRedirectURL: new URL('https://provider.example/auth') }, + } as ExternalAccountResource); + const openReverification = vi + .spyOn(fixtures.clerk, '__internal_openReverification') + .mockImplementation(() => {}); + const { userEvent, getByRole } = render(, { wrapper }); + + await userEvent.click(getByRole('button', { name: /reconnect/i })); + + await waitFor(() => expect(openReverification).toHaveBeenCalledTimes(1)); + const expectedParams = { + strategy: 'oauth_google', + redirectUrl: window.location.href, + additionalScopes: [], + }; + expect(createExternalAccount).toHaveBeenCalledTimes(1); + expect(createExternalAccount).toHaveBeenNthCalledWith(1, expectedParams); + expect(fixtures.router.navigate).not.toHaveBeenCalled(); + + await act(() => { + openReverification.mock.calls[0][0].afterVerification(); + }); + + expect(createExternalAccount).toHaveBeenCalledTimes(2); + expect(createExternalAccount).toHaveBeenNthCalledWith(2, expectedParams); + expect(fixtures.router.navigate).toHaveBeenCalledWith('https://provider.example/auth'); + }, + ); + it('Additional scopes need reconnection', async () => { const { wrapper, fixtures, props } = await createFixtures(withReconnectableConnectionAdditionalScopes);