diff --git a/.changeset/afraid-apes-cough.md b/.changeset/afraid-apes-cough.md new file mode 100644 index 00000000000..d56bab12231 --- /dev/null +++ b/.changeset/afraid-apes-cough.md @@ -0,0 +1,7 @@ +--- +'@clerk/localizations': minor +'@clerk/clerk-js': minor +'@clerk/shared': minor +--- + +Add Web3 Solana support to `` diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index f1ff076fd5b..562644e636a 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -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" }, diff --git a/packages/clerk-js/src/ui/common/WalletInitialIcon.tsx b/packages/clerk-js/src/ui/common/WalletInitialIcon.tsx index 362a9f64312..3272f295480 100644 --- a/packages/clerk-js/src/ui/common/WalletInitialIcon.tsx +++ b/packages/clerk-js/src/ui/common/WalletInitialIcon.tsx @@ -16,8 +16,8 @@ export const WalletInitialIcon = (props: WalletInitialIconProps) => { return ( ({ ...common.centeredFlex('inline-flex'), width: t.space.$4, diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneSolanaWalletsCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneSolanaWalletsCard.tsx index 321ac4ee56d..aa4674a24e7 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneSolanaWalletsCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneSolanaWalletsCard.tsx @@ -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, })), @@ -60,7 +60,7 @@ const SignInFactorOneSolanaWalletsCardInner = () => { } > - { return clerk .authenticateWithWeb3({ diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStartSolanaWalletsCard.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStartSolanaWalletsCard.tsx index 6968e59052a..ed00e6e3e69 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpStartSolanaWalletsCard.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStartSolanaWalletsCard.tsx @@ -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, })), @@ -41,8 +41,26 @@ const SignUpStartSolanaWalletsCardInner = () => { direction='col' gap={4} > - - ({ + height: '100%', + minHeight: t.sizes.$32, + })} + > + + + } + > + { return clerk .authenticateWithWeb3({ diff --git a/packages/clerk-js/src/ui/components/UserProfile/Web3Form.tsx b/packages/clerk-js/src/ui/components/UserProfile/Web3Form.tsx index 8ebea04501d..c80ce1eb8c0 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/Web3Form.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/Web3Form.tsx @@ -1,7 +1,10 @@ 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'; @@ -9,27 +12,41 @@ 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'); @@ -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) { @@ -52,45 +69,48 @@ export const AddWeb3WalletActionMenu = withCardStateProvider(({ onClick }: { onC } }; - if (unconnectedStrategies.length === 0) { - return null; - } - return ( <> - - {unconnectedStrategies.map(strategy => ( - 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={ - {`Connect ({ width: theme.sizes.$5 })} - /> - } - /> - ))} - + + + {unconnectedStrategies.map(strategy => ( + 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={ + {`Connect ({ width: theme.sizes.$5 })} + /> + } + /> + ))} + + + + + + + + {card.error && ( ); -}); +}; diff --git a/packages/clerk-js/src/ui/components/UserProfile/Web3Section.tsx b/packages/clerk-js/src/ui/components/UserProfile/Web3Section.tsx index 1902f819f19..cbdd0934091 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/Web3Section.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/Web3Section.tsx @@ -108,7 +108,7 @@ export const Web3Section = withCardStateProvider( ); })} - {shouldAllowCreation && setActionValue(null)} />} + {shouldAllowCreation && } ); diff --git a/packages/clerk-js/src/ui/components/UserProfile/Web3SelectSolanaWalletScreen.tsx b/packages/clerk-js/src/ui/components/UserProfile/Web3SelectSolanaWalletScreen.tsx new file mode 100644 index 00000000000..707b591f056 --- /dev/null +++ b/packages/clerk-js/src/ui/components/UserProfile/Web3SelectSolanaWalletScreen.tsx @@ -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; +}; + +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 ( + + + ({ + height: '100%', + minHeight: t.sizes.$32, + })} + > + + + } + > + + + +