Skip to content

Commit c6deea6

Browse files
committed
feat(clerk-js): add Web3 wallet selection and connection functionality to <UserProfile />
Signed-off-by: Kenton Duprey <[email protected]>
1 parent f626046 commit c6deea6

File tree

4 files changed

+156
-15
lines changed

4 files changed

+156
-15
lines changed

packages/clerk-js/src/ui/components/UserProfile/Web3Form.tsx

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useReverification, useUser } from '@clerk/shared/react';
22
import type { Web3Provider, Web3Strategy } from '@clerk/shared/types';
33

4+
import { useWizard, Wizard } from '@/ui/common';
5+
import { Web3SelectWalletScreen } from '@/ui/components/UserProfile/Web3SelectWalletScreen';
46
import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
57
import { ProfileSection } from '@/ui/elements/Section';
68
import { getFieldError, handleError } from '@/ui/utils/errorHandler';
@@ -9,27 +11,27 @@ import { generateWeb3Signature, getWeb3Identifier } from '../../../utils/web3';
911
import { descriptors, Image, localizationKeys, Text } from '../../customizables';
1012
import { useEnabledThirdPartyProviders } from '../../hooks';
1113

12-
export const AddWeb3WalletActionMenu = withCardStateProvider(({ onClick }: { onClick?: () => void }) => {
14+
export const AddWeb3WalletActionMenu = withCardStateProvider(() => {
1315
const card = useCardState();
1416
const { user } = useUser();
15-
const { strategies, strategyToDisplayData } = useEnabledThirdPartyProviders();
1617

17-
const enabledStrategies = strategies.filter(s => s.startsWith('web3')) as Web3Strategy[];
18-
const connectedStrategies = user?.verifiedWeb3Wallets.map(w => w.verification.strategy) as Web3Strategy[];
19-
const unconnectedStrategies = enabledStrategies.filter(strategy => {
20-
return !connectedStrategies.includes(strategy);
21-
});
18+
const wizard = useWizard();
19+
2220
const createWeb3Wallet = useReverification((identifier: string) =>
2321
user?.createWeb3Wallet({ web3Wallet: identifier }),
2422
);
2523

26-
const connect = async (strategy: Web3Strategy) => {
24+
const connect = async ({ strategy, walletName }: { strategy: Web3Strategy; walletName?: string }) => {
25+
if (strategy === 'web3_solana_signature' && !walletName) {
26+
wizard.nextStep();
27+
return;
28+
}
2729
const provider = strategy.replace('web3_', '').replace('_signature', '') as Web3Provider;
2830
card.setError(undefined);
2931

3032
try {
3133
card.setLoading(strategy);
32-
const identifier = await getWeb3Identifier({ provider });
34+
const identifier = await getWeb3Identifier({ provider, walletName });
3335

3436
if (!user) {
3537
throw new Error('user is not defined');
@@ -38,7 +40,7 @@ export const AddWeb3WalletActionMenu = withCardStateProvider(({ onClick }: { onC
3840
let web3Wallet = await createWeb3Wallet(identifier);
3941
web3Wallet = await web3Wallet?.prepareVerification({ strategy });
4042
const message = web3Wallet?.verification.message as string;
41-
const signature = await generateWeb3Signature({ identifier, nonce: message, provider });
43+
const signature = await generateWeb3Signature({ identifier, nonce: message, provider, walletName });
4244
await web3Wallet?.attemptVerification({ signature });
4345
card.setIdle();
4446
} catch (err) {
@@ -52,6 +54,29 @@ export const AddWeb3WalletActionMenu = withCardStateProvider(({ onClick }: { onC
5254
}
5355
};
5456

57+
return (
58+
<Wizard {...wizard.props}>
59+
<Web3SelectStrategyScreen onConnect={connect} />
60+
61+
<Web3SelectWalletScreen onConnect={connect} />
62+
</Wizard>
63+
);
64+
});
65+
66+
const Web3SelectStrategyScreen = ({
67+
onConnect,
68+
}: {
69+
onConnect: (params: { strategy: Web3Strategy; walletName?: string }) => Promise<void>;
70+
}) => {
71+
const card = useCardState();
72+
const { user } = useUser();
73+
const { strategies, strategyToDisplayData } = useEnabledThirdPartyProviders();
74+
const enabledStrategies = strategies.filter(s => s.startsWith('web3')) as Web3Strategy[];
75+
const connectedStrategies = user?.verifiedWeb3Wallets?.map(w => w.verification.strategy) ?? ([] as Web3Strategy[]);
76+
const unconnectedStrategies = enabledStrategies.filter(strategy => {
77+
return !connectedStrategies.includes(strategy) && strategyToDisplayData[strategy];
78+
});
79+
5580
if (unconnectedStrategies.length === 0) {
5681
return null;
5782
}
@@ -61,13 +86,12 @@ export const AddWeb3WalletActionMenu = withCardStateProvider(({ onClick }: { onC
6186
<ProfileSection.ActionMenu
6287
id='web3Wallets'
6388
triggerLocalizationKey={localizationKeys('userProfile.start.web3WalletsSection.primaryButton')}
64-
onClick={onClick}
6589
>
6690
{unconnectedStrategies.map(strategy => (
6791
<ProfileSection.ActionMenuItem
6892
key={strategy}
6993
id={strategyToDisplayData[strategy].id}
70-
onClick={() => connect(strategy)}
94+
onClick={() => onConnect({ strategy })}
7195
isLoading={card.loadingMetadata === strategy}
7296
isDisabled={card.isLoading}
7397
localizationKey={localizationKeys('userProfile.web3WalletPage.web3WalletButtonsBlockButton', {
@@ -104,4 +128,4 @@ export const AddWeb3WalletActionMenu = withCardStateProvider(({ onClick }: { onC
104128
)}
105129
</>
106130
);
107-
});
131+
};

packages/clerk-js/src/ui/components/UserProfile/Web3Section.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export const Web3Section = withCardStateProvider(
108108
);
109109
})}
110110
</ProfileSection.ItemList>
111-
{shouldAllowCreation && <AddWeb3WalletActionMenu onClick={() => setActionValue(null)} />}
111+
{shouldAllowCreation && <AddWeb3WalletActionMenu />}
112112
</Action.Root>
113113
</ProfileSection.Root>
114114
);
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import type { Web3Strategy } from '@clerk/shared/types';
2+
import { WalletReadyState } from '@solana/wallet-adapter-base';
3+
import { ConnectionProvider, useWallet, WalletProvider } from '@solana/wallet-adapter-react';
4+
import { MAINNET_ENDPOINT } from '@solana/wallet-standard';
5+
import { useMemo } from 'react';
6+
7+
import { Action } from '@/ui/elements/Action';
8+
import { useActionContext } from '@/ui/elements/Action/ActionRoot';
9+
import { useCardState } from '@/ui/elements/contexts';
10+
import { Form } from '@/ui/elements/Form';
11+
import { FormContainer } from '@/ui/elements/FormContainer';
12+
13+
import { Button, Grid, Image, localizationKeys, Text } from '../../customizables';
14+
15+
export type Web3SelectWalletProps = {
16+
onConnect: (params: { strategy: Web3Strategy; walletName: string }) => Promise<void>;
17+
};
18+
19+
const Web3SelectWalletInner = ({ onConnect }: Web3SelectWalletProps) => {
20+
const card = useCardState();
21+
const { wallets } = useWallet();
22+
const { close } = useActionContext();
23+
24+
const installedWallets = useMemo(
25+
() =>
26+
wallets
27+
.filter(w => w.readyState === WalletReadyState.Installed)
28+
.map(wallet => ({
29+
name: wallet.adapter.name,
30+
icon: wallet.adapter.icon,
31+
})),
32+
[wallets],
33+
);
34+
35+
if (installedWallets.length === 0) {
36+
return null;
37+
}
38+
39+
const onClick = async (wallet: { name: string; icon: string }) => {
40+
card.setLoading(wallet.name);
41+
try {
42+
await onConnect({ strategy: 'web3_solana_signature', walletName: wallet.name });
43+
card.setIdle();
44+
} catch (err) {
45+
card.setIdle();
46+
console.error(err);
47+
} finally {
48+
close();
49+
}
50+
};
51+
52+
return (
53+
<Action.Card>
54+
<FormContainer
55+
headerTitle='Add Solana wallet'
56+
headerSubtitle={localizationKeys('userProfile.start.web3WalletsSection.primaryButton')}
57+
>
58+
<Form.Root>
59+
<Form.ControlRow elementId='web3WalletName'>
60+
<Grid
61+
columns={2}
62+
gap={3}
63+
>
64+
{installedWallets.map(wallet => (
65+
<Button
66+
key={wallet.name}
67+
textVariant='buttonLarge'
68+
isDisabled={card.isLoading}
69+
isLoading={card.isLoading && card.loadingMetadata === wallet.name}
70+
sx={theme => ({
71+
gap: theme.space.$4,
72+
justifyContent: 'flex-start',
73+
})}
74+
variant='outline'
75+
onClick={() => onClick(wallet)}
76+
>
77+
{wallet.icon && (
78+
<Image
79+
src={wallet.icon}
80+
alt={wallet.name}
81+
sx={theme => ({ width: theme.sizes.$4, height: 'auto', maxWidth: '100%' })}
82+
/>
83+
)}
84+
<Text
85+
as='span'
86+
truncate
87+
variant='buttonLarge'
88+
>
89+
{wallet.name}
90+
</Text>
91+
</Button>
92+
))}
93+
</Grid>
94+
</Form.ControlRow>
95+
</Form.Root>
96+
</FormContainer>
97+
</Action.Card>
98+
);
99+
};
100+
101+
export const Web3SelectWalletScreen = ({ onConnect }: Web3SelectWalletProps) => {
102+
const network = MAINNET_ENDPOINT;
103+
const wallets = useMemo(() => [], [network]);
104+
return (
105+
<ConnectionProvider endpoint={network}>
106+
<WalletProvider
107+
wallets={wallets}
108+
onError={err => {
109+
console.error(err);
110+
}}
111+
>
112+
<Web3SelectWalletInner onConnect={onConnect} />
113+
</WalletProvider>
114+
</ConnectionProvider>
115+
);
116+
};

packages/shared/src/types/elementIds.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export type FieldId =
2525
| 'apiKeyDescription'
2626
| 'apiKeyExpirationDate'
2727
| 'apiKeyRevokeConfirmation'
28-
| 'apiKeySecret';
28+
| 'apiKeySecret'
29+
| 'web3WalletName';
2930
export type ProfileSectionId =
3031
| 'profile'
3132
| 'username'

0 commit comments

Comments
 (0)