Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/early-crabs-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/ui': patch
---

Fix reconnecting disconnected Google One Tap accounts in UserProfile to use Google OAuth while preserving session reverification.
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(<ConnectedAccountsSection />, { 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);

Expand Down