From 6fe9ce9afd39db1f15027649621c3b7cf23a3502 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Mon, 14 Sep 2026 15:57:34 -0600 Subject: [PATCH 01/11] feat(ui): add connected account row actions and removal dialog --- .../connected-account-removal-dialog.md | 2 + ...rofile-connected-accounts-actions.test.tsx | 86 ++++++++++++++ ...e-connected-accounts-section.view.test.tsx | 106 ++++++++++++++++++ .../user-profile-profile-panel.view.test.tsx | 48 +++++--- .../user-profile-connect-account-row.view.tsx | 45 ++++++++ ...ser-profile-connected-account-row.view.tsx | 76 +++++++++++++ ...rofile-connected-accounts-section.view.tsx | 97 +++++++--------- ...ser-profile-connected-accounts.messages.ts | 16 +++ .../user-profile-connected-accounts.styles.ts | 21 ++++ .../user-profile-connected-provider-icon.tsx | 20 ++++ .../user-profile-profile-panel.view.tsx | 16 ++- ...rofile-remove-connected-account.dialog.tsx | 64 +++++++++++ 12 files changed, 518 insertions(+), 79 deletions(-) create mode 100644 .changeset/connected-account-removal-dialog.md create mode 100644 packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-actions.test.tsx create mode 100644 packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx create mode 100644 packages/ui/src/mosaic/features/user-profile/user-profile-connect-account-row.view.tsx create mode 100644 packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx create mode 100644 packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.messages.ts create mode 100644 packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.styles.ts create mode 100644 packages/ui/src/mosaic/features/user-profile/user-profile-connected-provider-icon.tsx create mode 100644 packages/ui/src/mosaic/features/user-profile/user-profile-remove-connected-account.dialog.tsx diff --git a/.changeset/connected-account-removal-dialog.md b/.changeset/connected-account-removal-dialog.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/connected-account-removal-dialog.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-actions.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-actions.test.tsx new file mode 100644 index 00000000000..eefd8b99e4a --- /dev/null +++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-actions.test.tsx @@ -0,0 +1,86 @@ +import { render, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { useState } from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { MosaicProvider } from '../../MosaicProvider'; +import { UserProfileConnectedAccountsSectionView } from '../user-profile-connected-accounts-section.view'; + +const account = { id: 'github', provider: 'GitHub', identifier: 'test' }; + +function renderAccounts(onRemove: (id: string) => void) { + return render( + + + , + ); +} + +async function openRemoval(user: ReturnType) { + await user.click(screen.getByRole('button', { name: 'Manage GitHub' })); + await user.click(screen.getByRole('menuitem', { name: 'Remove' })); +} + +describe('connected account removal', () => { + it('returns keyboard focus to the menu when canceled', async () => { + const user = userEvent.setup(); + const onRemove = vi.fn(); + renderAccounts(onRemove); + const trigger = screen.getByRole('button', { name: 'Manage GitHub' }); + trigger.focus(); + await user.keyboard('{Enter}{Enter}'); + expect(screen.getByRole('alertdialog')).toBeInTheDocument(); + await user.keyboard('{Escape}'); + await waitFor(() => expect(trigger).toHaveFocus()); + expect(onRemove).not.toHaveBeenCalled(); + }); + + it('closes confirmation when the caller removes the account', async () => { + const user = userEvent.setup(); + function Example() { + const [accounts, setAccounts] = useState([account]); + return ( + + setAccounts(current => current.filter(item => item.id !== id))} + /> + + ); + } + render(); + await openRemoval(user); + await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove' })); + await waitFor(() => expect(screen.queryByRole('button', { name: 'Manage GitHub' })).not.toBeInTheDocument()); + await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()); + }); + + it('shows a removal failure and allows retrying', async () => { + const user = userEvent.setup(); + const onRemove = vi.fn(); + function Example() { + const [removalError, setRemovalError] = useState(); + return ( + + { + onRemove(id); + setRemovalError(onRemove.mock.calls.length === 1 ? 'Unable to disconnect' : undefined); + }} + /> + + ); + } + render(); + await openRemoval(user); + await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove' })); + expect(await screen.findByRole('alert')).toHaveTextContent('Unable to disconnect'); + await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove' })); + expect(onRemove).toHaveBeenCalledTimes(2); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx new file mode 100644 index 00000000000..d76a3ffc6f4 --- /dev/null +++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx @@ -0,0 +1,106 @@ +import { render, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it, vi } from 'vitest'; + +import { UserProfileConnectedAccountsSectionView } from '../user-profile-connected-accounts-section.view'; + +const account = { id: 'account_1', provider: 'Google', identifier: 'test@example.com' }; + +describe('connected accounts section', () => { + it('forwards connect actions and allows retrying after an error', async () => { + const user = userEvent.setup(); + const onConnect = vi.fn(); + const availableProviders = [ + { id: 'apple', provider: 'Apple' }, + { id: 'github', provider: 'GitHub' }, + ]; + const { rerender } = render( + , + ); + await user.click(screen.getByRole('button', { name: 'Connect Apple' })); + expect(onConnect).toHaveBeenCalledExactlyOnceWith('apple'); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + rerender( + , + ); + expect(screen.getByRole('alert')).toHaveTextContent('Connection failed'); + await user.click(screen.getByRole('button', { name: 'Connect Apple' })); + expect(onConnect).toHaveBeenCalledTimes(2); + }); + + it('keeps confirmation open for pending and error props and forwards retry', async () => { + const user = userEvent.setup(); + const onRemove = vi.fn(); + const { rerender } = render( + , + ); + await user.click(screen.getByRole('button', { name: 'Manage Google' })); + await user.click(screen.getByRole('menuitem', { name: 'Remove' })); + const dialog = screen.getByRole('alertdialog'); + expect(dialog).toHaveTextContent('dependent features'); + await user.click(within(dialog).getByRole('button', { name: 'Remove', exact: true })); + expect(onRemove).toHaveBeenCalledExactlyOnceWith('account_1'); + rerender( + , + ); + expect(within(dialog).getByRole('button', { name: 'Remove', exact: true })).toHaveAttribute( + 'aria-disabled', + 'true', + ); + expect(within(dialog).getByRole('progressbar')).toBeInTheDocument(); + await user.click(within(dialog).getByRole('button', { name: 'Remove', exact: true })); + expect(onRemove).toHaveBeenCalledTimes(1); + expect(within(dialog).getByRole('button', { name: 'Cancel' })).toBeDisabled(); + await user.click(within(dialog).getByRole('button', { name: 'Cancel' })); + expect(dialog).toBeInTheDocument(); + rerender( + , + ); + expect(within(dialog).getByRole('alert')).toHaveTextContent('Try again'); + expect(within(dialog).getByRole('button', { name: 'Cancel' })).toBeEnabled(); + await user.click(within(dialog).getByRole('button', { name: 'Remove', exact: true })); + expect(onRemove).toHaveBeenCalledTimes(2); + await user.click(within(dialog).getByRole('button', { name: 'Cancel' })); + await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()); + expect(screen.getByRole('button', { name: 'Manage Google' })).toHaveFocus(); + }); + + it('renders reconnect and verification errors without a generic manage action', async () => { + const user = userEvent.setup(); + const onReconnect = vi.fn(); + render( + , + ); + expect(screen.getByText('Disconnected')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Reconnect' })).not.toBeInTheDocument(); + await user.click(screen.getByRole('button', { name: 'Manage Google' })); + await user.click(screen.getByRole('menuitem', { name: 'Reconnect' })); + expect(onReconnect).toHaveBeenCalledExactlyOnceWith('account_1'); + expect(screen.getByText('Provider error')).toBeInTheDocument(); + expect(screen.getAllByRole('alert').some(alert => alert.textContent?.includes('Reconnection failed'))).toBe(true); + expect(screen.queryByRole('button', { name: 'Manage Other' })).not.toBeInTheDocument(); + }); +}); diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx index a76fbd926ec..d29e2d75be3 100644 --- a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx +++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx @@ -29,6 +29,18 @@ function renderView(overrides: Partial = {}) { } describe('UserProfileProfilePanelView', () => { + it('keeps available providers visible without connected accounts and forwards connect', async () => { + const onConnectAccount = vi.fn(); + const user = userEvent.setup(); + renderView({ + connectedAccounts: [], + availableConnectionProviders: [{ id: 'google', provider: 'Google' }], + onConnectAccount, + }); + await user.click(screen.getByRole('button', { name: 'Connect Google' })); + expect(onConnectAccount).toHaveBeenCalledExactlyOnceWith('google'); + }); + it.each([false, true])('formats normalized phone numbers with multiple accounts set to %s', allowMultipleAccounts => { renderView({ allowMultipleAccounts, @@ -196,44 +208,40 @@ describe('UserProfileProfilePanelView', () => { }); it('renders connected accounts and the danger zone when provided', async () => { - const onConnectAccount = vi.fn(); - const onManageConnectedAccount = vi.fn(); + const onRemoveConnectedAccount = vi.fn(); const onDeleteAccount = vi.fn(() => Promise.resolve()); const user = userEvent.setup(); renderView({ connectedAccounts: [ { id: 'google', provider: 'Google', identifier: 'test@google.com', iconUrl: 'https://example.com/google.svg' }, - { id: 'apple', provider: 'Apple', connected: false }, ], - onConnectAccount, - onManageConnectedAccount, + onRemoveConnectedAccount, onDeleteAccount, }); expect(screen.getByRole('heading', { level: 4, name: 'Connected accounts' })).toBeInTheDocument(); - expect( - screen.getByRole('region', { name: 'Connected accounts' }).querySelector('.cl-section-media[data-size="lg"] img'), - ).toHaveAttribute('src', 'https://example.com/google.svg'); + expect(screen.getByRole('img', { name: 'Google' })).toHaveAttribute('src', 'https://example.com/google.svg'); expect(screen.getByRole('heading', { level: 4, name: 'Danger zone' })).toBeInTheDocument(); expect(screen.getByText('Delete account', { selector: '.cl-section-label' })).toBeInTheDocument(); expect(screen.getByText('Permanently delete this account and all its data. This cannot be undone.')).toHaveClass( 'cl-section-description', ); await user.click(screen.getByRole('button', { name: 'Manage Google' })); - expect(onManageConnectedAccount).not.toHaveBeenCalled(); - await user.click(screen.getByRole('menuitem', { name: 'Manage' })); - await user.click(screen.getByRole('button', { name: 'Connect' })); + expect(onRemoveConnectedAccount).not.toHaveBeenCalled(); + await user.click(screen.getByRole('menuitem', { name: 'Remove' })); + await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove' })); + await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Cancel' })); + await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()); await user.click(screen.getByRole('button', { name: 'Delete account' })); const deleteDialog = screen.getByRole('dialog'); await user.type(within(deleteDialog).getByRole('textbox'), 'Delete account'); await user.click(within(deleteDialog).getByRole('button', { name: 'Delete account' })); - expect(onManageConnectedAccount).toHaveBeenCalledWith('google'); - expect(onConnectAccount).toHaveBeenCalledWith('apple'); + expect(onRemoveConnectedAccount).toHaveBeenCalledWith('google'); expect(onDeleteAccount).toHaveBeenCalledOnce(); }); - it('renders provider images in icon frames', () => { + it('renders connected provider images and Web3 icon frames', () => { const { container } = renderView({ connectedAccounts: [{ id: 'google', provider: 'Google', iconUrl: '/google.svg' }], web3Wallets: [{ id: 'metamask', provider: 'MetaMask', iconUrl: '/metamask.svg' }], @@ -241,9 +249,9 @@ describe('UserProfileProfilePanelView', () => { const frames = container.querySelectorAll('.cl-icon-frame'); const images = container.querySelectorAll('img'); - expect(frames).toHaveLength(2); - expect(frames[0]).toContainElement(images[0]); - expect(frames[1]).toContainElement(images[1]); + expect(frames).toHaveLength(1); + expect(screen.getByRole('img', { name: 'Google' })).toHaveAttribute('src', '/google.svg'); + expect(frames[0]).toContainElement(images[1]); frames.forEach(frame => expect(frame.closest('.cl-section-media')).toHaveAttribute('data-size', 'lg')); }); @@ -450,6 +458,12 @@ describe('UserProfileProfilePanelView', () => { const removeConnectedAccount = screen.getByRole('menuitem', { name: 'Remove' }); expect(removeConnectedAccount).toHaveAttribute('data-color', 'negative'); await user.click(removeConnectedAccount); + expect(onRemoveConnectedAccount).not.toHaveBeenCalled(); + await user.click( + within(screen.getByRole('alertdialog', { name: 'Remove connected account' })).getByRole('button', { + name: 'Remove', + }), + ); expect(onRemoveConnectedAccount).toHaveBeenCalledWith('github'); }); diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connect-account-row.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connect-account-row.view.tsx new file mode 100644 index 00000000000..645f26b3930 --- /dev/null +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connect-account-row.view.tsx @@ -0,0 +1,45 @@ +import { Button } from '../components/button'; +import { Icon } from '../components/icon'; +import { Section } from '../components/section'; +import { userProfileConnectedAccountsMessages as m } from './user-profile-connected-accounts.messages'; +import { styles } from './user-profile-connected-accounts.styles'; +import type { UserProfileConnectionProvider } from './user-profile-connected-accounts-section.view'; +import { UserProfileConnectedProviderIcon } from './user-profile-connected-provider-icon'; + +export function UserProfileConnectAccountRowView({ + provider, + onConnect, +}: { + provider: UserProfileConnectionProvider; + onConnect: (id: string) => void; +}) { + return ( + + + + + + + {provider.provider} + + + + + + {provider.connectError ? {provider.connectError} : null} + + ); +} diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx new file mode 100644 index 00000000000..88bbe1f973c --- /dev/null +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react'; + +import { Badge } from '../components/badge'; +import { Section } from '../components/section'; +import type { UserProfileMenuAction } from './user-profile-action-menu'; +import { UserProfileActionMenu } from './user-profile-action-menu'; +import { userProfileConnectedAccountsMessages as m } from './user-profile-connected-accounts.messages'; +import { styles } from './user-profile-connected-accounts.styles'; +import type { UserProfileConnectedAccount } from './user-profile-connected-accounts-section.view'; +import { UserProfileConnectedProviderIcon } from './user-profile-connected-provider-icon'; +import { UserProfileRemoveConnectedAccountDialog } from './user-profile-remove-connected-account.dialog'; + +export function UserProfileConnectedAccountRowView({ + account, + onReconnect, + onRemove, +}: { + account: UserProfileConnectedAccount; + onReconnect?: (id: string) => void; + onRemove?: (id: string) => void; +}) { + const [open, setOpen] = useState(false); + const actions: UserProfileMenuAction[] = []; + if (account.status === 'reconnect' && onReconnect) { + actions.push({ label: m.reconnect, onClick: () => onReconnect(account.id) }); + } + if (onRemove && account.canRemove !== false) { + actions.push({ label: m.remove, color: 'negative', onClick: () => setOpen(true) }); + } + return ( + + + + + + + + {account.provider} + {account.status === 'reconnect' ? {m.disconnected} : null} + + {account.identifier ? ( + + {account.identifier} + + ) : null} + + {actions.length > 0 ? ( + + + {onRemove && account.canRemove !== false ? ( + onRemove(account.id)} + isPending={account.isRemoving} + errorMessage={account.removalError} + /> + ) : null} + + + ) : null} + + {account.reconnectError ? {account.reconnectError} : null} + {account.status === 'error' && account.verificationError ? ( + {account.verificationError} + ) : null} + + ); +} diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts-section.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts-section.view.tsx index f0b4b990981..38174f80c86 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts-section.view.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts-section.view.tsx @@ -1,81 +1,64 @@ -import { Button } from '../../components/button'; -import { Icon } from '../../components/icon'; import { Section } from '../../components/section'; -import type { UserProfileMenuAction } from './user-profile-action-menu'; -import { UserProfileActionMenu } from './user-profile-action-menu'; -import { UserProfileProviderIcon } from './user-profile-provider-icon'; +import { UserProfileConnectAccountRowView } from './user-profile-connect-account-row.view'; +import { UserProfileConnectedAccountRowView } from './user-profile-connected-account-row.view'; +import { userProfileConnectedAccountsMessages as m } from './user-profile-connected-accounts.messages'; -export interface UserProfileConnectedAccount { +export interface UserProfileConnectionProvider { id: string; provider: string; - identifier?: string; iconUrl?: string; - connected?: boolean; + connectError?: string; +} + +export interface UserProfileConnectedAccount extends UserProfileConnectionProvider { + identifier?: string; canRemove?: boolean; + status?: 'connected' | 'reconnect' | 'error'; + verificationError?: string; + reconnectError?: string; + isRemoving?: boolean; + removalError?: string; } export interface UserProfileConnectedAccountsSectionViewProps { accounts: UserProfileConnectedAccount[]; + availableProviders?: UserProfileConnectionProvider[]; onConnect?: (id: string) => void; - onManage?: (id: string) => void; + onReconnect?: (id: string) => void; onRemove?: (id: string) => void; } export function UserProfileConnectedAccountsSectionView({ accounts, + availableProviders = [], onConnect, - onManage, + onReconnect, onRemove, }: UserProfileConnectedAccountsSectionViewProps) { return ( - Connected accounts - - {accounts.map(account => { - const connected = account.connected ?? Boolean(account.identifier); - const actions: UserProfileMenuAction[] = []; - if (onRemove && account.canRemove !== false) { - actions.push({ label: 'Remove', color: 'negative', onClick: () => onRemove(account.id) }); - } else if (!onRemove && onManage) { - actions.push({ label: 'Manage', onClick: () => onManage(account.id) }); - } - - return ( - - - {account.iconUrl ? : null} - - {account.provider} - {account.identifier ? {account.identifier} : null} - - - {connected ? ( - - ) : null} - {!connected && onConnect ? ( - - ) : null} - - - - ); - })} - + {m.title} + {accounts.length > 0 || (availableProviders.length > 0 && onConnect) ? ( + + {accounts.map(account => ( + + ))} + {onConnect + ? availableProviders.map(provider => ( + + )) + : null} + + ) : null} ); } diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.messages.ts b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.messages.ts new file mode 100644 index 00000000000..5f86638e39d --- /dev/null +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.messages.ts @@ -0,0 +1,16 @@ +export const userProfileConnectedAccountsMessages = { + title: 'Connected accounts', + connect: 'Connect', + reconnect: 'Reconnect', + disconnected: 'Disconnected', + manage: 'Manage', + remove: 'Remove', + removeDialog: { + title: 'Remove connected account', + description: + '{provider} will be removed from this account. You will no longer be able to use this connected account and any dependent features will no longer work.', + confirm: 'Remove', + pending: 'Removing…', + cancel: 'Cancel', + }, +}; diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.styles.ts b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.styles.ts new file mode 100644 index 00000000000..8c785da208b --- /dev/null +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.styles.ts @@ -0,0 +1,21 @@ +import * as stylex from '@stylexjs/stylex'; + +import { colorVars, radiusVars, space } from '../tokens.stylex'; + +export const styles = stylex.create({ + connectRow: { justifyContent: 'center' }, + icon: { display: 'block', objectFit: 'contain', height: space['6'], width: space['6'] }, + fallback: { + borderRadius: radiusVars['--cl-radius-sm'], + alignItems: 'center', + backgroundColor: `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 8%, transparent)`, + color: colorVars['--cl-color-neutral'], + display: 'inline-flex', + fontSize: '0.625rem', + justifyContent: 'center', + height: space['6'], + width: space['6'], + }, + label: { gap: space['2'], alignItems: 'center', display: 'flex', flexWrap: 'wrap' }, + text: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 }, +}); diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-provider-icon.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-provider-icon.tsx new file mode 100644 index 00000000000..d529794b91c --- /dev/null +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-provider-icon.tsx @@ -0,0 +1,20 @@ +import * as stylex from '@stylexjs/stylex'; + +import { styles } from './user-profile-connected-accounts.styles'; + +export function UserProfileConnectedProviderIcon({ provider, iconUrl }: { provider: string; iconUrl?: string }) { + return iconUrl?.trim() ? ( + {provider} + ) : ( + + {provider.trim().charAt(0).toUpperCase()} + + ); +} diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-profile-panel.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-profile-panel.view.tsx index 1a71ffc5a64..e9b2792a731 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-profile-panel.view.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-profile-panel.view.tsx @@ -9,7 +9,10 @@ import type { UserProfilePhone, } from './user-profile-account-section/user-profile-account-section.view'; import { UserProfileAccountSectionView } from './user-profile-account-section/user-profile-account-section.view'; -import type { UserProfileConnectedAccount } from './user-profile-connected-accounts-section.view'; +import type { + UserProfileConnectedAccount, + UserProfileConnectionProvider, +} from './user-profile-connected-accounts-section.view'; import { UserProfileConnectedAccountsSectionView } from './user-profile-connected-accounts-section.view'; import { UserProfileDeleteSectionView } from './user-profile-delete-section/user-profile-delete-section.view'; import { styles } from './user-profile-profile-panel.styles'; @@ -25,9 +28,10 @@ export type { UserProfileEditNameValue } from './user-profile-account-section/us export interface UserProfileProfilePanelViewProps extends UserProfileAccountSectionViewProps { connectedAccounts?: UserProfileConnectedAccount[]; + availableConnectionProviders?: UserProfileConnectionProvider[]; + onReconnectAccount?: (id: string) => void; web3Wallets?: UserProfileWeb3Wallet[]; onConnectAccount?: (id: string) => void; - onManageConnectedAccount?: (id: string) => void; onRemoveConnectedAccount?: (id: string) => void; onConnectWeb3Wallet?: (id: string) => void; onManageWeb3Wallet?: (id: string) => void; @@ -50,6 +54,8 @@ export function UserProfileProfilePanelView({ emails = [], phones = [], connectedAccounts = [], + availableConnectionProviders = [], + onReconnectAccount, web3Wallets = [], onProfilePictureChange, onProfilePictureReject, @@ -68,7 +74,6 @@ export function UserProfileProfilePanelView({ onSetPrimaryPhone, onRemovePhone, onConnectAccount, - onManageConnectedAccount, onRemoveConnectedAccount, onConnectWeb3Wallet, onManageWeb3Wallet, @@ -109,11 +114,12 @@ export function UserProfileProfilePanelView({ onSubmitName={onSubmitName} onSubmitUsername={onSubmitUsername} /> - {connectedAccounts.length > 0 ? ( + {connectedAccounts.length > 0 || availableConnectionProviders.length > 0 ? ( ) : null} diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-remove-connected-account.dialog.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-remove-connected-account.dialog.tsx new file mode 100644 index 00000000000..9cd8f051173 --- /dev/null +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-remove-connected-account.dialog.tsx @@ -0,0 +1,64 @@ +import { Banner } from '../components/banner'; +import { Button, SubmitButton } from '../components/button'; +import { Dialog } from '../components/dialog'; +import { Heading } from '../components/heading'; +import { Text } from '../components/text'; +import { userProfileConnectedAccountsMessages as m } from './user-profile-connected-accounts.messages'; + +export interface UserProfileRemoveConnectedAccountDialogProps { + provider: string; + open: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: () => void; + isPending?: boolean; + errorMessage?: string; +} + +export function UserProfileRemoveConnectedAccountDialog({ + provider, + open, + onOpenChange, + onConfirm, + isPending, + errorMessage, +}: UserProfileRemoveConnectedAccountDialogProps) { + return ( + + + }>{m.removeDialog.title} + }> + {m.removeDialog.description.replace('{provider}', provider)} + + {errorMessage ? ( + + {errorMessage} + + ) : null} + + } + > + {m.removeDialog.cancel} + + + {m.removeDialog.confirm} + + + + + ); +} From 5384e33cb9d13bd10c548baa912196ae246e64fc Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Mon, 14 Sep 2026 15:58:06 -0600 Subject: [PATCH 02/11] docs(swingset): add interactive connected account examples --- .changeset/connected-accounts-view-states.md | 2 + packages/swingset/src/lib/registry.ts | 14 ++++ .../user-profile-connected-accounts.ts | 64 +++++++++++++++++ .../src/stories/fixtures/user-profile.ts | 7 ++ ...ser-profile-connected-accounts-section.mdx | 48 ++++++++++--- ...ile-connected-accounts-section.stories.tsx | 69 ++++++++++++------- .../user-profile-profile-panel.stories.tsx | 19 ++--- 7 files changed, 177 insertions(+), 46 deletions(-) create mode 100644 .changeset/connected-accounts-view-states.md create mode 100644 packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts diff --git a/.changeset/connected-accounts-view-states.md b/.changeset/connected-accounts-view-states.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/connected-accounts-view-states.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/swingset/src/lib/registry.ts b/packages/swingset/src/lib/registry.ts index 05fcb700e63..450b450f4b5 100644 --- a/packages/swingset/src/lib/registry.ts +++ b/packages/swingset/src/lib/registry.ts @@ -217,8 +217,15 @@ import { meta as userProfileBillingPanelMeta, } from '../stories/user-profile-billing-panel.stories'; import { + ConnectionError as ConnectedAccountsConnectionError, + ConnectOnly as ConnectedAccountsConnectOnly, Default as UserProfileConnectedAccountsSectionDefault, + LinkedAccounts as ConnectedAccountsLinkedAccounts, meta as userProfileConnectedAccountsSectionMeta, + ReconnectRequired as ConnectedAccountsReconnectRequired, + RemovalError as ConnectedAccountsRemovalError, + RemovalPending as ConnectedAccountsRemovalPending, + VerificationError as ConnectedAccountsVerificationError, } from '../stories/user-profile-connected-accounts-section.stories'; import { Default as UserProfileDeleteSectionDefault, @@ -522,6 +529,13 @@ const userProfilePaymentMethodsSectionModule: StoryModule = { const userProfileConnectedAccountsSectionModule: StoryModule = { meta: userProfileConnectedAccountsSectionMeta, Default: UserProfileConnectedAccountsSectionDefault, + LinkedAccounts: ConnectedAccountsLinkedAccounts, + ConnectOnly: ConnectedAccountsConnectOnly, + ReconnectRequired: ConnectedAccountsReconnectRequired, + VerificationError: ConnectedAccountsVerificationError, + ConnectionError: ConnectedAccountsConnectionError, + RemovalPending: ConnectedAccountsRemovalPending, + RemovalError: ConnectedAccountsRemovalError, }; const userProfileWeb3WalletsSectionModule: StoryModule = { meta: userProfileWeb3WalletsSectionMeta, diff --git a/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts b/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts new file mode 100644 index 00000000000..a0bc7e21bfb --- /dev/null +++ b/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts @@ -0,0 +1,64 @@ +import type { + UserProfileConnectedAccount, + UserProfileConnectionProvider, +} from '@clerk/ui/mosaic/user-profile/user-profile-connected-accounts-section.view'; +import { useState } from 'react'; + +export const connectedAccount = { + id: 'google', + provider: 'Google', + identifier: 'test@example.com', + iconUrl: 'https://img.clerk.com/static/google.svg', +}; +const connectionProviders: UserProfileConnectionProvider[] = [ + { id: 'google', provider: 'Google', iconUrl: 'https://img.clerk.com/static/google.svg' }, + { id: 'apple', provider: 'Apple', iconUrl: 'https://img.clerk.com/static/apple.svg' }, +]; + +export function useConnectedAccountsFixture({ + initialAccounts = [connectedAccount], + providers = connectionProviders, + removalState, +}: { + initialAccounts?: UserProfileConnectedAccount[]; + providers?: UserProfileConnectionProvider[]; + removalState?: 'pending' | 'error'; +} = {}) { + const [accounts, setAccounts] = useState(initialAccounts); + const availableProviders = providers.filter(provider => !accounts.some(account => account.id === provider.id)); + + return { + accounts, + availableProviders, + onConnect: (id: string) => { + const provider = providers.find(item => item.id === id); + if (provider) { + setAccounts(current => [ + ...current, + { id: provider.id, provider: provider.provider, iconUrl: provider.iconUrl, identifier: 'test@example.com' }, + ]); + } + }, + onReconnect: (id: string) => + setAccounts(current => + current.map(account => + account.id === id + ? { ...account, status: 'connected', reconnectError: undefined, verificationError: undefined } + : account, + ), + ), + onRemove: (id: string) => + setAccounts(current => { + const account = current.find(item => item.id === id); + if (removalState === 'pending') { + return current.map(item => (item.id === id ? { ...item, isRemoving: true } : item)); + } + if (removalState === 'error' && account && !account.removalError) { + return current.map(item => + item.id === id ? { ...item, removalError: 'Unable to remove this account. Please try again.' } : item, + ); + } + return current.filter(item => item.id !== id); + }), + }; +} diff --git a/packages/swingset/src/stories/fixtures/user-profile.ts b/packages/swingset/src/stories/fixtures/user-profile.ts index 6de6dfa4197..5186d371ce0 100644 --- a/packages/swingset/src/stories/fixtures/user-profile.ts +++ b/packages/swingset/src/stories/fixtures/user-profile.ts @@ -17,6 +17,7 @@ import { useMemo, useState } from 'react'; import { usePreviewImage } from './use-preview-image'; import { createUserProfileAddPhoneFixture } from './user-profile-add-phone'; +import { useConnectedAccountsFixture } from './user-profile-connected-accounts'; import { useUserProfileEditNameFixture } from './user-profile-edit-name'; import { useUserProfileEditUsernameFixture } from './user-profile-edit-username'; @@ -48,6 +49,7 @@ const initialAPIKeys: UserProfileAPIKey[] = [ * stories that need a realistic profile surface without being about it. */ export function useUserProfileFixture({ onAddEmail }: UserProfileFixtureOptions = {}) { + const connections = useConnectedAccountsFixture(); const editName = useUserProfileEditNameFixture(); const editUsername = useUserProfileEditUsernameFixture(); const [activePage, setActivePage] = useState('account'); @@ -119,6 +121,11 @@ export function useUserProfileFixture({ onAddEmail }: UserProfileFixtureOptions account: { ...editName, ...editUsername, + connectedAccounts: connections.accounts, + availableConnectionProviders: connections.availableProviders, + onConnectAccount: connections.onConnect, + onReconnectAccount: connections.onReconnect, + onRemoveConnectedAccount: connections.onRemove, allowMultipleAccounts: true, hasImage: Boolean(imageUrl), imageUrl, diff --git a/packages/swingset/src/stories/user-profile-connected-accounts-section.mdx b/packages/swingset/src/stories/user-profile-connected-accounts-section.mdx index ef3e6e1564e..ea938dfcc4f 100644 --- a/packages/swingset/src/stories/user-profile-connected-accounts-section.mdx +++ b/packages/swingset/src/stories/user-profile-connected-accounts-section.mdx @@ -2,14 +2,40 @@ import * as Stories from './user-profile-connected-accounts-section.stories'; # UserProfileConnectedAccountsSection -Connected and available external identity providers with provider-specific actions. - - +Linked accounts and available providers are supplied separately. These examples simulate state through props; they do not start OAuth or mutate a user. Reload to reset interactive examples. + + + +## Linked Accounts + + + +## Connect Only + + + +## Reconnect Required + +Recoverable verification errors and additional scope requirements show a Disconnected badge. Choose Reconnect from the account menu to restore the connection. + + + +## Verification Error + + + +## Connection Error + + + +## Removal Pending + +Choose Remove from the row menu, then confirm to show the pending state. + + + +## Removal Error + +The first removal attempt shows an error inside the dialog. Retry succeeds. + + diff --git a/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx b/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx index 41ce1a8260a..bd4276360a7 100644 --- a/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx +++ b/packages/swingset/src/stories/user-profile-connected-accounts-section.stories.tsx @@ -2,8 +2,9 @@ import { UserProfileConnectedAccountsSectionView } from '@clerk/ui/mosaic/featur import type { StoryMeta } from '@/lib/types'; -export { default as __source } from './user-profile-connected-accounts-section.stories?raw'; +import { connectedAccount, useConnectedAccountsFixture } from './fixtures/user-profile-connected-accounts'; +export { default as __source } from './user-profile-connected-accounts-section.stories?raw'; export const meta: StoryMeta = { group: 'User Profile', status: 'wip', @@ -12,27 +13,49 @@ export const meta: StoryMeta = { navigation: { category: 'Sections' }, source: 'packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts-section.view.tsx', }; - export function Default() { - return ( - undefined} - onManage={() => undefined} - /> - ); + const fixture = useConnectedAccountsFixture(); + return ; +} +export function LinkedAccounts() { + const fixture = useConnectedAccountsFixture({ providers: [connectedAccount] }); + return ; +} +export function ConnectOnly() { + const fixture = useConnectedAccountsFixture({ initialAccounts: [] }); + return ; +} +export function ReconnectRequired() { + const fixture = useConnectedAccountsFixture({ initialAccounts: [{ ...connectedAccount, status: 'reconnect' }] }); + return ; +} +export function VerificationError() { + const fixture = useConnectedAccountsFixture({ + initialAccounts: [ + { ...connectedAccount, status: 'error', verificationError: 'The provider could not verify this account.' }, + ], + }); + return ; +} +export function ConnectionError() { + const fixture = useConnectedAccountsFixture({ + providers: [ + connectedAccount, + { + id: 'apple', + provider: 'Apple', + iconUrl: 'https://img.clerk.com/static/apple.svg', + connectError: 'Unable to connect. Please try again.', + }, + ], + }); + return ; +} +export function RemovalPending() { + const fixture = useConnectedAccountsFixture({ removalState: 'pending' }); + return ; +} +export function RemovalError() { + const fixture = useConnectedAccountsFixture({ removalState: 'error' }); + return ; } diff --git a/packages/swingset/src/stories/user-profile-profile-panel.stories.tsx b/packages/swingset/src/stories/user-profile-profile-panel.stories.tsx index e8090770faf..7cca7d8322e 100644 --- a/packages/swingset/src/stories/user-profile-profile-panel.stories.tsx +++ b/packages/swingset/src/stories/user-profile-profile-panel.stories.tsx @@ -9,6 +9,7 @@ import type { StoryMeta } from '@/lib/types'; import { usePreviewImage } from './fixtures/use-preview-image'; import { createUserProfileAddPhoneFixture } from './fixtures/user-profile-add-phone'; +import { useConnectedAccountsFixture } from './fixtures/user-profile-connected-accounts'; import { useUserProfileEditNameFixture } from './fixtures/user-profile-edit-name'; import { useUserProfileEditUsernameFixture } from './fixtures/user-profile-edit-username'; @@ -35,6 +36,7 @@ export function Default(_args: Record) { { id: 'phone_1', value: '+1 801-888-8181', isDefault: true, isVerified: true }, ]); const { imageUrl, showFile, clearImage } = usePreviewImage(profileImageUrl); + const connections = useConnectedAccountsFixture(); const editName = useUserProfileEditNameFixture(); const editUsername = useUserProfileEditUsernameFixture(); @@ -44,16 +46,9 @@ export function Default(_args: Record) { {...editUsername} allowMultipleAccounts emails={emails} - connectedAccounts={[ - { - id: 'google', - provider: 'Google', - identifier: 'test@google.com', - iconUrl: providerIconUrl('google'), - connected: true, - }, - { id: 'apple', provider: 'Apple', iconUrl: providerIconUrl('apple'), connected: false }, - ]} + connectedAccounts={connections.accounts} + availableConnectionProviders={connections.availableProviders} + onReconnectAccount={connections.onReconnect} web3Wallets={[ { id: 'metamask', @@ -82,12 +77,12 @@ export function Default(_args: Record) { {...createUserProfileAddPhoneFixture({ onVerified: value => setPhones(current => [...current, { id: `phone_${Date.now()}`, value, isVerified: true }]), })} - onConnectAccount={() => undefined} + onConnectAccount={connections.onConnect} onDeleteAccount={() => Promise.resolve()} onManageEmail={() => undefined} onManagePhone={() => undefined} onProfilePictureChange={showFile} - onRemoveConnectedAccount={() => undefined} + onRemoveConnectedAccount={connections.onRemove} onRemoveProfilePicture={clearImage} onRemoveEmail={id => setEmails(current => current.filter(email => email.id !== id))} onRemovePhone={id => setPhones(current => current.filter(phone => phone.id !== id))} From bda4612cdc7f1939ed22375eac2824e19ba55b78 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Mon, 14 Sep 2026 16:16:12 -0600 Subject: [PATCH 03/11] fix(ui): frame connected account provider icons --- .../user-profile-profile-panel.view.test.tsx | 7 +++-- .../user-profile-connect-account-row.view.tsx | 2 +- ...ser-profile-connected-account-row.view.tsx | 2 +- .../user-profile-connected-provider-icon.tsx | 31 +++++++++++-------- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx index d29e2d75be3..9d12f4849ac 100644 --- a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx +++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx @@ -241,7 +241,7 @@ describe('UserProfileProfilePanelView', () => { expect(onDeleteAccount).toHaveBeenCalledOnce(); }); - it('renders connected provider images and Web3 icon frames', () => { + it('renders connected provider and Web3 images inside icon frames', () => { const { container } = renderView({ connectedAccounts: [{ id: 'google', provider: 'Google', iconUrl: '/google.svg' }], web3Wallets: [{ id: 'metamask', provider: 'MetaMask', iconUrl: '/metamask.svg' }], @@ -249,9 +249,10 @@ describe('UserProfileProfilePanelView', () => { const frames = container.querySelectorAll('.cl-icon-frame'); const images = container.querySelectorAll('img'); - expect(frames).toHaveLength(1); + expect(frames).toHaveLength(2); expect(screen.getByRole('img', { name: 'Google' })).toHaveAttribute('src', '/google.svg'); - expect(frames[0]).toContainElement(images[1]); + expect(frames[0]).toContainElement(images[0]); + expect(frames[1]).toContainElement(images[1]); frames.forEach(frame => expect(frame.closest('.cl-section-media')).toHaveAttribute('data-size', 'lg')); }); diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connect-account-row.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connect-account-row.view.tsx index 645f26b3930..48c2243f9a5 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connect-account-row.view.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connect-account-row.view.tsx @@ -16,7 +16,7 @@ export function UserProfileConnectAccountRowView({ return ( - + diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx index 88bbe1f973c..3202c533ef5 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx @@ -30,7 +30,7 @@ export function UserProfileConnectedAccountRowView({ return ( - + diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-provider-icon.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-provider-icon.tsx index d529794b91c..5887671e17d 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-provider-icon.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-provider-icon.tsx @@ -1,20 +1,25 @@ import * as stylex from '@stylexjs/stylex'; +import { IconFrame } from '../components/icon'; import { styles } from './user-profile-connected-accounts.styles'; export function UserProfileConnectedProviderIcon({ provider, iconUrl }: { provider: string; iconUrl?: string }) { - return iconUrl?.trim() ? ( - {provider} - ) : ( - - {provider.trim().charAt(0).toUpperCase()} - + return ( + + {iconUrl?.trim() ? ( + {provider} + ) : ( + + {provider.trim().charAt(0).toUpperCase()} + + )} + ); } From 91587d3b9d47ab630648246cbc84eac3dc852d68 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Mon, 14 Sep 2026 16:20:28 -0600 Subject: [PATCH 04/11] refactor(ui): share connected and available account row layout --- .../user-profile-connect-account-row.view.tsx | 45 ------------------- ...ser-profile-connected-account-row.view.tsx | 26 ++++++++++- ...rofile-connected-accounts-section.view.tsx | 5 +-- 3 files changed, 26 insertions(+), 50 deletions(-) delete mode 100644 packages/ui/src/mosaic/features/user-profile/user-profile-connect-account-row.view.tsx diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connect-account-row.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connect-account-row.view.tsx deleted file mode 100644 index 48c2243f9a5..00000000000 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connect-account-row.view.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Button } from '../components/button'; -import { Icon } from '../components/icon'; -import { Section } from '../components/section'; -import { userProfileConnectedAccountsMessages as m } from './user-profile-connected-accounts.messages'; -import { styles } from './user-profile-connected-accounts.styles'; -import type { UserProfileConnectionProvider } from './user-profile-connected-accounts-section.view'; -import { UserProfileConnectedProviderIcon } from './user-profile-connected-provider-icon'; - -export function UserProfileConnectAccountRowView({ - provider, - onConnect, -}: { - provider: UserProfileConnectionProvider; - onConnect: (id: string) => void; -}) { - return ( - - - - - - - {provider.provider} - - - - - - {provider.connectError ? {provider.connectError} : null} - - ); -} diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx index 3202c533ef5..7eac090db7a 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx @@ -1,6 +1,8 @@ import { useState } from 'react'; import { Badge } from '../components/badge'; +import { Button } from '../components/button'; +import { Icon } from '../components/icon'; import { Section } from '../components/section'; import type { UserProfileMenuAction } from './user-profile-action-menu'; import { UserProfileActionMenu } from './user-profile-action-menu'; @@ -12,10 +14,12 @@ import { UserProfileRemoveConnectedAccountDialog } from './user-profile-remove-c export function UserProfileConnectedAccountRowView({ account, + onConnect, onReconnect, onRemove, }: { account: UserProfileConnectedAccount; + onConnect?: (id: string) => void; onReconnect?: (id: string) => void; onRemove?: (id: string) => void; }) { @@ -28,7 +32,7 @@ export function UserProfileConnectedAccountRowView({ actions.push({ label: m.remove, color: 'negative', onClick: () => setOpen(true) }); } return ( - + @@ -47,7 +51,24 @@ export function UserProfileConnectedAccountRowView({ ) : null} - {actions.length > 0 ? ( + {onConnect ? ( + + + + ) : actions.length > 0 ? ( ) : null} + {account.connectError ? {account.connectError} : null} {account.reconnectError ? {account.reconnectError} : null} {account.status === 'error' && account.verificationError ? ( {account.verificationError} diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts-section.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts-section.view.tsx index 38174f80c86..8b5dca4144c 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts-section.view.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts-section.view.tsx @@ -1,5 +1,4 @@ import { Section } from '../../components/section'; -import { UserProfileConnectAccountRowView } from './user-profile-connect-account-row.view'; import { UserProfileConnectedAccountRowView } from './user-profile-connected-account-row.view'; import { userProfileConnectedAccountsMessages as m } from './user-profile-connected-accounts.messages'; @@ -50,9 +49,9 @@ export function UserProfileConnectedAccountsSectionView({ ))} {onConnect ? availableProviders.map(provider => ( - )) From 0c24e138c81d8d8f538de8ce059de94d71f46137 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Mon, 14 Sep 2026 16:29:47 -0600 Subject: [PATCH 05/11] refactor(ui): inline connected provider icon in account row --- ...ser-profile-connected-account-row.view.tsx | 22 +++++++++++++--- .../user-profile-connected-provider-icon.tsx | 25 ------------------- 2 files changed, 19 insertions(+), 28 deletions(-) delete mode 100644 packages/ui/src/mosaic/features/user-profile/user-profile-connected-provider-icon.tsx diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx index 7eac090db7a..eb48a68eac0 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx @@ -1,15 +1,15 @@ +import * as stylex from '@stylexjs/stylex'; import { useState } from 'react'; import { Badge } from '../components/badge'; import { Button } from '../components/button'; -import { Icon } from '../components/icon'; +import { Icon, IconFrame } from '../components/icon'; import { Section } from '../components/section'; import type { UserProfileMenuAction } from './user-profile-action-menu'; import { UserProfileActionMenu } from './user-profile-action-menu'; import { userProfileConnectedAccountsMessages as m } from './user-profile-connected-accounts.messages'; import { styles } from './user-profile-connected-accounts.styles'; import type { UserProfileConnectedAccount } from './user-profile-connected-accounts-section.view'; -import { UserProfileConnectedProviderIcon } from './user-profile-connected-provider-icon'; import { UserProfileRemoveConnectedAccountDialog } from './user-profile-remove-connected-account.dialog'; export function UserProfileConnectedAccountRowView({ @@ -24,6 +24,7 @@ export function UserProfileConnectedAccountRowView({ onRemove?: (id: string) => void; }) { const [open, setOpen] = useState(false); + const iconUrl = account.iconUrl?.trim(); const actions: UserProfileMenuAction[] = []; if (account.status === 'reconnect' && onReconnect) { actions.push({ label: m.reconnect, onClick: () => onReconnect(account.id) }); @@ -35,7 +36,22 @@ export function UserProfileConnectedAccountRowView({ - + + {iconUrl ? ( + {account.provider} + ) : ( + + {account.provider.trim().charAt(0).toUpperCase()} + + )} + diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-provider-icon.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-provider-icon.tsx deleted file mode 100644 index 5887671e17d..00000000000 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-provider-icon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as stylex from '@stylexjs/stylex'; - -import { IconFrame } from '../components/icon'; -import { styles } from './user-profile-connected-accounts.styles'; - -export function UserProfileConnectedProviderIcon({ provider, iconUrl }: { provider: string; iconUrl?: string }) { - return ( - - {iconUrl?.trim() ? ( - {provider} - ) : ( - - {provider.trim().charAt(0).toUpperCase()} - - )} - - ); -} From 286b48435e4a9ad2fef2f4eb557569a01aa0c59f Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Mon, 14 Sep 2026 16:35:13 -0600 Subject: [PATCH 06/11] fix(ui): hide connected account section without available actions --- .../__tests__/user-profile-profile-panel.view.test.tsx | 8 ++++++++ .../user-profile/user-profile-profile-panel.view.tsx | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx index 9d12f4849ac..8d1b5475988 100644 --- a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx +++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx @@ -29,6 +29,14 @@ function renderView(overrides: Partial = {}) { } describe('UserProfileProfilePanelView', () => { + it('hides connected accounts when only providers without a connect callback are supplied', () => { + renderView({ + connectedAccounts: [], + availableConnectionProviders: [{ id: 'google', provider: 'Google' }], + }); + expect(screen.queryByRole('region', { name: 'Connected accounts' })).not.toBeInTheDocument(); + }); + it('keeps available providers visible without connected accounts and forwards connect', async () => { const onConnectAccount = vi.fn(); const user = userEvent.setup(); diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-profile-panel.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-profile-panel.view.tsx index e9b2792a731..754c77ec83a 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-profile-panel.view.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-profile-panel.view.tsx @@ -114,7 +114,7 @@ export function UserProfileProfilePanelView({ onSubmitName={onSubmitName} onSubmitUsername={onSubmitUsername} /> - {connectedAccounts.length > 0 || availableConnectionProviders.length > 0 ? ( + {connectedAccounts.length > 0 || (availableConnectionProviders.length > 0 && onConnectAccount) ? ( Date: Mon, 14 Sep 2026 16:56:21 -0600 Subject: [PATCH 07/11] fix(ui): mark connected account logos as decorative --- ...er-profile-connected-accounts-section.view.test.tsx | 10 ++++++++++ .../user-profile-connected-account-row.view.tsx | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx index d76a3ffc6f4..6c64f5f677d 100644 --- a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx +++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx @@ -7,6 +7,16 @@ import { UserProfileConnectedAccountsSectionView } from '../user-profile-connect const account = { id: 'account_1', provider: 'Google', identifier: 'test@example.com' }; describe('connected accounts section', () => { + it('shows provider names without announcing decorative logos', () => { + render( + , + ); + expect(screen.getByText('Google')).toBeVisible(); + expect(screen.queryByRole('img')).not.toBeInTheDocument(); + }); + it('forwards connect actions and allows retrying after an error', async () => { const user = userEvent.setup(); const onConnect = vi.fn(); diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx index eb48a68eac0..230e81e80f6 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx @@ -40,7 +40,8 @@ export function UserProfileConnectedAccountRowView({ {iconUrl ? ( {account.provider} ) : ( From 450738857ce80e38698a57c86b490e767d60322e Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Mon, 14 Sep 2026 17:46:28 -0600 Subject: [PATCH 08/11] fix(ui): hide empty connected accounts section --- ...e-connected-accounts-section.view.test.tsx | 13 ++++++ ...rofile-connected-accounts-section.view.tsx | 44 ++++++++++--------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx index 6c64f5f677d..747ac8ec22b 100644 --- a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx +++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx @@ -7,6 +7,19 @@ import { UserProfileConnectedAccountsSectionView } from '../user-profile-connect const account = { id: 'account_1', provider: 'Google', identifier: 'test@example.com' }; describe('connected accounts section', () => { + it.each([{ availableProviders: [] }, { availableProviders: [{ id: 'google', provider: 'Google' }] }])( + 'hides the entire section without accounts or actionable providers (%j)', + ({ availableProviders }) => { + const { container } = render( + , + ); + expect(container).toBeEmptyDOMElement(); + }, + ); + it('shows provider names without announcing decorative logos', () => { render( {m.title} - {accounts.length > 0 || (availableProviders.length > 0 && onConnect) ? ( - - {accounts.map(account => ( - - ))} - {onConnect - ? availableProviders.map(provider => ( - - )) - : null} - - ) : null} + + {accounts.map(account => ( + + ))} + {onConnect + ? availableProviders.map(provider => ( + + )) + : null} + ); } From 0c654fad24ca9e132a5eafe8024d3b8c6b27f86a Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Mon, 14 Sep 2026 17:49:02 -0600 Subject: [PATCH 09/11] test(ui): trim duplicate connected account callback checks --- ...e-connected-accounts-section.view.test.tsx | 31 +++++-------------- .../user-profile-profile-panel.view.test.tsx | 17 +++------- 2 files changed, 11 insertions(+), 37 deletions(-) diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx index 747ac8ec22b..f7cd162a1ec 100644 --- a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx +++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-section.view.test.tsx @@ -30,33 +30,17 @@ describe('connected accounts section', () => { expect(screen.queryByRole('img')).not.toBeInTheDocument(); }); - it('forwards connect actions and allows retrying after an error', async () => { - const user = userEvent.setup(); - const onConnect = vi.fn(); - const availableProviders = [ - { id: 'apple', provider: 'Apple' }, - { id: 'github', provider: 'GitHub' }, - ]; - const { rerender } = render( - , - ); - await user.click(screen.getByRole('button', { name: 'Connect Apple' })); - expect(onConnect).toHaveBeenCalledExactlyOnceWith('apple'); - expect(screen.queryByRole('menu')).not.toBeInTheDocument(); - rerender( + it('shows connection errors while keeping Connect available', () => { + render( , ); expect(screen.getByRole('alert')).toHaveTextContent('Connection failed'); - await user.click(screen.getByRole('button', { name: 'Connect Apple' })); - expect(onConnect).toHaveBeenCalledTimes(2); + expect(screen.getByRole('button', { name: 'Connect Apple' })).toBeEnabled(); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); }); it('keeps confirmation open for pending and error props and forwards retry', async () => { @@ -120,8 +104,7 @@ describe('connected accounts section', () => { expect(screen.getByText('Disconnected')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Reconnect' })).not.toBeInTheDocument(); await user.click(screen.getByRole('button', { name: 'Manage Google' })); - await user.click(screen.getByRole('menuitem', { name: 'Reconnect' })); - expect(onReconnect).toHaveBeenCalledExactlyOnceWith('account_1'); + expect(screen.getByRole('menuitem', { name: 'Reconnect' })).toBeEnabled(); expect(screen.getByText('Provider error')).toBeInTheDocument(); expect(screen.getAllByRole('alert').some(alert => alert.textContent?.includes('Reconnection failed'))).toBe(true); expect(screen.queryByRole('button', { name: 'Manage Other' })).not.toBeInTheDocument(); diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx index 8d1b5475988..78f973331ea 100644 --- a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx +++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx @@ -37,16 +37,14 @@ describe('UserProfileProfilePanelView', () => { expect(screen.queryByRole('region', { name: 'Connected accounts' })).not.toBeInTheDocument(); }); - it('keeps available providers visible without connected accounts and forwards connect', async () => { + it('keeps available providers visible without connected accounts', () => { const onConnectAccount = vi.fn(); - const user = userEvent.setup(); renderView({ connectedAccounts: [], availableConnectionProviders: [{ id: 'google', provider: 'Google' }], onConnectAccount, }); - await user.click(screen.getByRole('button', { name: 'Connect Google' })); - expect(onConnectAccount).toHaveBeenCalledExactlyOnceWith('google'); + expect(screen.getByRole('button', { name: 'Connect Google' })).toBeVisible(); }); it.each([false, true])('formats normalized phone numbers with multiple accounts set to %s', allowMultipleAccounts => { @@ -228,24 +226,17 @@ describe('UserProfileProfilePanelView', () => { }); expect(screen.getByRole('heading', { level: 4, name: 'Connected accounts' })).toBeInTheDocument(); - expect(screen.getByRole('img', { name: 'Google' })).toHaveAttribute('src', 'https://example.com/google.svg'); + expect(screen.getByText('Google')).toBeVisible(); expect(screen.getByRole('heading', { level: 4, name: 'Danger zone' })).toBeInTheDocument(); expect(screen.getByText('Delete account', { selector: '.cl-section-label' })).toBeInTheDocument(); expect(screen.getByText('Permanently delete this account and all its data. This cannot be undone.')).toHaveClass( 'cl-section-description', ); - await user.click(screen.getByRole('button', { name: 'Manage Google' })); - expect(onRemoveConnectedAccount).not.toHaveBeenCalled(); - await user.click(screen.getByRole('menuitem', { name: 'Remove' })); - await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove' })); - await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Cancel' })); - await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()); await user.click(screen.getByRole('button', { name: 'Delete account' })); const deleteDialog = screen.getByRole('dialog'); await user.type(within(deleteDialog).getByRole('textbox'), 'Delete account'); await user.click(within(deleteDialog).getByRole('button', { name: 'Delete account' })); - expect(onRemoveConnectedAccount).toHaveBeenCalledWith('google'); expect(onDeleteAccount).toHaveBeenCalledOnce(); }); @@ -258,7 +249,7 @@ describe('UserProfileProfilePanelView', () => { const frames = container.querySelectorAll('.cl-icon-frame'); const images = container.querySelectorAll('img'); expect(frames).toHaveLength(2); - expect(screen.getByRole('img', { name: 'Google' })).toHaveAttribute('src', '/google.svg'); + expect(screen.queryByRole('img', { name: 'Google' })).not.toBeInTheDocument(); expect(frames[0]).toContainElement(images[0]); expect(frames[1]).toContainElement(images[1]); frames.forEach(frame => expect(frame.closest('.cl-section-media')).toHaveAttribute('data-size', 'lg')); From 366fb8014f5ce6ae820abb9434bd0c26debb1d36 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Mon, 14 Sep 2026 17:57:19 -0600 Subject: [PATCH 10/11] fix(ui): template connected account action labels --- .../user-profile/user-profile-connected-account-row.view.tsx | 4 ++-- .../user-profile/user-profile-connected-accounts.messages.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx index 230e81e80f6..3920e01deab 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx @@ -74,7 +74,7 @@ export function UserProfileConnectedAccountRowView({ size='sm' variant='outline' color='neutral' - aria-label={`${m.connect} ${account.provider}`} + aria-label={m.connectLabel.replace('{provider}', account.provider)} onClick={() => onConnect(account.id)} > {m.connect} @@ -88,7 +88,7 @@ export function UserProfileConnectedAccountRowView({ ) : actions.length > 0 ? ( {onRemove && account.canRemove !== false ? ( diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.messages.ts b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.messages.ts index 5f86638e39d..1efd85fdc17 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.messages.ts +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.messages.ts @@ -1,9 +1,10 @@ export const userProfileConnectedAccountsMessages = { title: 'Connected accounts', connect: 'Connect', + connectLabel: 'Connect {provider}', reconnect: 'Reconnect', disconnected: 'Disconnected', - manage: 'Manage', + manageLabel: 'Manage {provider}', remove: 'Remove', removeDialog: { title: 'Remove connected account', From 56181f97c9a0496684ae23efdcadb26ec6204afe Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Tue, 15 Sep 2026 10:46:57 -0600 Subject: [PATCH 11/11] fix(ui): update connected account imports after feature move --- .../fixtures/user-profile-connected-accounts.ts | 2 +- .../user-profile-connected-accounts-actions.test.tsx | 2 +- .../user-profile-connected-account-row.view.tsx | 8 ++++---- .../user-profile-connected-accounts.styles.ts | 2 +- .../user-profile-remove-connected-account.dialog.tsx | 10 +++++----- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts b/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts index a0bc7e21bfb..b2ddebf7df9 100644 --- a/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts +++ b/packages/swingset/src/stories/fixtures/user-profile-connected-accounts.ts @@ -1,7 +1,7 @@ import type { UserProfileConnectedAccount, UserProfileConnectionProvider, -} from '@clerk/ui/mosaic/user-profile/user-profile-connected-accounts-section.view'; +} from '@clerk/ui/mosaic/features/user-profile/user-profile-connected-accounts-section.view'; import { useState } from 'react'; export const connectedAccount = { diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-actions.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-actions.test.tsx index eefd8b99e4a..d9ef673f720 100644 --- a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-actions.test.tsx +++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-connected-accounts-actions.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import { useState } from 'react'; import { describe, expect, it, vi } from 'vitest'; -import { MosaicProvider } from '../../MosaicProvider'; +import { MosaicProvider } from '../../../MosaicProvider'; import { UserProfileConnectedAccountsSectionView } from '../user-profile-connected-accounts-section.view'; const account = { id: 'github', provider: 'GitHub', identifier: 'test' }; diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx index 3920e01deab..e2b1b7152a4 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-account-row.view.tsx @@ -1,10 +1,10 @@ import * as stylex from '@stylexjs/stylex'; import { useState } from 'react'; -import { Badge } from '../components/badge'; -import { Button } from '../components/button'; -import { Icon, IconFrame } from '../components/icon'; -import { Section } from '../components/section'; +import { Badge } from '../../components/badge'; +import { Button } from '../../components/button'; +import { Icon, IconFrame } from '../../components/icon'; +import { Section } from '../../components/section'; import type { UserProfileMenuAction } from './user-profile-action-menu'; import { UserProfileActionMenu } from './user-profile-action-menu'; import { userProfileConnectedAccountsMessages as m } from './user-profile-connected-accounts.messages'; diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.styles.ts b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.styles.ts index 8c785da208b..1cc5a4d1947 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.styles.ts +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-connected-accounts.styles.ts @@ -1,6 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, radiusVars, space } from '../tokens.stylex'; +import { colorVars, radiusVars, space } from '../../tokens.stylex'; export const styles = stylex.create({ connectRow: { justifyContent: 'center' }, diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-remove-connected-account.dialog.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-remove-connected-account.dialog.tsx index 9cd8f051173..2664ea40c4f 100644 --- a/packages/ui/src/mosaic/features/user-profile/user-profile-remove-connected-account.dialog.tsx +++ b/packages/ui/src/mosaic/features/user-profile/user-profile-remove-connected-account.dialog.tsx @@ -1,8 +1,8 @@ -import { Banner } from '../components/banner'; -import { Button, SubmitButton } from '../components/button'; -import { Dialog } from '../components/dialog'; -import { Heading } from '../components/heading'; -import { Text } from '../components/text'; +import { Banner } from '../../components/banner'; +import { Button, SubmitButton } from '../../components/button'; +import { Dialog } from '../../components/dialog'; +import { Heading } from '../../components/heading'; +import { Text } from '../../components/text'; import { userProfileConnectedAccountsMessages as m } from './user-profile-connected-accounts.messages'; export interface UserProfileRemoveConnectedAccountDialogProps {