Skip to content

Commit f4bc382

Browse files
feat(ui): add Web3 wallets section
1 parent 5f664e7 commit f4bc382

7 files changed

Lines changed: 186 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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
1616
'user-profile-connected-accounts-section': dynamic(
1717
() => import('../stories/user-profile-connected-accounts-section.mdx'),
1818
),
19+
'user-profile-web3wallets-section': dynamic(() => import('../stories/user-profile-web3-wallets-section.mdx')),
1920
},
2021
organization: {
2122
'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
@@ -138,6 +138,10 @@ import {
138138
Default as UserProfileConnectedAccountsSectionDefault,
139139
meta as userProfileConnectedAccountsSectionMeta,
140140
} from '../stories/user-profile-connected-accounts-section.stories';
141+
import {
142+
Default as UserProfileWeb3WalletsSectionDefault,
143+
meta as userProfileWeb3WalletsSectionMeta,
144+
} from '../stories/user-profile-web3-wallets-section.stories';
141145
import { toSlug } from './slug';
142146
import type { StoryModule } from './types';
143147

@@ -287,12 +291,17 @@ const userProfileConnectedAccountsSectionModule: StoryModule = {
287291
meta: userProfileConnectedAccountsSectionMeta,
288292
Default: UserProfileConnectedAccountsSectionDefault,
289293
};
294+
const userProfileWeb3WalletsSectionModule: StoryModule = {
295+
meta: userProfileWeb3WalletsSectionMeta,
296+
Default: UserProfileWeb3WalletsSectionDefault,
297+
};
290298

291299
export const registry: StoryModule[] = [
292300
// User
293301
userButtonModule,
294302
userProfileAccountSectionModule,
295303
userProfileConnectedAccountsSectionModule,
304+
userProfileWeb3WalletsSectionModule,
296305
// Organization
297306
organizationProfileModule,
298307
organizationProfileGeneralPanelModule,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as Stories from './user-profile-web3-wallets-section.stories';
2+
3+
# UserProfileWeb3WalletsSection
4+
5+
Web3 wallet identities, verification state, primary status, and wallet actions.
6+
7+
<Story
8+
name='Default'
9+
storyModule={Stories}
10+
composition={[
11+
{ name: 'Section', href: '/components/section', layer: 'Components' },
12+
{ name: 'Badge', href: '/components/badge', layer: 'Components' },
13+
]}
14+
/>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/** @jsxImportSource @emotion/react */
2+
import { UserProfileWeb3WalletsSectionView } from '@clerk/ui/mosaic/user-profile/user-profile-web3-wallets-section.view';
3+
4+
import type { StoryMeta } from '@/lib/types';
5+
6+
export { default as __source } from './user-profile-web3-wallets-section.stories?raw';
7+
8+
export const meta: StoryMeta = {
9+
group: 'User',
10+
title: 'UserProfileWeb3WalletsSection',
11+
source: 'packages/ui/src/mosaic/user-profile/user-profile-web3-wallets-section.view.tsx',
12+
styleEngine: 'stylex',
13+
};
14+
15+
export function Default() {
16+
return (
17+
<UserProfileWeb3WalletsSectionView
18+
wallets={[
19+
{
20+
id: 'metamask',
21+
address: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
22+
provider: 'MetaMask',
23+
iconUrl: 'https://img.clerk.com/static/metamask.svg',
24+
isPrimary: true,
25+
isVerified: true,
26+
},
27+
]}
28+
onAdd={() => undefined}
29+
onManage={() => undefined}
30+
/>
31+
);
32+
}

packages/ui/src/mosaic/user-profile/user-profile-profile-panel.styles.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ export const styles = stylex.create({
1515
height: space['6'],
1616
width: space['6'],
1717
},
18+
sectionHeader: {
19+
alignItems: 'flex-end',
20+
columnGap: space['4'],
21+
display: 'flex',
22+
justifyContent: 'space-between',
23+
minWidth: 0,
24+
},
25+
sectionTitle: {
26+
flexGrow: 1,
27+
minWidth: 0,
28+
},
1829
root: {
1930
gap: space['6'],
2031
display: 'flex',
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import * as stylex from '@stylexjs/stylex';
2+
3+
import { Badge } from '../components/badge';
4+
import { Button } from '../components/button';
5+
import { Section } from '../components/section';
6+
import type { UserProfileMenuAction } from './user-profile-action-menu';
7+
import { UserProfileActionMenu } from './user-profile-action-menu';
8+
import { styles } from './user-profile-profile-panel.styles';
9+
10+
export interface UserProfileWeb3Wallet {
11+
id: string;
12+
address: string;
13+
provider?: string;
14+
iconUrl?: string;
15+
isPrimary?: boolean;
16+
isVerified?: boolean;
17+
canRemove?: boolean;
18+
}
19+
20+
export interface UserProfileWeb3WalletsSectionViewProps {
21+
wallets: UserProfileWeb3Wallet[];
22+
onAdd?: () => void;
23+
onManage?: (id: string) => void;
24+
onSetPrimary?: (id: string) => void;
25+
onRemove?: (id: string) => void;
26+
}
27+
28+
const shortenWeb3Address = (address: string) => {
29+
if (address.length <= 10) {
30+
return address;
31+
}
32+
33+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
34+
};
35+
36+
export function UserProfileWeb3WalletsSectionView({
37+
wallets,
38+
onAdd,
39+
onManage,
40+
onSetPrimary,
41+
onRemove,
42+
}: UserProfileWeb3WalletsSectionViewProps) {
43+
return (
44+
<Section.Root>
45+
<div {...stylex.props(styles.sectionHeader)}>
46+
<Section.Title {...stylex.props(styles.sectionTitle)}>Web3 wallets</Section.Title>
47+
{onAdd ? (
48+
<Button
49+
color='neutral'
50+
size='sm'
51+
variant='outline'
52+
onClick={onAdd}
53+
>
54+
Add
55+
</Button>
56+
) : null}
57+
</div>
58+
<Section.Group>
59+
{wallets.map(wallet => {
60+
const actions: UserProfileMenuAction[] = [];
61+
const hasExplicitActions = Boolean(onSetPrimary || onRemove);
62+
63+
if (!wallet.isPrimary && wallet.isVerified !== false && onSetPrimary) {
64+
actions.push({ label: 'Set as primary', onClick: () => onSetPrimary(wallet.id) });
65+
}
66+
67+
if (onRemove && wallet.canRemove !== false) {
68+
actions.push({ label: 'Remove wallet', color: 'negative', onClick: () => onRemove(wallet.id) });
69+
}
70+
71+
if (!hasExplicitActions && onManage) {
72+
actions.push({ label: 'Manage', onClick: () => onManage(wallet.id) });
73+
}
74+
75+
const address = shortenWeb3Address(wallet.address);
76+
const badges = (
77+
<>
78+
{wallet.isPrimary ? <Badge color='neutral'>Primary</Badge> : null}
79+
{wallet.isVerified === false ? <Badge color='warning'>Unverified</Badge> : null}
80+
</>
81+
);
82+
83+
return (
84+
<Section.Row key={wallet.id}>
85+
<Section.Item>
86+
{wallet.iconUrl ? (
87+
<Section.Media>
88+
<img
89+
alt=''
90+
src={wallet.iconUrl}
91+
{...stylex.props(styles.providerIcon)}
92+
/>
93+
</Section.Media>
94+
) : null}
95+
<Section.Content>
96+
<Section.Label>
97+
<span {...stylex.props(styles.contactValue)}>
98+
{wallet.provider ?? address}
99+
{badges}
100+
</span>
101+
</Section.Label>
102+
{wallet.provider ? <Section.Description>{address}</Section.Description> : null}
103+
</Section.Content>
104+
<Section.Actions>
105+
<UserProfileActionMenu
106+
actions={actions}
107+
label={`Manage ${wallet.provider ?? address}`}
108+
/>
109+
</Section.Actions>
110+
</Section.Item>
111+
</Section.Row>
112+
);
113+
})}
114+
</Section.Group>
115+
</Section.Root>
116+
);
117+
}

0 commit comments

Comments
 (0)