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);