Skip to content

Commit e5a96e4

Browse files
feat(ui): add connected accounts section
1 parent 80790d6 commit e5a96e4

7 files changed

Lines changed: 155 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

packages/swingset/src/components/DocsViewer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import { ViewSource } from './ViewSource';
1212
const docModules: Record<string, Record<string, React.ComponentType>> = {
1313
user: {
1414
'user-profile-account-section': dynamic(() => import('../stories/user-profile-account-section.mdx')),
15+
'user-profile-connected-accounts-section': dynamic(
16+
() => import('../stories/user-profile-connected-accounts-section.mdx'),
17+
),
1518
},
1619
organization: {
1720
'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')),

packages/swingset/src/lib/registry.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ import {
131131
Default as UserProfileAccountSectionDefault,
132132
meta as userProfileAccountSectionMeta,
133133
} from '../stories/user-profile-account-section.stories';
134+
import {
135+
Default as UserProfileConnectedAccountsSectionDefault,
136+
meta as userProfileConnectedAccountsSectionMeta,
137+
} from '../stories/user-profile-connected-accounts-section.stories';
134138
import { toSlug } from './slug';
135139
import type { StoryModule } from './types';
136140

@@ -272,10 +276,15 @@ const userProfileAccountSectionModule: StoryModule = {
272276
meta: userProfileAccountSectionMeta,
273277
Default: UserProfileAccountSectionDefault,
274278
};
279+
const userProfileConnectedAccountsSectionModule: StoryModule = {
280+
meta: userProfileConnectedAccountsSectionMeta,
281+
Default: UserProfileConnectedAccountsSectionDefault,
282+
};
275283

276284
export const registry: StoryModule[] = [
277285
// User
278286
userProfileAccountSectionModule,
287+
userProfileConnectedAccountsSectionModule,
279288
// Organization
280289
organizationProfileModule,
281290
organizationProfileGeneralPanelModule,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as Stories from './user-profile-connected-accounts-section.stories';
2+
3+
# UserProfileConnectedAccountsSection
4+
5+
Connected and available external identity providers with provider-specific actions.
6+
7+
<Story name='Default' storyModule={Stories} composition={[{ name: 'SettingsGroup', href: '/blocks/settings-group', layer: 'Blocks' }]} />
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/** @jsxImportSource @emotion/react */
2+
import { UserProfileConnectedAccountsSectionView } from '@clerk/ui/mosaic/user-profile/user-profile-connected-accounts-section.view';
3+
4+
import type { StoryMeta } from '@/lib/types';
5+
6+
export { default as __source } from './user-profile-connected-accounts-section.stories?raw';
7+
8+
export const meta: StoryMeta = {
9+
group: 'User',
10+
title: 'UserProfileConnectedAccountsSection',
11+
source: 'packages/ui/src/mosaic/user-profile/user-profile-connected-accounts-section.view.tsx',
12+
styleEngine: 'stylex',
13+
};
14+
15+
export function Default() {
16+
return (
17+
<UserProfileConnectedAccountsSectionView
18+
accounts={[
19+
{
20+
id: 'google',
21+
provider: 'Google',
22+
identifier: 'test@google.com',
23+
iconUrl: 'https://img.clerk.com/static/google.svg',
24+
connected: true,
25+
},
26+
{
27+
id: 'apple',
28+
provider: 'Apple',
29+
iconUrl: 'https://img.clerk.com/static/apple.svg',
30+
connected: false,
31+
},
32+
]}
33+
onConnect={() => undefined}
34+
onManage={() => undefined}
35+
/>
36+
);
37+
}

packages/ui/src/mosaic/icons/registry.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ const Plus = glyph(
8080
/>,
8181
);
8282

83+
const ArrowRightTop = glyph(
84+
<path
85+
d='M6.35014 5.40727L10.8285 5.17157M10.8285 5.17157L10.5928 9.64991M10.8285 5.17157L5.17163 10.8284'
86+
{...strokeProps}
87+
/>,
88+
);
89+
8390
const Pen = glyph(
8491
<path
8592
clipRule='evenodd'
@@ -115,6 +122,7 @@ const AlertCircle = glyph(
115122
/** Runtime name → glyph map. `Icon`'s `name` prop is typed from these keys. */
116123
export const iconRegistry = {
117124
'alert-circle': AlertCircle,
125+
'arrow-right-top': ArrowRightTop,
118126
'chevron-right': ChevronRight,
119127
'chevron-left': ChevronLeft,
120128
'chevron-down': ChevronDown,
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import * as stylex from '@stylexjs/stylex';
2+
3+
import { SettingsGroup } from '../block/settings-group';
4+
import { Button } from '../components/button';
5+
import { Icon } from '../components/icon';
6+
import { space } from '../tokens.stylex';
7+
import type { UserProfileMenuAction } from './user-profile-action-menu';
8+
import { UserProfileActionMenu } from './user-profile-action-menu';
9+
import { styles } from './user-profile-profile-panel.styles';
10+
11+
export interface UserProfileConnectedAccount {
12+
id: string;
13+
provider: string;
14+
identifier?: string;
15+
iconUrl?: string;
16+
connected?: boolean;
17+
canRemove?: boolean;
18+
}
19+
20+
export interface UserProfileConnectedAccountsSectionViewProps {
21+
accounts: UserProfileConnectedAccount[];
22+
onConnect?: (id: string) => void;
23+
onManage?: (id: string) => void;
24+
onRemove?: (id: string) => void;
25+
}
26+
27+
export function UserProfileConnectedAccountsSectionView({
28+
accounts,
29+
onConnect,
30+
onManage,
31+
onRemove,
32+
}: UserProfileConnectedAccountsSectionViewProps) {
33+
return (
34+
<SettingsGroup.Root title='Connected accounts'>
35+
<SettingsGroup.List>
36+
{accounts.map(account => {
37+
const connected = account.connected ?? Boolean(account.identifier);
38+
const actions: UserProfileMenuAction[] = [];
39+
if (onRemove && account.canRemove !== false) {
40+
actions.push({ label: 'Remove', color: 'negative', onClick: () => onRemove(account.id) });
41+
} else if (!onRemove && onManage) {
42+
actions.push({ label: 'Manage', onClick: () => onManage(account.id) });
43+
}
44+
45+
return (
46+
<SettingsGroup.Row
47+
key={account.id}
48+
style={{ columnGap: space['3'] }}
49+
>
50+
{account.iconUrl ? (
51+
<SettingsGroup.Media>
52+
<img
53+
alt=''
54+
src={account.iconUrl}
55+
{...stylex.props(styles.providerIcon)}
56+
/>
57+
</SettingsGroup.Media>
58+
) : null}
59+
<SettingsGroup.Label description={account.identifier}>{account.provider}</SettingsGroup.Label>
60+
<SettingsGroup.Control>
61+
{connected ? (
62+
<UserProfileActionMenu
63+
actions={actions}
64+
label={`Manage ${account.provider}`}
65+
/>
66+
) : null}
67+
{!connected && onConnect ? (
68+
<Button
69+
color='neutral'
70+
size='sm'
71+
variant='outline'
72+
onClick={() => onConnect(account.id)}
73+
>
74+
Connect
75+
<Icon
76+
name='arrow-right-top'
77+
placement='inline-end'
78+
size='sm'
79+
/>
80+
</Button>
81+
) : null}
82+
</SettingsGroup.Control>
83+
</SettingsGroup.Row>
84+
);
85+
})}
86+
</SettingsGroup.List>
87+
</SettingsGroup.Root>
88+
);
89+
}

0 commit comments

Comments
 (0)