From 645d58afe74c43e13ed79b560830f8ae192a8980 Mon Sep 17 00:00:00 2001 From: Michael Novotny Date: Tue, 29 Sep 2026 21:38:44 -0500 Subject: [PATCH 1/3] fix(ui): list Solana wallets through the Wallet Standard The Solana wallet buttons used @solana/wallet-adapter-react only to list installed wallets. Signing already goes through @wallet-standard/core in @clerk/shared, so read the list from the same registry and drop the adapter packages. They pulled React Native, Metro, and @solana/web3.js into every @clerk/ui install. Co-Authored-By: Claude Opus 5.5 --- .changeset/ui-solana-wallet-standard.md | 5 ++ packages/ui/package.json | 4 +- .../src/elements/Web3SolanaWalletButtons.tsx | 71 +++++++-------- .../Web3SolanaWalletButtons.test.tsx | 88 +++++++++++++++++++ pnpm-lock.yaml | 12 +-- 5 files changed, 129 insertions(+), 51 deletions(-) create mode 100644 .changeset/ui-solana-wallet-standard.md create mode 100644 packages/ui/src/elements/__tests__/Web3SolanaWalletButtons.test.tsx diff --git a/.changeset/ui-solana-wallet-standard.md b/.changeset/ui-solana-wallet-standard.md new file mode 100644 index 00000000000..7a824780a28 --- /dev/null +++ b/.changeset/ui-solana-wallet-standard.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +List Solana wallets through the Wallet Standard instead of `@solana/wallet-adapter-react`. `@clerk/ui` no longer depends on the Solana wallet adapter packages, which pulled in React Native, Metro, and `@solana/web3.js`, and the lazy-loaded Solana wallet chunk drops from about 306 KB to 2 KB. The list now shows exactly the wallets Clerk can sign in with. 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..7818aca9054 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(); +// Matches the wallets `getInjectedWeb3SolanaProviders` in @clerk/shared can sign in with, so every button works. +const isSolanaSignInWallet = (wallet: Wallet) => + wallet.chains.some(chain => chain.startsWith('solana:')) && 'solana:signMessage' in wallet.features; + +const getRegisteredWallets = () => (typeof window === 'undefined' ? [] : getWallets().get()); + +// Installed wallets announce themselves through the Wallet Standard, sometimes after this mounts. +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..830a1ce64b9 --- /dev/null +++ b/packages/ui/src/elements/__tests__/Web3SolanaWalletButtons.test.tsx @@ -0,0 +1,88 @@ +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 { 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 sign messages', async () => { + registry.wallets = [ + makeWallet('Phantom', ['solana:mainnet'], ['solana:signMessage']), + makeWallet('Solana Viewer', ['solana:mainnet'], ['standard:connect']), + makeWallet('MetaMask', ['eip155:1'], ['standard:connect']), + ]; + const { wrapper } = await createFixtures(); + + render(, { wrapper }); + + expect(await screen.findByText('Continue with Phantom')).toBeInTheDocument(); + expect(screen.queryByText(/Solana Viewer/)).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'], ['solana:signMessage'])]; + 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'], ['solana:signMessage'])]; + 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..4186ee610d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1245,18 +1245,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 From e015fdf83f2127e91e572334ab53f7a8294f5ad1 Mon Sep 17 00:00:00 2001 From: Michael Novotny Date: Tue, 29 Sep 2026 22:31:50 -0500 Subject: [PATCH 2/3] chore(clerk-js): drop unused Solana wallet adapter dependencies clerk-js never imports them. @clerk/shared's Solana code only type-imports @solana/wallet-standard and lazy-loads @wallet-standard/core at runtime, which stays. Co-Authored-By: Claude Opus 5.5 --- .changeset/ui-solana-wallet-standard.md | 3 ++- packages/clerk-js/package.json | 3 --- pnpm-lock.yaml | 9 --------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/.changeset/ui-solana-wallet-standard.md b/.changeset/ui-solana-wallet-standard.md index 7a824780a28..75eb1eb9385 100644 --- a/.changeset/ui-solana-wallet-standard.md +++ b/.changeset/ui-solana-wallet-standard.md @@ -1,5 +1,6 @@ --- '@clerk/ui': patch +'@clerk/clerk-js': patch --- -List Solana wallets through the Wallet Standard instead of `@solana/wallet-adapter-react`. `@clerk/ui` no longer depends on the Solana wallet adapter packages, which pulled in React Native, Metro, and `@solana/web3.js`, and the lazy-loaded Solana wallet chunk drops from about 306 KB to 2 KB. The list now shows exactly the wallets Clerk can sign in with. +List Solana wallets through the Wallet Standard instead of `@solana/wallet-adapter-react`. `@clerk/ui` no longer depends on the Solana wallet adapter packages, which pulled in React Native, Metro, and `@solana/web3.js`, and the lazy-loaded Solana wallet chunk drops from about 306 KB to 2 KB. The list now shows exactly the wallets Clerk can sign in with. `@clerk/clerk-js` drops the same packages, which it listed but never imported. 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/pnpm-lock.yaml b/pnpm-lock.yaml index 4186ee610d6..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 From 4b8da39bf61f37feeb7281f5628f6f4e7a9c2890 Mon Sep 17 00:00:00 2001 From: Michael Novotny Date: Tue, 29 Sep 2026 22:58:23 -0500 Subject: [PATCH 3/3] fix(ui): list only Solana wallets that can connect Sign-in calls standard:connect before signing, so a wallet without it could be listed but never finish. Also drops the new code comments and rewrites the changeset for people upgrading. Co-Authored-By: Claude Opus 5.5 --- .changeset/ui-solana-wallet-standard.md | 2 +- .../ui/src/elements/Web3SolanaWalletButtons.tsx | 6 +++--- .../__tests__/Web3SolanaWalletButtons.test.tsx | 14 +++++++++----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.changeset/ui-solana-wallet-standard.md b/.changeset/ui-solana-wallet-standard.md index 75eb1eb9385..424dbe7ab16 100644 --- a/.changeset/ui-solana-wallet-standard.md +++ b/.changeset/ui-solana-wallet-standard.md @@ -3,4 +3,4 @@ '@clerk/clerk-js': patch --- -List Solana wallets through the Wallet Standard instead of `@solana/wallet-adapter-react`. `@clerk/ui` no longer depends on the Solana wallet adapter packages, which pulled in React Native, Metro, and `@solana/web3.js`, and the lazy-loaded Solana wallet chunk drops from about 306 KB to 2 KB. The list now shows exactly the wallets Clerk can sign in with. `@clerk/clerk-js` drops the same packages, which it listed but never imported. +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/ui/src/elements/Web3SolanaWalletButtons.tsx b/packages/ui/src/elements/Web3SolanaWalletButtons.tsx index 7818aca9054..8ea9c60b9ce 100644 --- a/packages/ui/src/elements/Web3SolanaWalletButtons.tsx +++ b/packages/ui/src/elements/Web3SolanaWalletButtons.tsx @@ -31,13 +31,13 @@ const SOCIAL_BUTTON_BLOCK_THRESHOLD = 2; const SOCIAL_BUTTON_PRE_TEXT_THRESHOLD = 1; const MAX_STRATEGIES_PER_ROW = 5; -// Matches the wallets `getInjectedWeb3SolanaProviders` in @clerk/shared can sign in with, so every button works. const isSolanaSignInWallet = (wallet: Wallet) => - wallet.chains.some(chain => chain.startsWith('solana:')) && 'solana:signMessage' in wallet.features; + wallet.chains.some(chain => chain.startsWith('solana:')) && + 'standard:connect' in wallet.features && + 'solana:signMessage' in wallet.features; const getRegisteredWallets = () => (typeof window === 'undefined' ? [] : getWallets().get()); -// Installed wallets announce themselves through the Wallet Standard, sometimes after this mounts. const useInstalledSolanaWallets = () => { const [wallets, setWallets] = useState(getRegisteredWallets); diff --git a/packages/ui/src/elements/__tests__/Web3SolanaWalletButtons.test.tsx b/packages/ui/src/elements/__tests__/Web3SolanaWalletButtons.test.tsx index 830a1ce64b9..ffb2a9f0fa4 100644 --- a/packages/ui/src/elements/__tests__/Web3SolanaWalletButtons.test.tsx +++ b/packages/ui/src/elements/__tests__/Web3SolanaWalletButtons.test.tsx @@ -33,6 +33,8 @@ const makeWallet = (name: string, chains: string[], features: string[]): Wallet 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); @@ -44,11 +46,12 @@ describe('Web3SolanaWalletButtons', () => { registry.listeners.unregister.clear(); }); - it('lists only Solana wallets that can sign messages', async () => { + it('lists only Solana wallets that can connect and sign messages', async () => { registry.wallets = [ - makeWallet('Phantom', ['solana:mainnet'], ['solana:signMessage']), + makeWallet('Phantom', ['solana:mainnet'], SIGN_IN_FEATURES), makeWallet('Solana Viewer', ['solana:mainnet'], ['standard:connect']), - makeWallet('MetaMask', ['eip155:1'], ['standard:connect']), + makeWallet('Solana Signer', ['solana:mainnet'], ['solana:signMessage']), + makeWallet('MetaMask', ['eip155:1'], SIGN_IN_FEATURES), ]; const { wrapper } = await createFixtures(); @@ -56,6 +59,7 @@ describe('Web3SolanaWalletButtons', () => { 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(); }); @@ -67,7 +71,7 @@ describe('Web3SolanaWalletButtons', () => { expect(await screen.findByText(/No Solana Web3 wallets detected/)).toBeInTheDocument(); act(() => { - registry.wallets = [makeWallet('Backpack', ['solana:mainnet'], ['solana:signMessage'])]; + registry.wallets = [makeWallet('Backpack', ['solana:mainnet'], SIGN_IN_FEATURES)]; registry.listeners.register.forEach(listener => listener()); }); @@ -75,7 +79,7 @@ describe('Web3SolanaWalletButtons', () => { }); it('passes the chosen wallet name to the auth callback', async () => { - registry.wallets = [makeWallet('Phantom', ['solana:mainnet'], ['solana:signMessage'])]; + registry.wallets = [makeWallet('Phantom', ['solana:mainnet'], SIGN_IN_FEATURES)]; const web3AuthCallback = vi.fn().mockResolvedValue(undefined); const { wrapper } = await createFixtures();