diff --git a/.changeset/ui-solana-wallet-standard.md b/.changeset/ui-solana-wallet-standard.md new file mode 100644 index 00000000000..424dbe7ab16 --- /dev/null +++ b/.changeset/ui-solana-wallet-standard.md @@ -0,0 +1,6 @@ +--- +'@clerk/ui': patch +'@clerk/clerk-js': patch +--- + +The Solana wallet picker downloads about 2 KB instead of about 306 KB, and installing `@clerk/ui` or `@clerk/clerk-js` no longer pulls in React Native, Metro, and `@solana/web3.js`. The picker now lists only wallets that can complete Sign in with Solana, so the Mobile Wallet Adapter option that appeared on Android Chrome is gone. diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index 271835eca03..b82378f860c 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -88,9 +88,6 @@ "@base-org/account": "catalog:module-manager", "@clerk/shared": "workspace:^", "@coinbase/wallet-sdk": "catalog:module-manager", - "@solana/wallet-adapter-base": "catalog:module-manager", - "@solana/wallet-adapter-react": "catalog:module-manager", - "@solana/wallet-standard": "catalog:module-manager", "@stripe/stripe-js": "5.6.0", "@swc/helpers": "catalog:repo", "@tanstack/query-core": "catalog:repo", diff --git a/packages/ui/package.json b/packages/ui/package.json index 4905bd6c41c..67fbf4ae428 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -107,10 +107,8 @@ "@emotion/react": "11.11.1", "@floating-ui/react": "catalog:repo", "@formkit/auto-animate": "^0.8.2", - "@solana/wallet-adapter-base": "catalog:module-manager", - "@solana/wallet-adapter-react": "catalog:module-manager", - "@solana/wallet-standard": "catalog:module-manager", "@swc/helpers": "catalog:repo", + "@wallet-standard/core": "catalog:module-manager", "copy-to-clipboard": "3.3.3", "core-js": "catalog:repo", "csstype": "3.1.3", diff --git a/packages/ui/src/elements/Web3SolanaWalletButtons.tsx b/packages/ui/src/elements/Web3SolanaWalletButtons.tsx index cfa88a47b0b..8ea9c60b9ce 100644 --- a/packages/ui/src/elements/Web3SolanaWalletButtons.tsx +++ b/packages/ui/src/elements/Web3SolanaWalletButtons.tsx @@ -1,8 +1,6 @@ -import { WalletReadyState } from '@solana/wallet-adapter-base'; -import { ConnectionProvider, useWallet, WalletProvider } from '@solana/wallet-adapter-react'; -import { MAINNET_ENDPOINT } from '@solana/wallet-standard'; +import { getWallets, type Wallet } from '@wallet-standard/core'; import type { Ref } from 'react'; -import React, { forwardRef, isValidElement, useMemo } from 'react'; +import React, { forwardRef, isValidElement, useEffect, useMemo, useState } from 'react'; import { WalletInitialIcon } from '@/ui/common/WalletInitialIcon'; import { @@ -33,26 +31,38 @@ const SOCIAL_BUTTON_BLOCK_THRESHOLD = 2; const SOCIAL_BUTTON_PRE_TEXT_THRESHOLD = 1; const MAX_STRATEGIES_PER_ROW = 5; -const Web3SolanaWalletButtonsInner = ({ web3AuthCallback }: Web3WalletButtonsProps) => { - const card = useCardState(); - const { wallets } = useWallet(); - const { t } = useLocalizations(); +const isSolanaSignInWallet = (wallet: Wallet) => + wallet.chains.some(chain => chain.startsWith('solana:')) && + 'standard:connect' in wallet.features && + 'solana:signMessage' in wallet.features; + +const getRegisteredWallets = () => (typeof window === 'undefined' ? [] : getWallets().get()); + +const useInstalledSolanaWallets = () => { + const [wallets, setWallets] = useState(getRegisteredWallets); - // Filter to only show installed wallets - const installedWallets = React.useMemo( - () => - wallets - .filter(w => { - return w.readyState === WalletReadyState.Installed; - }) - .map(wallet => { - return { - name: wallet.adapter.name, - icon: wallet.adapter.icon, - }; - }), + useEffect(() => { + const registry = getWallets(); + const update = () => setWallets(registry.get()); + update(); + const offRegister = registry.on('register', update); + const offUnregister = registry.on('unregister', update); + return () => { + offRegister(); + offUnregister(); + }; + }, []); + + return useMemo( + () => wallets.filter(isSolanaSignInWallet).map(wallet => ({ name: wallet.name, icon: wallet.icon })), [wallets], ); +}; + +export const Web3SolanaWalletButtons = ({ web3AuthCallback }: Web3WalletButtonsProps) => { + const card = useCardState(); + const installedWallets = useInstalledSolanaWallets(); + const { t } = useLocalizations(); const startWeb3AuthFlow = (walletName: string) => async () => { card.setLoading(walletName); @@ -113,7 +123,7 @@ const Web3SolanaWalletButtonsInner = ({ web3AuthCallback }: Web3WalletButtonsPro gridTemplateColumns: shouldForceSingleColumnOnMobile ? 'repeat(1, minmax(0, 1fr))' : undefined, }, gridTemplateColumns: - wallets.length < 1 + installedWallets.length < 1 ? `repeat(1, minmax(0, 1fr))` : `repeat(${row.length}, ${ rowIndex === 0 @@ -262,20 +272,3 @@ const WalletButtonBlock = forwardRef((props: WalletButtonProps, ref: Ref ); }); - -export const Web3SolanaWalletButtons = (props: Web3WalletButtonsProps) => { - const network = MAINNET_ENDPOINT; - const wallets = useMemo(() => [], [network]); - return ( - - { - console.error(err); - }} - > - - - - ); -}; diff --git a/packages/ui/src/elements/__tests__/Web3SolanaWalletButtons.test.tsx b/packages/ui/src/elements/__tests__/Web3SolanaWalletButtons.test.tsx new file mode 100644 index 00000000000..ffb2a9f0fa4 --- /dev/null +++ b/packages/ui/src/elements/__tests__/Web3SolanaWalletButtons.test.tsx @@ -0,0 +1,92 @@ +import type { Wallet } from '@wallet-standard/core'; +import { act } from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { bindCreateFixtures } from '@/test/create-fixtures'; +import { render, screen } from '@/test/utils'; + +import { withCardStateProvider } from '../contexts'; +import { Web3SolanaWalletButtons } from '../Web3SolanaWalletButtons'; + +const registry = vi.hoisted(() => ({ + wallets: [] as Wallet[], + listeners: { register: new Set<() => void>(), unregister: new Set<() => void>() }, +})); + +vi.mock('@wallet-standard/core', () => ({ + getWallets: () => ({ + get: () => registry.wallets, + on: (event: 'register' | 'unregister', listener: () => void) => { + registry.listeners[event].add(listener); + return () => registry.listeners[event].delete(listener); + }, + }), +})); + +const makeWallet = (name: string, chains: string[], features: string[]): Wallet => + ({ + name, + icon: 'data:image/svg+xml;base64,', + version: '1.0.0', + chains, + accounts: [], + features: Object.fromEntries(features.map(feature => [feature, {}])), + }) as unknown as Wallet; + +const SIGN_IN_FEATURES = ['standard:connect', 'solana:signMessage']; + +const { createFixtures } = bindCreateFixtures('SignIn'); + +const Buttons = withCardStateProvider(Web3SolanaWalletButtons); + +describe('Web3SolanaWalletButtons', () => { + beforeEach(() => { + registry.wallets = []; + registry.listeners.register.clear(); + registry.listeners.unregister.clear(); + }); + + it('lists only Solana wallets that can connect and sign messages', async () => { + registry.wallets = [ + makeWallet('Phantom', ['solana:mainnet'], SIGN_IN_FEATURES), + makeWallet('Solana Viewer', ['solana:mainnet'], ['standard:connect']), + makeWallet('Solana Signer', ['solana:mainnet'], ['solana:signMessage']), + makeWallet('MetaMask', ['eip155:1'], SIGN_IN_FEATURES), + ]; + const { wrapper } = await createFixtures(); + + render(, { wrapper }); + + expect(await screen.findByText('Continue with Phantom')).toBeInTheDocument(); + expect(screen.queryByText(/Solana Viewer/)).not.toBeInTheDocument(); + expect(screen.queryByText(/Solana Signer/)).not.toBeInTheDocument(); + expect(screen.queryByText(/MetaMask/)).not.toBeInTheDocument(); + }); + + it('shows the none-available message, then picks up a wallet that registers later', async () => { + const { wrapper } = await createFixtures(); + + render(, { wrapper }); + + expect(await screen.findByText(/No Solana Web3 wallets detected/)).toBeInTheDocument(); + + act(() => { + registry.wallets = [makeWallet('Backpack', ['solana:mainnet'], SIGN_IN_FEATURES)]; + registry.listeners.register.forEach(listener => listener()); + }); + + expect(await screen.findByText('Continue with Backpack')).toBeInTheDocument(); + }); + + it('passes the chosen wallet name to the auth callback', async () => { + registry.wallets = [makeWallet('Phantom', ['solana:mainnet'], SIGN_IN_FEATURES)]; + const web3AuthCallback = vi.fn().mockResolvedValue(undefined); + const { wrapper } = await createFixtures(); + + const { userEvent } = render(, { wrapper }); + + await userEvent.click(await screen.findByText('Continue with Phantom')); + + expect(web3AuthCallback).toHaveBeenCalledWith({ walletName: 'Phantom' }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df232371ea4..977c305c4f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -461,15 +461,6 @@ importers: '@coinbase/wallet-sdk': specifier: catalog:module-manager version: 4.3.7(bufferutil@4.1.0)(typescript@6.0.3)(utf-8-validate@5.0.10)(zod@4.4.3) - '@solana/wallet-adapter-base': - specifier: catalog:module-manager - version: 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.3)(utf-8-validate@5.0.10)) - '@solana/wallet-adapter-react': - specifier: catalog:module-manager - version: 0.15.39(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.3)(utf-8-validate@5.0.10))(bs58@6.0.0)(fastestsmallesttextencoderdecoder@1.0.22)(react-native@0.86.0(@babel/core@7.29.7)(@react-native-community/cli@12.3.7(bufferutil@4.1.0)(utf-8-validate@5.0.10))(@types/react@18.3.28)(bufferutil@4.1.0)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@6.0.3) - '@solana/wallet-standard': - specifier: catalog:module-manager - version: 1.1.4(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.3)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.3)(utf-8-validate@5.0.10))(bs58@6.0.0)(react@18.3.1) '@stripe/stripe-js': specifier: 5.6.0 version: 5.6.0 @@ -1245,18 +1236,12 @@ importers: '@formkit/auto-animate': specifier: ^0.8.2 version: 0.8.4 - '@solana/wallet-adapter-base': - specifier: catalog:module-manager - version: 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.3)(utf-8-validate@5.0.10)) - '@solana/wallet-adapter-react': - specifier: catalog:module-manager - version: 0.15.39(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.3)(utf-8-validate@5.0.10))(bs58@6.0.0)(fastestsmallesttextencoderdecoder@1.0.22)(react-native@0.86.0(@babel/core@7.29.7)(@react-native-community/cli@12.3.7(bufferutil@4.1.0)(utf-8-validate@5.0.10))(@types/react@18.3.28)(bufferutil@4.1.0)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@6.0.3) - '@solana/wallet-standard': - specifier: catalog:module-manager - version: 1.1.4(@solana/wallet-adapter-base@0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.3)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.3)(utf-8-validate@5.0.10))(bs58@6.0.0)(react@18.3.1) '@swc/helpers': specifier: catalog:repo version: 0.5.21 + '@wallet-standard/core': + specifier: catalog:module-manager + version: 1.1.1 copy-to-clipboard: specifier: 3.3.3 version: 3.3.3