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
7 changes: 7 additions & 0 deletions .changeset/afraid-apes-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': minor
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

Add Web3 Solana support to `<UserProfile />`
2 changes: 1 addition & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "922.1KB" },
{ "path": "./dist/clerk.js", "maxSize": "922.6KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "87KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "129KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "66KB" },
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/ui/common/WalletInitialIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export const WalletInitialIcon = (props: WalletInitialIconProps) => {
return (
<Box
as='span'
elementDescriptor={[descriptors.walletIcon, descriptors.web3WalletButtonsWalletInitialIcon]}
elementId={descriptors.web3WalletButtonsWalletInitialIcon.setId(id)}
elementDescriptor={[descriptors.walletIcon, descriptors.web3SolanaWalletButtonsWalletInitialIcon]}
elementId={descriptors.web3SolanaWalletButtonsWalletInitialIcon.setId(id)}
sx={t => ({
...common.centeredFlex('inline-flex'),
width: t.space.$4,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
import { Header } from '@/ui/elements/Header';
import { web3CallbackErrorHandler } from '@/ui/utils/web3CallbackErrorHandler';

const Web3WalletButtons = lazy(() =>
const Web3SolanaWalletButtons = lazy(() =>
import(/* webpackChunkName: "web3-wallet-buttons" */ '@/ui/elements/Web3SolanaWalletButtons').then(m => ({
default: m.Web3SolanaWalletButtons,
})),
Expand Down Expand Up @@ -60,7 +60,7 @@ const SignInFactorOneSolanaWalletsCardInner = () => {
</Flex>
}
>
<Web3WalletButtons
<Web3SolanaWalletButtons
web3AuthCallback={({ walletName }) => {
return clerk
.authenticateWithWeb3({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { useClerk } from '@clerk/shared/react';
import { lazy, Suspense } from 'react';

import { withRedirectToAfterSignUp, withRedirectToSignUpTask } from '@/ui/common/withRedirect';
import { descriptors, Flex, Flow, localizationKeys } from '@/ui/customizables';
import { descriptors, Flex, Flow, localizationKeys, Spinner } from '@/ui/customizables';
import { BackLink } from '@/ui/elements/BackLink';
import { Card } from '@/ui/elements/Card';
import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
import { Header } from '@/ui/elements/Header';
import { web3CallbackErrorHandler } from '@/ui/utils/web3CallbackErrorHandler';

const Web3WalletButtons = lazy(() =>
const Web3SolanaWalletButtons = lazy(() =>
import(/* webpackChunkName: "web3-wallet-buttons" */ '@/ui/elements/Web3SolanaWalletButtons').then(m => ({
default: m.Web3SolanaWalletButtons,
})),
Expand Down Expand Up @@ -41,8 +41,26 @@ const SignUpStartSolanaWalletsCardInner = () => {
direction='col'
gap={4}
>
<Suspense fallback={null}>
<Web3WalletButtons
<Suspense
fallback={
<Flex
direction={'row'}
align={'center'}
justify={'center'}
sx={t => ({
height: '100%',
minHeight: t.sizes.$32,
})}
>
<Spinner
size={'lg'}
colorScheme={'primary'}
elementDescriptor={descriptors.spinner}
/>
</Flex>
}
>
<Web3SolanaWalletButtons
web3AuthCallback={({ walletName }) => {
return clerk
.authenticateWithWeb3({
Expand Down
112 changes: 66 additions & 46 deletions packages/clerk-js/src/ui/components/UserProfile/Web3Form.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,52 @@
import { useReverification, useUser } from '@clerk/shared/react';
import type { Web3Provider, Web3Strategy } from '@clerk/shared/types';

import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
import { Web3SelectSolanaWalletScreen } from '@/ui/components/UserProfile/Web3SelectSolanaWalletScreen';
import { Action } from '@/ui/elements/Action';
import { useActionContext } from '@/ui/elements/Action/ActionRoot';
import { useCardState } from '@/ui/elements/contexts';
import { ProfileSection } from '@/ui/elements/Section';
import { getFieldError, handleError } from '@/ui/utils/errorHandler';

import { generateWeb3Signature, getWeb3Identifier } from '../../../utils/web3';
import { descriptors, Image, localizationKeys, Text } from '../../customizables';
import { useEnabledThirdPartyProviders } from '../../hooks';

export const AddWeb3WalletActionMenu = withCardStateProvider(({ onClick }: { onClick?: () => void }) => {
export const AddWeb3WalletActionMenu = () => {
const card = useCardState();
const { open } = useActionContext();
const { user } = useUser();
const { strategies, strategyToDisplayData } = useEnabledThirdPartyProviders();

const enabledStrategies = strategies.filter(s => s.startsWith('web3')) as Web3Strategy[];
const connectedStrategies = user?.verifiedWeb3Wallets.map(w => w.verification.strategy) as Web3Strategy[];
const connectedStrategies = user?.verifiedWeb3Wallets?.map(w => w.verification.strategy) ?? ([] as Web3Strategy[]);
const unconnectedStrategies = enabledStrategies.filter(strategy => {
return !connectedStrategies.includes(strategy);
return !connectedStrategies.includes(strategy) && strategyToDisplayData[strategy];
});

if (unconnectedStrategies.length === 0) {
return null;
}

const createWeb3Wallet = useReverification((identifier: string) =>
user?.createWeb3Wallet({ web3Wallet: identifier }),
);

const connect = async (strategy: Web3Strategy) => {
// If the user selects `web3_solana_signature` as their strategy,
// we need to obtain the wallet name to use when connecting and signing the message during the auth flow
//
// Otherwise, our current Web3 providers are all based on the wallet provider name,
// which is sufficient for our current use case when connecting to a wallet.
const connect = async ({ strategy, walletName }: { strategy: Web3Strategy; walletName?: string }) => {
if (strategy === 'web3_solana_signature' && !walletName) {
open('web3Wallets');
return;
}
const provider = strategy.replace('web3_', '').replace('_signature', '') as Web3Provider;
card.setError(undefined);

try {
card.setLoading(strategy);
const identifier = await getWeb3Identifier({ provider });
const identifier = await getWeb3Identifier({ provider, walletName });

if (!user) {
throw new Error('user is not defined');
Expand All @@ -38,7 +55,7 @@ export const AddWeb3WalletActionMenu = withCardStateProvider(({ onClick }: { onC
let web3Wallet = await createWeb3Wallet(identifier);
web3Wallet = await web3Wallet?.prepareVerification({ strategy });
const message = web3Wallet?.verification.message as string;
const signature = await generateWeb3Signature({ identifier, nonce: message, provider });
const signature = await generateWeb3Signature({ identifier, nonce: message, provider, walletName });
await web3Wallet?.attemptVerification({ signature });
card.setIdle();
} catch (err) {
Expand All @@ -52,45 +69,48 @@ export const AddWeb3WalletActionMenu = withCardStateProvider(({ onClick }: { onC
}
};

if (unconnectedStrategies.length === 0) {
return null;
}

return (
<>
<ProfileSection.ActionMenu
id='web3Wallets'
triggerLocalizationKey={localizationKeys('userProfile.start.web3WalletsSection.primaryButton')}
onClick={onClick}
>
{unconnectedStrategies.map(strategy => (
<ProfileSection.ActionMenuItem
key={strategy}
id={strategyToDisplayData[strategy].id}
onClick={() => connect(strategy)}
isLoading={card.loadingMetadata === strategy}
isDisabled={card.isLoading}
localizationKey={localizationKeys('userProfile.web3WalletPage.web3WalletButtonsBlockButton', {
provider: strategyToDisplayData[strategy].name,
})}
sx={t => ({
justifyContent: 'start',
gap: t.space.$2,
})}
leftIcon={
<Image
elementDescriptor={descriptors.providerIcon}
elementId={descriptors.providerIcon.setId(strategyToDisplayData[strategy].id)}
isLoading={card.loadingMetadata === strategy}
isDisabled={card.isLoading}
src={strategyToDisplayData[strategy].iconUrl}
alt={`Connect ${strategyToDisplayData[strategy].name}`}
sx={theme => ({ width: theme.sizes.$5 })}
/>
}
/>
))}
</ProfileSection.ActionMenu>
<Action.Closed value='web3Wallets'>
<ProfileSection.ActionMenu
id='web3Wallets'
triggerLocalizationKey={localizationKeys('userProfile.start.web3WalletsSection.primaryButton')}
>
{unconnectedStrategies.map(strategy => (
<ProfileSection.ActionMenuItem
key={strategy}
id={strategyToDisplayData[strategy].id}
onClick={() => connect({ strategy })}
isLoading={card.loadingMetadata === strategy}
isDisabled={card.isLoading}
localizationKey={localizationKeys('userProfile.web3WalletPage.web3WalletButtonsBlockButton', {
provider: strategyToDisplayData[strategy].name,
})}
sx={t => ({
justifyContent: 'start',
gap: t.space.$2,
})}
leftIcon={
<Image
elementDescriptor={descriptors.providerIcon}
elementId={descriptors.providerIcon.setId(strategyToDisplayData[strategy].id)}
isLoading={card.loadingMetadata === strategy}
isDisabled={card.isLoading}
src={strategyToDisplayData[strategy].iconUrl}
alt={`Connect ${strategyToDisplayData[strategy].name}`}
sx={theme => ({ width: theme.sizes.$5 })}
/>
}
/>
))}
</ProfileSection.ActionMenu>
</Action.Closed>
<Action.Open value='web3Wallets'>
<Action.Card>
<Web3SelectSolanaWalletScreen onConnect={connect} />
</Action.Card>
</Action.Open>

{card.error && (
<Text
colorScheme='danger'
Expand All @@ -104,4 +124,4 @@ export const AddWeb3WalletActionMenu = withCardStateProvider(({ onClick }: { onC
)}
</>
);
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export const Web3Section = withCardStateProvider(
);
})}
</ProfileSection.ItemList>
{shouldAllowCreation && <AddWeb3WalletActionMenu onClick={() => setActionValue(null)} />}
{shouldAllowCreation && <AddWeb3WalletActionMenu />}
</Action.Root>
</ProfileSection.Root>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { Web3Strategy } from '@clerk/shared/types';
import { lazy, Suspense } from 'react';

import { useActionContext } from '@/ui/elements/Action/ActionRoot';
import { useCardState } from '@/ui/elements/contexts';
import { Form } from '@/ui/elements/Form';
import { FormButtonContainer } from '@/ui/elements/FormButtons';
import { FormContainer } from '@/ui/elements/FormContainer';

import { Button, descriptors, Flex, localizationKeys, Spinner } from '../../customizables';

const Web3SolanaWalletButtons = lazy(() =>
import(/* webpackChunkName: "web3-wallet-buttons" */ '@/ui/elements/Web3SolanaWalletButtons').then(m => ({
default: m.Web3SolanaWalletButtons,
})),
);

export type Web3SelectWalletProps = {
onConnect: (params: { strategy: Web3Strategy; walletName: string }) => Promise<void>;
};

export const Web3SelectSolanaWalletScreen = ({ onConnect }: Web3SelectWalletProps) => {
const card = useCardState();
const { close } = useActionContext();

const onClick = async ({ walletName }: { walletName: string }) => {
card.setLoading(walletName);
try {
await onConnect({ strategy: 'web3_solana_signature', walletName });
card.setIdle();
} catch (err) {
card.setIdle();
console.error(err);
} finally {
close();
}
};

return (
<FormContainer
headerTitle={localizationKeys('userProfile.start.web3WalletsSection.web3SelectSolanaWalletScreen.title')}
headerSubtitle={localizationKeys('userProfile.start.web3WalletsSection.web3SelectSolanaWalletScreen.subtitle')}
>
<Form.Root>
<Suspense
fallback={
<Flex
direction={'row'}
align={'center'}
justify={'center'}
sx={t => ({
height: '100%',
minHeight: t.sizes.$32,
})}
>
<Spinner
size={'lg'}
colorScheme={'primary'}
elementDescriptor={descriptors.spinner}
/>
</Flex>
}
>
<Web3SolanaWalletButtons web3AuthCallback={onClick} />
</Suspense>
<FormButtonContainer>
<Button
variant='ghost'
onClick={() => {
close();
}}
localizationKey={localizationKeys('userProfile.formButtonReset')}
elementDescriptor={descriptors.formButtonReset}
/>
</FormButtonContainer>
</Form.Root>
</FormContainer>
);
};
14 changes: 7 additions & 7 deletions packages/clerk-js/src/ui/customizables/elementDescriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,13 +516,13 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
'enterpriseConnectionButton',
'enterpriseConnectionButtonText',

'web3WalletButtonsRoot',
'web3WalletButtons',
'web3WalletButtonsIconButton',
'web3WalletButtonsBlockButton',
'web3WalletButtonsBlockButtonText',
'web3WalletButtonsWalletIcon',
'web3WalletButtonsWalletInitialIcon',
'web3SolanaWalletButtonsRoot',
'web3SolanaWalletButtons',
'web3SolanaWalletButtonsIconButton',
'web3SolanaWalletButtonsBlockButton',
'web3SolanaWalletButtonsBlockButtonText',
'web3SolanaWalletButtonsWalletIcon',
'web3SolanaWalletButtonsWalletInitialIcon',

'walletIcon',
'walletInitialIcon',
Expand Down
Loading