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/web3-wallet-removal-dialog.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 @@ -262,8 +262,15 @@ import {
meta as userProfileSubscriptionSectionMeta,
} from '../stories/user-profile-subscription-section.stories';
import {
ConnectedWallets as UserProfileWeb3WalletsSectionConnectedWallets,
ConnectionError as UserProfileWeb3WalletsSectionConnectionError,
ConnectOnly as UserProfileWeb3WalletsSectionConnectOnly,
Default as UserProfileWeb3WalletsSectionDefault,
meta as userProfileWeb3WalletsSectionMeta,
PrimaryError as UserProfileWeb3WalletsSectionPrimaryError,
RemovalError as UserProfileWeb3WalletsSectionRemovalError,
RemovalPending as UserProfileWeb3WalletsSectionRemovalPending,
UnverifiedWallet as UserProfileWeb3WalletsSectionUnverifiedWallet,
} from '../stories/user-profile-web3-wallets-section.stories';
import {
Default as VisuallyHiddenDefault,
Expand Down Expand Up @@ -536,6 +543,13 @@ const userProfileConnectedAccountsSectionModule: StoryModule = {
const userProfileWeb3WalletsSectionModule: StoryModule = {
meta: userProfileWeb3WalletsSectionMeta,
Default: UserProfileWeb3WalletsSectionDefault,
ConnectedWallets: UserProfileWeb3WalletsSectionConnectedWallets,
ConnectOnly: UserProfileWeb3WalletsSectionConnectOnly,
ConnectionError: UserProfileWeb3WalletsSectionConnectionError,
PrimaryError: UserProfileWeb3WalletsSectionPrimaryError,
RemovalPending: UserProfileWeb3WalletsSectionRemovalPending,
RemovalError: UserProfileWeb3WalletsSectionRemovalError,
UnverifiedWallet: UserProfileWeb3WalletsSectionUnverifiedWallet,
};
const userProfileDeleteSectionModule: StoryModule = {
meta: userProfileDeleteSectionMeta,
Expand Down
115 changes: 115 additions & 0 deletions packages/swingset/src/stories/fixtures/user-profile-web3-wallets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type {
UserProfileWeb3Provider,
UserProfileWeb3Wallet,
} from '@clerk/ui/mosaic/features/user-profile/user-profile-web3-wallets-section.view';
import { useState } from 'react';

interface DemoWallet extends UserProfileWeb3Wallet {
providerId?: string;
}

const providers: UserProfileWeb3Provider[] = [
{ id: 'metamask', provider: 'MetaMask', iconUrl: 'https://img.clerk.com/static/metamask.svg' },
{ id: 'coinbase-wallet', provider: 'Coinbase Wallet', iconUrl: 'https://img.clerk.com/static/coinbase_wallet.svg' },
];

export const primaryWallet: DemoWallet = {
id: 'wallet_1',
providerId: 'metamask',
provider: 'MetaMask',
iconUrl: 'https://img.clerk.com/static/metamask.svg',
address: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
isPrimary: true,
isVerified: true,
};

export const secondaryWallet: DemoWallet = {
id: 'wallet_2',
providerId: 'coinbase-wallet',
provider: 'Coinbase Wallet',
iconUrl: 'https://img.clerk.com/static/coinbase_wallet.svg',
address: '0x1234567890abcdef1234567890abcdef12345678',
isVerified: true,
};

export function useWeb3WalletsFixture({
Comment thread
austincalvelage marked this conversation as resolved.
initialWallets = [primaryWallet],
availableProviders = providers,
primaryError = false,
removalState,
}: {
initialWallets?: DemoWallet[];
availableProviders?: UserProfileWeb3Provider[];
primaryError?: boolean;
removalState?: 'pending' | 'error';
} = {}) {
const [wallets, setWallets] = useState(initialWallets);
const [connectionProviders, setConnectionProviders] = useState(availableProviders);
const [primaryFailed, setPrimaryFailed] = useState(false);

return {
wallets,
availableProviders: connectionProviders.filter(
provider => !wallets.some(wallet => wallet.providerId === provider.id && wallet.isVerified),
),
onConnect: (id: string) => {
const provider = connectionProviders.find(item => item.id === id);
if (!provider) {
return;
}
setConnectionProviders(current =>
current.map(item => (item.id === id ? { ...item, connectError: undefined } : item)),
);
setWallets(current => {
if (current.some(wallet => wallet.providerId === id && !wallet.isVerified)) {
return current.map(wallet => (wallet.providerId === id ? { ...wallet, isVerified: true } : wallet));
}
return [
...current,
{
provider: provider.provider,
iconUrl: provider.iconUrl,
id: `wallet_${id}`,
providerId: id,
address: '0x1234567890abcdef1234567890abcdef12345678',
isVerified: true,
isPrimary: !current.some(wallet => wallet.isPrimary),
},
];
});
},
onSetPrimary: (id: string) => {
if (primaryError && !primaryFailed) {
setPrimaryFailed(true);
setWallets(current =>
current.map(wallet =>
wallet.id === id
? { ...wallet, primaryError: 'Unable to set this wallet as primary. Please try again.' }
: wallet,
),
);
return;
}
setWallets(current =>
current.map(wallet => ({ ...wallet, isPrimary: wallet.id === id, primaryError: undefined })),
);
},
onRemove: (id: string) => {
if (removalState === 'pending') {
setWallets(current => current.map(wallet => (wallet.id === id ? { ...wallet, isRemoving: true } : wallet)));
setTimeout(() => {
setWallets(current => current.filter(wallet => wallet.id !== id));
}, 1500);
return;
}
setWallets(current => {
if (removalState === 'error' && !current.find(wallet => wallet.id === id)?.removalError) {
return current.map(wallet =>
wallet.id === id ? { ...wallet, removalError: 'Unable to remove wallet. Please try again.' } : wallet,
);
}
return current.filter(wallet => wallet.id !== id);
});
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,9 @@ export function Default(_args: Record<string, unknown>) {
isPrimary: true,
isVerified: true,
},
{
id: 'coinbase-wallet',
provider: 'Coinbase Wallet',
iconUrl: providerIconUrl('coinbase_wallet'),
connected: false,
},
]}
availableWeb3Providers={[
{ id: 'coinbase-wallet', provider: 'Coinbase Wallet', iconUrl: providerIconUrl('coinbase_wallet') },
]}
hasImage={Boolean(imageUrl)}
imageUrl={imageUrl}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,58 @@ import * as Stories from './user-profile-web3-wallets-section.stories';

# UserProfileWeb3WalletsSection

Supported Web3 wallet providers with per-provider connection actions, connected identities, verification state,
primary status, and wallet actions.
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.

<Story
name='Default'
storyModule={Stories}
composition={[
{ name: 'Section', href: '/components/section', layer: 'Components' },
{ name: 'Badge', href: '/components/badge', layer: 'Components' },
{ name: 'Button', href: '/components/button', layer: 'Components' },
{ name: 'Icon', href: '/components/icon', layer: 'Components' },
{ name: 'Menu', href: '/components/menu', layer: 'Components' },
{ name: 'Dialog', href: '/components/dialog', layer: 'Components' },
{ name: 'Banner', href: '/components/banner', layer: 'Components' },
]}
/>

## Connected Wallets

Only verified, non-primary wallets offer Set as primary. Removing a wallet makes its provider available to connect again.

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

## Connect Only

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

## Unverified Wallet

An unverified wallet offers Remove. After removal, the provider can be connected again.

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

## Connection Error

Retry Connect to clear the error and connect the wallet.

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

## Primary Error

Choose Set as primary on Coinbase Wallet to show an error. Retry succeeds.

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

## Removal Pending

Choose Remove wallet, then confirm to hold the pending state. Cancel is disabled while removal is pending.

<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,38 +2,74 @@ import { UserProfileWeb3WalletsSectionView } from '@clerk/ui/mosaic/features/use

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

import { primaryWallet, secondaryWallet, useWeb3WalletsFixture } from './fixtures/user-profile-web3-wallets';

export { default as __source } from './user-profile-web3-wallets-section.stories?raw';

export const meta: StoryMeta = {
group: 'User Profile',
status: 'wip',
substatus: 'needs wire-up',
title: 'UserProfileWeb3WalletsSection',
label: 'Web3 wallets',
navigation: { category: 'Sections' },
source: 'packages/ui/src/mosaic/features/user-profile/user-profile-web3-wallets-section.view.tsx',
};

export function Default() {
const fixture = useWeb3WalletsFixture();
return <UserProfileWeb3WalletsSectionView {...fixture} />;
}

export function ConnectedWallets() {
const fixture = useWeb3WalletsFixture({ initialWallets: [primaryWallet, secondaryWallet] });
return <UserProfileWeb3WalletsSectionView {...fixture} />;
}

export function ConnectOnly() {
const fixture = useWeb3WalletsFixture({ initialWallets: [] });
return <UserProfileWeb3WalletsSectionView {...fixture} />;
}

export function UnverifiedWallet() {
const fixture = useWeb3WalletsFixture({
initialWallets: [{ ...primaryWallet, isPrimary: false, isVerified: false }],
availableProviders: [{ id: 'metamask', provider: 'MetaMask', iconUrl: primaryWallet.iconUrl }],
});
return (
<UserProfileWeb3WalletsSectionView
wallets={[
{
id: 'metamask',
address: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
provider: 'MetaMask',
iconUrl: 'https://img.clerk.com/static/metamask.svg',
isPrimary: true,
isVerified: true,
},
{
id: 'coinbase-wallet',
provider: 'Coinbase Wallet',
iconUrl: 'https://img.clerk.com/static/coinbase_wallet.svg',
connected: false,
},
]}
onConnect={() => undefined}
onManage={() => undefined}
{...fixture}
availableProviders={fixture.wallets.length === 0 ? fixture.availableProviders : []}
Comment thread
austincalvelage marked this conversation as resolved.
/>
);
}

export function ConnectionError() {
const fixture = useWeb3WalletsFixture({
initialWallets: [],
availableProviders: [
{
id: 'metamask',
provider: 'MetaMask',
iconUrl: primaryWallet.iconUrl,
connectError: 'Wallet extension not found. Check your wallet and try again.',
},
],
});
return <UserProfileWeb3WalletsSectionView {...fixture} />;
}

export function PrimaryError() {
const fixture = useWeb3WalletsFixture({ initialWallets: [primaryWallet, secondaryWallet], primaryError: true });
return <UserProfileWeb3WalletsSectionView {...fixture} />;
}

export function RemovalPending() {
const fixture = useWeb3WalletsFixture({ removalState: 'pending' });
return <UserProfileWeb3WalletsSectionView {...fixture} />;
}

export function RemovalError() {
const fixture = useWeb3WalletsFixture({ removalState: 'error' });
return <UserProfileWeb3WalletsSectionView {...fixture} />;
}
Loading
Loading