Skip to content

Commit e9d84e1

Browse files
feat(ui): confirm Web3 wallet removal from its row (#9754)
1 parent 855c24f commit e9d84e1

15 files changed

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

‎packages/swingset/src/lib/registry.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,15 @@ import {
269269
meta as userProfileSubscriptionSectionMeta,
270270
} from '../stories/user-profile-subscription-section.stories';
271271
import {
272+
ConnectedWallets as UserProfileWeb3WalletsSectionConnectedWallets,
273+
ConnectionError as UserProfileWeb3WalletsSectionConnectionError,
274+
ConnectOnly as UserProfileWeb3WalletsSectionConnectOnly,
272275
Default as UserProfileWeb3WalletsSectionDefault,
273276
meta as userProfileWeb3WalletsSectionMeta,
277+
PrimaryError as UserProfileWeb3WalletsSectionPrimaryError,
278+
RemovalError as UserProfileWeb3WalletsSectionRemovalError,
279+
RemovalPending as UserProfileWeb3WalletsSectionRemovalPending,
280+
UnverifiedWallet as UserProfileWeb3WalletsSectionUnverifiedWallet,
274281
} from '../stories/user-profile-web3-wallets-section.stories';
275282
import {
276283
Default as VisuallyHiddenDefault,
@@ -550,6 +557,13 @@ const userProfileConnectedAccountsSectionModule: StoryModule = {
550557
const userProfileWeb3WalletsSectionModule: StoryModule = {
551558
meta: userProfileWeb3WalletsSectionMeta,
552559
Default: UserProfileWeb3WalletsSectionDefault,
560+
ConnectedWallets: UserProfileWeb3WalletsSectionConnectedWallets,
561+
ConnectOnly: UserProfileWeb3WalletsSectionConnectOnly,
562+
ConnectionError: UserProfileWeb3WalletsSectionConnectionError,
563+
PrimaryError: UserProfileWeb3WalletsSectionPrimaryError,
564+
RemovalPending: UserProfileWeb3WalletsSectionRemovalPending,
565+
RemovalError: UserProfileWeb3WalletsSectionRemovalError,
566+
UnverifiedWallet: UserProfileWeb3WalletsSectionUnverifiedWallet,
553567
};
554568
const userProfileDeleteSectionModule: StoryModule = {
555569
meta: userProfileDeleteSectionMeta,
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import type {
2+
UserProfileWeb3Provider,
3+
UserProfileWeb3Wallet,
4+
} from '@clerk/ui/mosaic/features/user-profile/user-profile-web3-wallets-section.view';
5+
import { useState } from 'react';
6+
7+
interface DemoWallet extends UserProfileWeb3Wallet {
8+
providerId?: string;
9+
}
10+
11+
const providers: UserProfileWeb3Provider[] = [
12+
{ id: 'metamask', provider: 'MetaMask', iconUrl: 'https://img.clerk.com/static/metamask.svg' },
13+
{ id: 'coinbase-wallet', provider: 'Coinbase Wallet', iconUrl: 'https://img.clerk.com/static/coinbase_wallet.svg' },
14+
];
15+
16+
export const primaryWallet: DemoWallet = {
17+
id: 'wallet_1',
18+
providerId: 'metamask',
19+
provider: 'MetaMask',
20+
iconUrl: 'https://img.clerk.com/static/metamask.svg',
21+
address: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
22+
isPrimary: true,
23+
isVerified: true,
24+
};
25+
26+
export const secondaryWallet: DemoWallet = {
27+
id: 'wallet_2',
28+
providerId: 'coinbase-wallet',
29+
provider: 'Coinbase Wallet',
30+
iconUrl: 'https://img.clerk.com/static/coinbase_wallet.svg',
31+
address: '0x1234567890abcdef1234567890abcdef12345678',
32+
isVerified: true,
33+
};
34+
35+
export function useWeb3WalletsFixture({
36+
initialWallets = [primaryWallet],
37+
availableProviders = providers,
38+
primaryError = false,
39+
removalState,
40+
}: {
41+
initialWallets?: DemoWallet[];
42+
availableProviders?: UserProfileWeb3Provider[];
43+
primaryError?: boolean;
44+
removalState?: 'pending' | 'error';
45+
} = {}) {
46+
const [wallets, setWallets] = useState(initialWallets);
47+
const [connectionProviders, setConnectionProviders] = useState(availableProviders);
48+
const [primaryFailed, setPrimaryFailed] = useState(false);
49+
50+
return {
51+
wallets,
52+
availableProviders: connectionProviders.filter(
53+
provider => !wallets.some(wallet => wallet.providerId === provider.id && wallet.isVerified),
54+
),
55+
onConnect: (id: string) => {
56+
const provider = connectionProviders.find(item => item.id === id);
57+
if (!provider) {
58+
return;
59+
}
60+
setConnectionProviders(current =>
61+
current.map(item => (item.id === id ? { ...item, connectError: undefined } : item)),
62+
);
63+
setWallets(current => {
64+
if (current.some(wallet => wallet.providerId === id && !wallet.isVerified)) {
65+
return current.map(wallet => (wallet.providerId === id ? { ...wallet, isVerified: true } : wallet));
66+
}
67+
return [
68+
...current,
69+
{
70+
provider: provider.provider,
71+
iconUrl: provider.iconUrl,
72+
id: `wallet_${id}`,
73+
providerId: id,
74+
address: '0x1234567890abcdef1234567890abcdef12345678',
75+
isVerified: true,
76+
isPrimary: !current.some(wallet => wallet.isPrimary),
77+
},
78+
];
79+
});
80+
},
81+
onSetPrimary: (id: string) => {
82+
if (primaryError && !primaryFailed) {
83+
setPrimaryFailed(true);
84+
setWallets(current =>
85+
current.map(wallet =>
86+
wallet.id === id
87+
? { ...wallet, primaryError: 'Unable to set this wallet as primary. Please try again.' }
88+
: wallet,
89+
),
90+
);
91+
return;
92+
}
93+
setWallets(current =>
94+
current.map(wallet => ({ ...wallet, isPrimary: wallet.id === id, primaryError: undefined })),
95+
);
96+
},
97+
onRemove: (id: string) => {
98+
if (removalState === 'pending') {
99+
setWallets(current => current.map(wallet => (wallet.id === id ? { ...wallet, isRemoving: true } : wallet)));
100+
setTimeout(() => {
101+
setWallets(current => current.filter(wallet => wallet.id !== id));
102+
}, 1500);
103+
return;
104+
}
105+
setWallets(current => {
106+
if (removalState === 'error' && !current.find(wallet => wallet.id === id)?.removalError) {
107+
return current.map(wallet =>
108+
wallet.id === id ? { ...wallet, removalError: 'Unable to remove wallet. Please try again.' } : wallet,
109+
);
110+
}
111+
return current.filter(wallet => wallet.id !== id);
112+
});
113+
},
114+
};
115+
}

‎packages/swingset/src/stories/user-profile-profile-panel.stories.tsx‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,9 @@ export function Default(_args: Record<string, unknown>) {
6363
isPrimary: true,
6464
isVerified: true,
6565
},
66-
{
67-
id: 'coinbase-wallet',
68-
provider: 'Coinbase Wallet',
69-
iconUrl: providerIconUrl('coinbase_wallet'),
70-
connected: false,
71-
},
66+
]}
67+
availableWeb3Providers={[
68+
{ id: 'coinbase-wallet', provider: 'Coinbase Wallet', iconUrl: providerIconUrl('coinbase_wallet') },
7269
]}
7370
hasImage={Boolean(imageUrl)}
7471
imageUrl={imageUrl}

‎packages/swingset/src/stories/user-profile-web3-wallets-section.mdx‎

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,58 @@ import * as Stories from './user-profile-web3-wallets-section.stories';
22

33
# UserProfileWeb3WalletsSection
44

5-
Supported Web3 wallet providers with per-provider connection actions, connected identities, verification state,
6-
primary status, and wallet actions.
5+
Wallets and available providers are supplied separately. These examples simulate state through props without connecting to browser extensions. Reload to reset them. Solana wallet selection is a separate flow.
76

87
<Story
98
name='Default'
109
storyModule={Stories}
1110
composition={[
1211
{ name: 'Section', href: '/components/section', layer: 'Components' },
1312
{ name: 'Badge', href: '/components/badge', layer: 'Components' },
13+
{ name: 'Button', href: '/components/button', layer: 'Components' },
14+
{ name: 'Icon', href: '/components/icon', layer: 'Components' },
15+
{ name: 'Menu', href: '/components/menu', layer: 'Components' },
16+
{ name: 'Dialog', href: '/components/dialog', layer: 'Components' },
17+
{ name: 'Banner', href: '/components/banner', layer: 'Components' },
1418
]}
1519
/>
20+
21+
## Connected Wallets
22+
23+
Only verified, non-primary wallets offer Set as primary. Removing a wallet makes its provider available to connect again.
24+
25+
<Story name='ConnectedWallets' storyModule={Stories} />
26+
27+
## Connect Only
28+
29+
<Story name='ConnectOnly' storyModule={Stories} />
30+
31+
## Unverified Wallet
32+
33+
An unverified wallet offers Remove. After removal, the provider can be connected again.
34+
35+
<Story name='UnverifiedWallet' storyModule={Stories} />
36+
37+
## Connection Error
38+
39+
Retry Connect to clear the error and connect the wallet.
40+
41+
<Story name='ConnectionError' storyModule={Stories} />
42+
43+
## Primary Error
44+
45+
Choose Set as primary on Coinbase Wallet to show an error. Retry succeeds.
46+
47+
<Story name='PrimaryError' storyModule={Stories} />
48+
49+
## Removal Pending
50+
51+
Choose Remove wallet, then confirm to hold the pending state. Cancel is disabled while removal is pending.
52+
53+
<Story name='RemovalPending' storyModule={Stories} />
54+
55+
## Removal Error
56+
57+
The first removal attempt shows an error inside the dialog. Retry succeeds.
58+
59+
<Story name='RemovalError' storyModule={Stories} />

‎packages/swingset/src/stories/user-profile-web3-wallets-section.stories.tsx‎

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,74 @@ import { UserProfileWeb3WalletsSectionView } from '@clerk/ui/mosaic/features/use
22

33
import type { StoryMeta } from '@/lib/types';
44

5+
import { primaryWallet, secondaryWallet, useWeb3WalletsFixture } from './fixtures/user-profile-web3-wallets';
6+
57
export { default as __source } from './user-profile-web3-wallets-section.stories?raw';
68

79
export const meta: StoryMeta = {
810
group: 'User Profile',
911
status: 'wip',
12+
substatus: 'needs wire-up',
1013
title: 'UserProfileWeb3WalletsSection',
1114
label: 'Web3 wallets',
1215
navigation: { category: 'Sections' },
1316
source: 'packages/ui/src/mosaic/features/user-profile/user-profile-web3-wallets-section.view.tsx',
1417
};
1518

1619
export function Default() {
20+
const fixture = useWeb3WalletsFixture();
21+
return <UserProfileWeb3WalletsSectionView {...fixture} />;
22+
}
23+
24+
export function ConnectedWallets() {
25+
const fixture = useWeb3WalletsFixture({ initialWallets: [primaryWallet, secondaryWallet] });
26+
return <UserProfileWeb3WalletsSectionView {...fixture} />;
27+
}
28+
29+
export function ConnectOnly() {
30+
const fixture = useWeb3WalletsFixture({ initialWallets: [] });
31+
return <UserProfileWeb3WalletsSectionView {...fixture} />;
32+
}
33+
34+
export function UnverifiedWallet() {
35+
const fixture = useWeb3WalletsFixture({
36+
initialWallets: [{ ...primaryWallet, isPrimary: false, isVerified: false }],
37+
availableProviders: [{ id: 'metamask', provider: 'MetaMask', iconUrl: primaryWallet.iconUrl }],
38+
});
1739
return (
1840
<UserProfileWeb3WalletsSectionView
19-
wallets={[
20-
{
21-
id: 'metamask',
22-
address: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
23-
provider: 'MetaMask',
24-
iconUrl: 'https://img.clerk.com/static/metamask.svg',
25-
isPrimary: true,
26-
isVerified: true,
27-
},
28-
{
29-
id: 'coinbase-wallet',
30-
provider: 'Coinbase Wallet',
31-
iconUrl: 'https://img.clerk.com/static/coinbase_wallet.svg',
32-
connected: false,
33-
},
34-
]}
35-
onConnect={() => undefined}
36-
onManage={() => undefined}
41+
{...fixture}
42+
availableProviders={fixture.wallets.length === 0 ? fixture.availableProviders : []}
3743
/>
3844
);
3945
}
46+
47+
export function ConnectionError() {
48+
const fixture = useWeb3WalletsFixture({
49+
initialWallets: [],
50+
availableProviders: [
51+
{
52+
id: 'metamask',
53+
provider: 'MetaMask',
54+
iconUrl: primaryWallet.iconUrl,
55+
connectError: 'Wallet extension not found. Check your wallet and try again.',
56+
},
57+
],
58+
});
59+
return <UserProfileWeb3WalletsSectionView {...fixture} />;
60+
}
61+
62+
export function PrimaryError() {
63+
const fixture = useWeb3WalletsFixture({ initialWallets: [primaryWallet, secondaryWallet], primaryError: true });
64+
return <UserProfileWeb3WalletsSectionView {...fixture} />;
65+
}
66+
67+
export function RemovalPending() {
68+
const fixture = useWeb3WalletsFixture({ removalState: 'pending' });
69+
return <UserProfileWeb3WalletsSectionView {...fixture} />;
70+
}
71+
72+
export function RemovalError() {
73+
const fixture = useWeb3WalletsFixture({ removalState: 'error' });
74+
return <UserProfileWeb3WalletsSectionView {...fixture} />;
75+
}

0 commit comments

Comments
 (0)