Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .changeset/connected-account-removal-dialog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
2 changes: 2 additions & 0 deletions .changeset/connected-accounts-view-states.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
14 changes: 14 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,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,
Expand Down Expand Up @@ -532,6 +539,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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type {
UserProfileConnectedAccount,
UserProfileConnectionProvider,
} from '@clerk/ui/mosaic/features/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);
}),
};
}
7 changes: 7 additions & 0 deletions packages/swingset/src/stories/fixtures/user-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useMemo, useState } from 'react';
import { usePreviewImage } from './use-preview-image';
import { createUserProfileAddEmailFixture } from './user-profile-add-email';
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';

Expand Down Expand Up @@ -49,6 +50,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<UserProfileViewProps['activePage']>('account');
Expand Down Expand Up @@ -123,6 +125,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Story
name='Default'
storyModule={Stories}
composition={[
{ name: 'Section', href: '/components/section', layer: 'Components' },
{ name: 'Button', href: '/components/button', layer: 'Components' },
{ name: 'Icon', href: '/components/icon', layer: 'Components' },
]}
/>
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.

<Story name='Default' storyModule={Stories} />

## Linked Accounts
Comment thread
austincalvelage marked this conversation as resolved.

<Story name='LinkedAccounts' storyModule={Stories} />

## Connect Only

<Story name='ConnectOnly' storyModule={Stories} />

## Reconnect Required

Recoverable verification errors and additional scope requirements show a Disconnected badge. Choose Reconnect from the account menu to restore the connection.

<Story name='ReconnectRequired' storyModule={Stories} />

## Verification Error

<Story name='VerificationError' storyModule={Stories} />

## Connection Error

<Story name='ConnectionError' storyModule={Stories} />

## Removal Pending

Choose Remove from the row menu, then confirm to show the pending state.

<Story name='RemovalPending' storyModule={Stories} />

## Removal Error

The first removal attempt shows an error inside the dialog. Retry succeeds.

<Story name='RemovalError' storyModule={Stories} />
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 (
<UserProfileConnectedAccountsSectionView
accounts={[
{
id: 'google',
provider: 'Google',
identifier: 'test@google.com',
iconUrl: 'https://img.clerk.com/static/google.svg',
connected: true,
},
{
id: 'apple',
provider: 'Apple',
iconUrl: 'https://img.clerk.com/static/apple.svg',
connected: false,
},
]}
onConnect={() => undefined}
onManage={() => undefined}
/>
);
const fixture = useConnectedAccountsFixture();
return <UserProfileConnectedAccountsSectionView {...fixture} />;
}
export function LinkedAccounts() {
const fixture = useConnectedAccountsFixture({ providers: [connectedAccount] });
return <UserProfileConnectedAccountsSectionView {...fixture} />;
}
export function ConnectOnly() {
const fixture = useConnectedAccountsFixture({ initialAccounts: [] });
return <UserProfileConnectedAccountsSectionView {...fixture} />;
}
export function ReconnectRequired() {
const fixture = useConnectedAccountsFixture({ initialAccounts: [{ ...connectedAccount, status: 'reconnect' }] });
return <UserProfileConnectedAccountsSectionView {...fixture} />;
}
export function VerificationError() {
const fixture = useConnectedAccountsFixture({
initialAccounts: [
{ ...connectedAccount, status: 'error', verificationError: 'The provider could not verify this account.' },
],
});
return <UserProfileConnectedAccountsSectionView {...fixture} />;
}
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 <UserProfileConnectedAccountsSectionView {...fixture} />;
}
export function RemovalPending() {
const fixture = useConnectedAccountsFixture({ removalState: 'pending' });
return <UserProfileConnectedAccountsSectionView {...fixture} />;
}
export function RemovalError() {
const fixture = useConnectedAccountsFixture({ removalState: 'error' });
return <UserProfileConnectedAccountsSectionView {...fixture} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { StoryMeta } from '@/lib/types';
import { usePreviewImage } from './fixtures/use-preview-image';
import { createUserProfileAddEmailFixture } from './fixtures/user-profile-add-email';
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';

Expand All @@ -36,6 +37,7 @@ export function Default(_args: Record<string, unknown>) {
{ 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();
const emailFlow = createUserProfileAddEmailFixture({
Expand All @@ -49,16 +51,9 @@ export function Default(_args: Record<string, unknown>) {
{...emailFlow}
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',
Expand All @@ -81,12 +76,12 @@ export function Default(_args: Record<string, unknown>) {
{...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))}
Expand Down
Loading
Loading