diff --git a/.changeset/connected-accounts-reconnect-pending.md b/.changeset/connected-accounts-reconnect-pending.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/connected-accounts-reconnect-pending.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.reconnect.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.reconnect.feature.test.tsx index 053eed1444e..0f836ee19ac 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.reconnect.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.reconnect.feature.test.tsx @@ -72,6 +72,26 @@ describe('connected accounts', () => { await waitFor(() => expect(navigate).toHaveBeenCalledWith('https://accounts.example/authorize')); }); + it('holds the menu open with a spinner on Reconnect until the provider redirect', async () => { + serveFapi(signedIn([disconnectedGoogle])); + const { clerk } = await renderWithClerk(); + const navigate = vi.spyOn(clerk, 'navigate').mockImplementation(() => Promise.resolve()); + const request = holdRequests('post', '/v1/me/external_accounts'); + const user = userEvent.setup(); + + try { + await startReconnect(user, 'Google'); + await waitFor(() => expect(request.requests).toHaveLength(1)); + expect(screen.getByRole('menu')).toBeVisible(); + expect(screen.getByRole('menuitem', { name: 'Reconnect' })).toHaveAttribute('aria-busy', 'true'); + expect(await screen.findByRole('progressbar', { name: 'Reconnect' })).toBeVisible(); + } finally { + request.release(); + } + + await waitFor(() => expect(navigate).toHaveBeenCalledWith('https://accounts.example/authorize')); + }); + it('reports an unavailable recovery when the account changes during redirect preparation', async () => { serveFapi(signedIn([disconnectedGoogle])); const redirect = deferred(); @@ -286,6 +306,25 @@ describe('reconnect reverification', () => { await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()); }); + it('holds the Reconnect menu item until session verification opens', async () => { + await renderDisconnectedNeedingReverification(); + const start = holdRequests('post', '/v1/client/sessions/:id/verify'); + const user = userEvent.setup(); + + try { + await startReconnect(user, 'Google'); + await waitFor(() => expect(start.requests).toHaveLength(1)); + expect(screen.getByRole('menu')).toBeVisible(); + expect(screen.getByRole('menuitem', { name: 'Reconnect' })).toHaveAttribute('aria-busy', 'true'); + expect(screen.queryByText('Enter your current password to continue')).toBeNull(); + } finally { + start.release(); + } + + expect(await screen.findByText('Enter your current password to continue')).toBeVisible(); + await waitFor(() => expect(screen.queryByRole('menu')).toBeNull()); + }); + it('lets the user cancel verification quietly and retry the reconnect', async () => { const { navigate } = await renderDisconnectedNeedingReverification(); const user = userEvent.setup(); diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx index cd82e7776e2..ba0dae6b9d2 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx @@ -80,21 +80,28 @@ export function UserProfileConnectedAccountRowView({ account, triggerRef, isDisabled = false, + isReconnecting = false, onReconnect, onRemove, }: { account: UserProfileConnectedAccount; triggerRef?: Ref; isDisabled?: boolean; + isReconnecting?: boolean; onReconnect?: (id: string) => void; onRemove?: (account: UserProfileConnectedAccount) => void; }) { const m = useMessages('userProfileConnectedAccounts'); const actions: ActionMenuAction[] = []; - if ((account.status === 'reconnect' || account.status === 'error') && onReconnect && !isDisabled) { + if ( + (account.status === 'reconnect' || account.status === 'error') && + onReconnect && + (!isDisabled || isReconnecting) + ) { actions.push({ label: account.status === 'reconnect' ? m.reconnect : m.tryAgain, onClick: () => onReconnect(account.id), + pending: isReconnecting, }); } if (onRemove) { diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx index ea65ed6c7e9..8792870f58d 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx @@ -53,6 +53,7 @@ export function UserProfileConnectedAccountsSectionView({ account={account} triggerRef={registerTrigger(account.id)} isDisabled={isBusy} + isReconnecting={pendingId === account.id && !reconnectReverification?.visible} onReconnect={onReconnect} onRemove={remove ? account => remove.open(account) : undefined} />