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}
/>