From 35425460e5c17dd615556528614e49f07bfda2af Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 30 Mar 2026 13:37:50 +0200 Subject: [PATCH 01/11] feat: add useWalletStatus hook with tests New state-only hook that answers "is the wallet ready to interact with this chain?" Built on top of useWeb3Status. Closes part of #303 --- src/hooks/useWalletStatus.test.ts | 158 ++++++++++++++++++++++++++++++ src/hooks/useWalletStatus.ts | 40 ++++++++ 2 files changed, 198 insertions(+) create mode 100644 src/hooks/useWalletStatus.test.ts create mode 100644 src/hooks/useWalletStatus.ts diff --git a/src/hooks/useWalletStatus.test.ts b/src/hooks/useWalletStatus.test.ts new file mode 100644 index 00000000..396ca1ec --- /dev/null +++ b/src/hooks/useWalletStatus.test.ts @@ -0,0 +1,158 @@ +import { renderHook } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { useWalletStatus } from './useWalletStatus' + +// Mock useWeb3Status +const mockSwitchChain = vi.fn() +const mockDisconnect = vi.fn() + +vi.mock('@/src/hooks/useWeb3Status', () => ({ + useWeb3Status: vi.fn(() => ({ + appChainId: 1, + isWalletConnected: false, + isWalletSynced: false, + switchChain: mockSwitchChain, + walletChainId: undefined, + })), +})) + +vi.mock('@/src/lib/networks.config', () => ({ + chains: [ + { id: 1, name: 'Ethereum' }, + { id: 10, name: 'OP Mainnet' }, + { id: 137, name: 'Polygon' }, + ], +})) + +vi.mock('viem', async () => { + const actual = await vi.importActual('viem') + return { + ...actual, + extractChain: vi.fn(({ chains, id }) => { + const chain = chains.find((c: { id: number }) => c.id === id) + if (!chain) { + throw new Error(`Chain with id ${id} not found`) + } + return chain + }), + } +}) + +// Import after mocks are set up +const { useWeb3Status } = await import('@/src/hooks/useWeb3Status') +const mockedUseWeb3Status = vi.mocked(useWeb3Status) + +const baseWeb3Status: ReturnType = { + readOnlyClient: undefined, + appChainId: 1, + address: undefined, + balance: undefined, + connectingWallet: false, + switchingChain: false, + isWalletConnected: false, + walletClient: undefined, + isWalletSynced: false, + walletChainId: undefined, + switchChain: mockSwitchChain, + disconnect: mockDisconnect, +} + +describe('useWalletStatus', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns needsConnect when wallet is not connected', () => { + mockedUseWeb3Status.mockReturnValue({ + ...baseWeb3Status, + appChainId: 1, + isWalletConnected: false, + isWalletSynced: false, + walletChainId: undefined, + }) + + const { result } = renderHook(() => useWalletStatus()) + + expect(result.current.needsConnect).toBe(true) + expect(result.current.needsChainSwitch).toBe(false) + expect(result.current.isReady).toBe(false) + }) + + it('returns needsChainSwitch when connected but on wrong chain', () => { + mockedUseWeb3Status.mockReturnValue({ + ...baseWeb3Status, + appChainId: 1, + isWalletConnected: true, + isWalletSynced: false, + walletChainId: 137, + }) + + const { result } = renderHook(() => useWalletStatus()) + + expect(result.current.needsConnect).toBe(false) + expect(result.current.needsChainSwitch).toBe(true) + expect(result.current.isReady).toBe(false) + expect(result.current.targetChain).toEqual({ id: 1, name: 'Ethereum' }) + }) + + it('returns isReady when connected and on correct chain', () => { + mockedUseWeb3Status.mockReturnValue({ + ...baseWeb3Status, + appChainId: 1, + isWalletConnected: true, + isWalletSynced: true, + walletChainId: 1, + }) + + const { result } = renderHook(() => useWalletStatus()) + + expect(result.current.needsConnect).toBe(false) + expect(result.current.needsChainSwitch).toBe(false) + expect(result.current.isReady).toBe(true) + }) + + it('uses provided chainId over appChainId', () => { + mockedUseWeb3Status.mockReturnValue({ + ...baseWeb3Status, + appChainId: 1, + isWalletConnected: true, + isWalletSynced: true, + walletChainId: 1, + }) + + const { result } = renderHook(() => useWalletStatus({ chainId: 10 })) + + expect(result.current.needsChainSwitch).toBe(true) + expect(result.current.isReady).toBe(false) + expect(result.current.targetChain).toEqual({ id: 10, name: 'OP Mainnet' }) + }) + + it('falls back to chains[0].id when no chainId or appChainId', () => { + mockedUseWeb3Status.mockReturnValue({ + ...baseWeb3Status, + appChainId: undefined as unknown as ReturnType['appChainId'], + isWalletConnected: true, + isWalletSynced: false, + walletChainId: 137, + }) + + const { result } = renderHook(() => useWalletStatus()) + + expect(result.current.targetChain).toEqual({ id: 1, name: 'Ethereum' }) + }) + + it('switchChain calls through to useWeb3Status switchChain', () => { + mockedUseWeb3Status.mockReturnValue({ + ...baseWeb3Status, + appChainId: 1, + isWalletConnected: true, + isWalletSynced: false, + walletChainId: 137, + }) + + const { result } = renderHook(() => useWalletStatus()) + + result.current.switchChain(10) + expect(mockSwitchChain).toHaveBeenCalledWith(10) + }) +}) diff --git a/src/hooks/useWalletStatus.ts b/src/hooks/useWalletStatus.ts new file mode 100644 index 00000000..f51c9ef4 --- /dev/null +++ b/src/hooks/useWalletStatus.ts @@ -0,0 +1,40 @@ +import { extractChain } from 'viem' +import type { Chain } from 'viem' + +import { useWeb3Status } from '@/src/hooks/useWeb3Status' +import { type ChainsIds, chains } from '@/src/lib/networks.config' + +interface UseWalletStatusOptions { + chainId?: ChainsIds +} + +interface WalletStatus { + isReady: boolean + needsConnect: boolean + needsChainSwitch: boolean + targetChain: Chain + switchChain: (chainId: ChainsIds) => void +} + +export const useWalletStatus = (options?: UseWalletStatusOptions): WalletStatus => { + const { appChainId, isWalletConnected, isWalletSynced, switchChain, walletChainId } = + useWeb3Status() + + const targetChain = extractChain({ + chains, + id: options?.chainId || appChainId || chains[0].id, + }) + + const needsConnect = !isWalletConnected + const needsChainSwitch = + isWalletConnected && (!isWalletSynced || walletChainId !== targetChain.id) + const isReady = isWalletConnected && !needsChainSwitch + + return { + isReady, + needsConnect, + needsChainSwitch, + targetChain, + switchChain, + } +} From 63f94f30e9b7fd820a8cd0248a2f1628cc4583cd Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 30 Mar 2026 13:38:16 +0200 Subject: [PATCH 02/11] refactor: WalletStatusVerifier uses useWalletStatus hook, remove HoC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The withWalletStatusVerifier HoC is removed. The WalletStatusVerifier wrapper component now uses the useWalletStatus hook internally. Extracts shared SwitchChainButton to ui/SwitchChainButton.tsx. Same external API — no breaking change for wrapper consumers. Closes part of #303 --- .../WalletStatusVerifier.test.tsx | 214 ++++++++++-------- .../sharedComponents/WalletStatusVerifier.tsx | 101 ++------- .../sharedComponents/ui/SwitchChainButton.tsx | 14 ++ 3 files changed, 149 insertions(+), 180 deletions(-) create mode 100644 src/components/sharedComponents/ui/SwitchChainButton.tsx diff --git a/src/components/sharedComponents/WalletStatusVerifier.test.tsx b/src/components/sharedComponents/WalletStatusVerifier.test.tsx index 09c5b00d..dd8d6acd 100644 --- a/src/components/sharedComponents/WalletStatusVerifier.test.tsx +++ b/src/components/sharedComponents/WalletStatusVerifier.test.tsx @@ -1,122 +1,150 @@ import { ChakraProvider, createSystem, defaultConfig } from '@chakra-ui/react' import { render, screen } from '@testing-library/react' -import type { ReactElement } from 'react' -import { describe, expect, it, vi } from 'vitest' -import { WalletStatusVerifier, withWalletStatusVerifier } from './WalletStatusVerifier' +import userEvent from '@testing-library/user-event' +import { type ReactNode, createElement } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { WalletStatusVerifier } from './WalletStatusVerifier' -const system = createSystem(defaultConfig) +const mockSwitchChain = vi.fn() -vi.mock('@/src/hooks/useWeb3Status', () => ({ - useWeb3Status: vi.fn(), +vi.mock('@/src/hooks/useWalletStatus', () => ({ + useWalletStatus: vi.fn(() => ({ + isReady: false, + needsConnect: true, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' }, + switchChain: mockSwitchChain, + })), })) vi.mock('@/src/providers/Web3Provider', () => ({ - ConnectWalletButton: () => , + ConnectWalletButton: () => + createElement( + 'button', + { type: 'button', 'data-testid': 'connect-wallet-button' }, + 'Connect Wallet', + ), })) -import * as useWeb3StatusModule from '@/src/hooks/useWeb3Status' - -// chains[0] = optimismSepolia (id: 11155420) when PUBLIC_INCLUDE_TESTNETS=true (default) -const OP_SEPOLIA_ID = 11155420 as const - -function connectedSyncedStatus(overrides = {}) { - return { - isWalletConnected: true, - isWalletSynced: true, - walletChainId: OP_SEPOLIA_ID, - appChainId: OP_SEPOLIA_ID, - switchChain: vi.fn(), - disconnect: vi.fn(), - address: '0x1234567890abcdef1234567890abcdef12345678' as `0x${string}`, - balance: undefined, - connectingWallet: false, - switchingChain: false, - walletClient: undefined, - readOnlyClient: undefined, - ...overrides, - } -} - -function wrap(ui: ReactElement) { - return render({ui}) -} +const { useWalletStatus } = await import('@/src/hooks/useWalletStatus') +const mockedUseWalletStatus = vi.mocked(useWalletStatus) + +const system = createSystem(defaultConfig) + +const renderWithChakra = (ui: ReactNode) => + render({ui}) describe('WalletStatusVerifier', () => { - it('renders default ConnectWalletButton fallback when wallet not connected', () => { - vi.mocked(useWeb3StatusModule.useWeb3Status).mockReturnValue( - // biome-ignore lint/suspicious/noExplicitAny: partial mock - connectedSyncedStatus({ isWalletConnected: false, isWalletSynced: false }) as any, - ) - wrap( - -
Protected Content
-
, - ) - expect(screen.getByText('Connect Wallet')).toBeDefined() - expect(screen.queryByText('Protected Content')).toBeNull() + beforeEach(() => { + vi.clearAllMocks() }) - it('renders custom fallback when wallet not connected', () => { - vi.mocked(useWeb3StatusModule.useWeb3Status).mockReturnValue( - // biome-ignore lint/suspicious/noExplicitAny: partial mock - connectedSyncedStatus({ isWalletConnected: false, isWalletSynced: false }) as any, - ) - wrap( - Custom Fallback}> -
Protected Content
-
, + it('renders default fallback (ConnectWalletButton) when wallet needs connect', () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: false, + needsConnect: true, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + switchChain: mockSwitchChain, + }) + + renderWithChakra( + createElement( + WalletStatusVerifier, + null, + createElement('div', { 'data-testid': 'protected-content' }, 'Protected'), + ), ) - expect(screen.getByText('Custom Fallback')).toBeDefined() + + expect(screen.getByTestId('connect-wallet-button')).toBeInTheDocument() + expect(screen.queryByTestId('protected-content')).toBeNull() }) - it('renders switch chain button when wallet is on wrong chain', () => { - vi.mocked(useWeb3StatusModule.useWeb3Status).mockReturnValue( - // biome-ignore lint/suspicious/noExplicitAny: partial mock - connectedSyncedStatus({ isWalletSynced: false, walletChainId: 1 }) as any, - ) - wrap( - -
Protected Content
-
, + it('renders custom fallback when provided and wallet needs connect', () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: false, + needsConnect: true, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + switchChain: mockSwitchChain, + }) + + renderWithChakra( + createElement( + WalletStatusVerifier, + { fallback: createElement('div', { 'data-testid': 'custom-fallback' }, 'Custom') }, + createElement('div', { 'data-testid': 'protected-content' }, 'Protected'), + ), ) - expect(screen.getByRole('button').textContent?.toLowerCase()).toContain('switch to') - expect(screen.queryByText('Protected Content')).toBeNull() + + expect(screen.getByTestId('custom-fallback')).toBeInTheDocument() + expect(screen.queryByTestId('protected-content')).toBeNull() }) - it('renders children when wallet is connected and synced', () => { - vi.mocked(useWeb3StatusModule.useWeb3Status).mockReturnValue( - // biome-ignore lint/suspicious/noExplicitAny: partial mock - connectedSyncedStatus() as any, - ) - wrap( - -
Protected Content
-
, + it('renders switch chain button when wallet needs chain switch', () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: false, + needsConnect: false, + needsChainSwitch: true, + targetChain: { id: 10, name: 'OP Mainnet' } as ReturnType< + typeof useWalletStatus + >['targetChain'], + switchChain: mockSwitchChain, + }) + + renderWithChakra( + createElement( + WalletStatusVerifier, + null, + createElement('div', { 'data-testid': 'protected-content' }, 'Protected'), + ), ) - expect(screen.getByText('Protected Content')).toBeDefined() + + expect(screen.getByText(/Switch to/)).toBeInTheDocument() + expect(screen.getByText(/OP Mainnet/)).toBeInTheDocument() + expect(screen.queryByTestId('protected-content')).toBeNull() }) -}) -describe('withWalletStatusVerifier HOC', () => { - const ProtectedComponent = () =>
Protected Component
- const Wrapped = withWalletStatusVerifier(ProtectedComponent) + it('renders children when wallet is ready', () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: true, + needsConnect: false, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + switchChain: mockSwitchChain, + }) - it('renders fallback when wallet not connected', () => { - vi.mocked(useWeb3StatusModule.useWeb3Status).mockReturnValue( - // biome-ignore lint/suspicious/noExplicitAny: partial mock - connectedSyncedStatus({ isWalletConnected: false, isWalletSynced: false }) as any, + renderWithChakra( + createElement( + WalletStatusVerifier, + null, + createElement('div', { 'data-testid': 'protected-content' }, 'Protected'), + ), ) - wrap() - expect(screen.getByText('Connect Wallet')).toBeDefined() - expect(screen.queryByText('Protected Component')).toBeNull() + + expect(screen.getByTestId('protected-content')).toBeInTheDocument() }) - it('renders wrapped component when wallet is connected and synced', () => { - vi.mocked(useWeb3StatusModule.useWeb3Status).mockReturnValue( - // biome-ignore lint/suspicious/noExplicitAny: partial mock - connectedSyncedStatus() as any, + it('calls switchChain when switch button is clicked', async () => { + const user = userEvent.setup() + + mockedUseWalletStatus.mockReturnValue({ + isReady: false, + needsConnect: false, + needsChainSwitch: true, + targetChain: { id: 10, name: 'OP Mainnet' } as ReturnType< + typeof useWalletStatus + >['targetChain'], + switchChain: mockSwitchChain, + }) + + renderWithChakra( + createElement(WalletStatusVerifier, null, createElement('div', null, 'Protected')), ) - wrap() - expect(screen.getByText('Protected Component')).toBeDefined() + + const switchButton = screen.getByText(/Switch to/) + await user.click(switchButton) + + expect(mockSwitchChain).toHaveBeenCalledWith(10) }) }) diff --git a/src/components/sharedComponents/WalletStatusVerifier.tsx b/src/components/sharedComponents/WalletStatusVerifier.tsx index 7e9d46c2..c0e29756 100644 --- a/src/components/sharedComponents/WalletStatusVerifier.tsx +++ b/src/components/sharedComponents/WalletStatusVerifier.tsx @@ -1,20 +1,8 @@ -import PrimaryButton from '@/src/components/sharedComponents/ui/PrimaryButton' -import { useWeb3Status } from '@/src/hooks/useWeb3Status' -import { type ChainsIds, chains } from '@/src/lib/networks.config' +import SwitchChainButton from '@/src/components/sharedComponents/ui/SwitchChainButton' +import { useWalletStatus } from '@/src/hooks/useWalletStatus' +import type { ChainsIds } from '@/src/lib/networks.config' import { ConnectWalletButton } from '@/src/providers/Web3Provider' -import { chakra } from '@chakra-ui/react' -import type { ComponentType, FC, ReactElement } from 'react' -import { extractChain } from 'viem' - -const Button = chakra(PrimaryButton, { - base: { - fontSize: '16px', - fontWeight: 500, - height: '48px', - paddingLeft: 6, - paddingRight: 6, - }, -}) +import type { FC, ReactElement } from 'react' interface WalletStatusVerifierProps { chainId?: ChainsIds @@ -24,21 +12,14 @@ interface WalletStatusVerifierProps { } /** - * WalletStatusVerifier Component - * - * This component checks the wallet connection and chain synchronization status. - * If the wallet is not connected, it displays a fallback component (default: ConnectWalletButton) - * If the wallet is connected but not synced with the correct chain, it provides an option to switch chain. + * Wrapper component that gates content on wallet connection and chain status. * - * @param {Object} props - WalletStatusVerifier component props - * @param {Chain['id']} [props.chainId] - The chain ID to check for synchronization - * @param {ReactElement} [props.fallback] - The fallback component to render if the wallet is not connected - * @param {ReactElement} props.children - The children components to render if the wallet is connected and synced + * This is the primary API for protecting UI that requires a connected wallet. * * @example * ```tsx * - * + * * * ``` */ @@ -48,75 +29,21 @@ const WalletStatusVerifier: FC = ({ fallback = , labelSwitchChain = 'Switch to', }: WalletStatusVerifierProps) => { - const { appChainId, isWalletConnected, isWalletSynced, switchChain, walletChainId } = - useWeb3Status() + const { needsConnect, needsChainSwitch, targetChain, switchChain } = useWalletStatus({ chainId }) - const chainToSwitch = extractChain({ chains, id: chainId || appChainId || chains[0].id }) - - if (!isWalletConnected) { + if (needsConnect) { return fallback } - if (!isWalletSynced || walletChainId !== chainToSwitch.id) { + if (needsChainSwitch) { return ( - + switchChain(targetChain.id as ChainsIds)}> + {labelSwitchChain} {targetChain.name} + ) } return children } -/** - * WalletStatusVerifier Component - * - * Checks the wallet connection and chain synchronization status. - * - If wallet is not connected, displays fallback component (default: ConnectWalletButton) - * - If wallet is connected but on wrong chain, provides option to switch networks - * - If wallet is connected and on correct chain, renders children - * - * @param {WalletStatusVerifierProps} props - Component props - * @param {ChainsIds} [props.chainId] - The required chain ID (defaults to appChainId) - * @param {ReactElement} [props.children] - The content to render when wallet is connected and synced - * @param {ReactElement} [props.fallback=] - Component to render when wallet is not connected - * @param {string} [props.labelSwitchChain='Switch to'] - Label for the chain switching button - * - * @example - * ```tsx - * - * - * - * ``` - */ -const withWalletStatusVerifier =

( - WrappedComponent: ComponentType

, - { - chainId, - fallback = , - labelSwitchChain = 'Switch to', - }: WalletStatusVerifierProps = {}, -): FC

=> { - const ComponentWithVerifier: FC

= (props: P) => { - const { appChainId, isWalletConnected, isWalletSynced, switchChain, walletChainId } = - useWeb3Status() - - const chainToSwitch = extractChain({ chains, id: chainId || appChainId || chains[0].id }) - - return !isWalletConnected ? ( - fallback - ) : !isWalletSynced || walletChainId !== chainToSwitch.id ? ( - - ) : ( - - ) - } - - ComponentWithVerifier.displayName = `withWalletStatusVerifier(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})` - - return ComponentWithVerifier -} - -export { WalletStatusVerifier, withWalletStatusVerifier } +export { WalletStatusVerifier } diff --git a/src/components/sharedComponents/ui/SwitchChainButton.tsx b/src/components/sharedComponents/ui/SwitchChainButton.tsx new file mode 100644 index 00000000..02fc0d1d --- /dev/null +++ b/src/components/sharedComponents/ui/SwitchChainButton.tsx @@ -0,0 +1,14 @@ +import PrimaryButton from '@/src/components/sharedComponents/ui/PrimaryButton' +import { chakra } from '@chakra-ui/react' + +const SwitchChainButton = chakra(PrimaryButton, { + base: { + fontSize: '16px', + fontWeight: 500, + height: '48px', + paddingLeft: 6, + paddingRight: 6, + }, +}) + +export default SwitchChainButton From 91357b87d3c793a54d6fc8cedcc93291f1a54796 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 30 Mar 2026 13:38:22 +0200 Subject: [PATCH 03/11] refactor: TransactionButton internalizes wallet verification TransactionButton now uses useWalletStatus hook directly instead of being wrapped with withWalletStatusVerifier HoC. Adds optional chainId, fallback, and switchChainLabel props. Closes part of #303 --- .../TransactionButton.test.tsx | 220 +++++++++--------- .../sharedComponents/TransactionButton.tsx | 119 ++++++---- 2 files changed, 183 insertions(+), 156 deletions(-) diff --git a/src/components/sharedComponents/TransactionButton.test.tsx b/src/components/sharedComponents/TransactionButton.test.tsx index 74e4686d..474fc0fa 100644 --- a/src/components/sharedComponents/TransactionButton.test.tsx +++ b/src/components/sharedComponents/TransactionButton.test.tsx @@ -1,146 +1,148 @@ import { ChakraProvider, createSystem, defaultConfig } from '@chakra-ui/react' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { describe, expect, it, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import { type ReactNode, createElement } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' import TransactionButton from './TransactionButton' -const system = createSystem(defaultConfig) +const mockSwitchChain = vi.fn() +const mockWatchTx = vi.fn() +const mockTransaction = vi.fn(() => Promise.resolve('0xabc' as `0x${string}`)) + +vi.mock('@/src/hooks/useWalletStatus', () => ({ + useWalletStatus: vi.fn(() => ({ + isReady: false, + needsConnect: true, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' }, + switchChain: mockSwitchChain, + })), +})) -vi.mock('@/src/hooks/useWeb3Status', () => ({ - useWeb3Status: vi.fn(), +vi.mock('@/src/providers/Web3Provider', () => ({ + ConnectWalletButton: () => + createElement( + 'button', + { type: 'button', 'data-testid': 'connect-wallet-button' }, + 'Connect Wallet', + ), })) vi.mock('@/src/providers/TransactionNotificationProvider', () => ({ useTransactionNotification: vi.fn(() => ({ - watchTx: vi.fn(), - watchHash: vi.fn(), - watchSignature: vi.fn(), + watchTx: mockWatchTx, })), })) vi.mock('wagmi', () => ({ - useWaitForTransactionReceipt: vi.fn(() => ({ data: undefined })), + useWaitForTransactionReceipt: vi.fn(() => ({ + data: undefined, + })), })) -vi.mock('@/src/providers/Web3Provider', () => ({ - ConnectWalletButton: () => , -})) +const { useWalletStatus } = await import('@/src/hooks/useWalletStatus') +const mockedUseWalletStatus = vi.mocked(useWalletStatus) -import * as useWeb3StatusModule from '@/src/hooks/useWeb3Status' -import * as wagmiModule from 'wagmi' - -// chains[0] = optimismSepolia (id: 11155420) when PUBLIC_INCLUDE_TESTNETS=true (default) -const OP_SEPOLIA_ID = 11155420 as const - -function connectedStatus() { - return { - isWalletConnected: true, - isWalletSynced: true, - walletChainId: OP_SEPOLIA_ID, - appChainId: OP_SEPOLIA_ID, - address: '0x1234567890abcdef1234567890abcdef12345678' as `0x${string}`, - balance: undefined, - connectingWallet: false, - switchingChain: false, - walletClient: undefined, - readOnlyClient: undefined, - switchChain: vi.fn(), - disconnect: vi.fn(), - } -} - -// biome-ignore lint/suspicious/noExplicitAny: test helper accepts flexible props -function renderButton(props: any = {}) { - return render( - - Promise.resolve('0x1' as `0x${string}`)} - {...props} - /> - , - ) -} +const system = createSystem(defaultConfig) + +const renderWithChakra = (ui: ReactNode) => + render({ui}) describe('TransactionButton', () => { - it('renders fallback when wallet not connected', () => { - vi.mocked(useWeb3StatusModule.useWeb3Status).mockReturnValue({ - ...connectedStatus(), - isWalletConnected: false, - isWalletSynced: false, - }) - renderButton() - expect(screen.getByText('Connect Wallet')).toBeDefined() + beforeEach(() => { + vi.clearAllMocks() }) - it('renders switch chain button when wallet is on wrong chain', () => { - vi.mocked(useWeb3StatusModule.useWeb3Status).mockReturnValue({ - ...connectedStatus(), - isWalletSynced: false, - walletChainId: 1, + it('renders connect button when wallet needs connect', () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: false, + needsConnect: true, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + switchChain: mockSwitchChain, }) - renderButton() - expect(screen.getByRole('button').textContent?.toLowerCase()).toContain('switch to') - }) - it('renders with default label when wallet is connected and synced', () => { - vi.mocked(useWeb3StatusModule.useWeb3Status).mockReturnValue(connectedStatus()) - vi.mocked(wagmiModule.useWaitForTransactionReceipt).mockReturnValue({ - data: undefined, - } as ReturnType) - renderButton() - expect(screen.getByText('Send Transaction')).toBeDefined() - }) + renderWithChakra(Send) - it('renders with custom children label', () => { - vi.mocked(useWeb3StatusModule.useWeb3Status).mockReturnValue(connectedStatus()) - vi.mocked(wagmiModule.useWaitForTransactionReceipt).mockReturnValue({ - data: undefined, - } as ReturnType) - renderButton({ children: 'Deposit ETH' }) - expect(screen.getByText('Deposit ETH')).toBeDefined() + expect(screen.getByTestId('connect-wallet-button')).toBeInTheDocument() + expect(screen.queryByText('Send')).toBeNull() }) - it('shows labelSending while transaction is pending', async () => { - vi.mocked(useWeb3StatusModule.useWeb3Status).mockReturnValue(connectedStatus()) - vi.mocked(wagmiModule.useWaitForTransactionReceipt).mockReturnValue({ - data: undefined, - } as ReturnType) - - const neverResolves = () => new Promise<`0x${string}`>(() => {}) - renderButton({ transaction: neverResolves, labelSending: 'Processing...' }) + it('renders custom fallback when provided and wallet needs connect', () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: false, + needsConnect: true, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + switchChain: mockSwitchChain, + }) - expect(screen.getByRole('button').textContent).not.toContain('Processing...') + renderWithChakra( + + Send + , + ) - fireEvent.click(screen.getByRole('button')) + expect(screen.getByTestId('custom-fallback')).toBeInTheDocument() + expect(screen.queryByText('Send')).toBeNull() + }) - await waitFor(() => { - expect(screen.getByRole('button').textContent).toContain('Processing...') + it('renders switch chain button when wallet needs chain switch', () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: false, + needsConnect: false, + needsChainSwitch: true, + targetChain: { id: 10, name: 'OP Mainnet' } as ReturnType< + typeof useWalletStatus + >['targetChain'], + switchChain: mockSwitchChain, }) + + renderWithChakra(Send) + + expect(screen.getByText(/Switch to/)).toBeInTheDocument() + expect(screen.getByText(/OP Mainnet/)).toBeInTheDocument() + expect(screen.queryByText('Send')).toBeNull() }) - it('calls onMined when receipt becomes available', async () => { - // biome-ignore lint/suspicious/noExplicitAny: mock receipt shape - const mockReceipt = { status: 'success', transactionHash: '0x1' } as any - const onMined = vi.fn() - - vi.mocked(useWeb3StatusModule.useWeb3Status).mockReturnValue(connectedStatus()) - // Only return a receipt when called with the matching hash so the mock - // doesn't fire prematurely before the transaction is submitted. - vi.mocked(wagmiModule.useWaitForTransactionReceipt).mockImplementation( - (config) => - ({ - data: config?.hash === '0x1' ? mockReceipt : undefined, - }) as ReturnType, + it('renders custom switch chain label when provided', () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: false, + needsConnect: false, + needsChainSwitch: true, + targetChain: { id: 10, name: 'OP Mainnet' } as ReturnType< + typeof useWalletStatus + >['targetChain'], + switchChain: mockSwitchChain, + }) + + renderWithChakra( + + Send + , ) - renderButton({ - transaction: () => Promise.resolve('0x1' as `0x${string}`), - onMined, + expect(screen.getByText(/Change to/)).toBeInTheDocument() + expect(screen.getByText(/OP Mainnet/)).toBeInTheDocument() + }) + + it('renders transaction button when wallet is ready', () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: true, + needsConnect: false, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + switchChain: mockSwitchChain, }) - fireEvent.click(screen.getByRole('button')) + renderWithChakra(Send ETH) - await waitFor(() => { - expect(onMined).toHaveBeenCalledWith(mockReceipt) - }) + expect(screen.getByText('Send ETH')).toBeInTheDocument() + expect(screen.queryByTestId('connect-wallet-button')).toBeNull() }) }) diff --git a/src/components/sharedComponents/TransactionButton.tsx b/src/components/sharedComponents/TransactionButton.tsx index 8710b8a3..a3e9bb28 100644 --- a/src/components/sharedComponents/TransactionButton.tsx +++ b/src/components/sharedComponents/TransactionButton.tsx @@ -1,15 +1,22 @@ -import { withWalletStatusVerifier } from '@/src/components/sharedComponents/WalletStatusVerifier' import PrimaryButton from '@/src/components/sharedComponents/ui/PrimaryButton' +import SwitchChainButton from '@/src/components/sharedComponents/ui/SwitchChainButton' +import { useWalletStatus } from '@/src/hooks/useWalletStatus' +import type { ChainsIds } from '@/src/lib/networks.config' import { useTransactionNotification } from '@/src/providers/TransactionNotificationProvider' +import { ConnectWalletButton } from '@/src/providers/Web3Provider' import type { ButtonProps } from '@chakra-ui/react' import { useEffect, useState } from 'react' +import type { ReactElement } from 'react' import type { Hash, TransactionReceipt } from 'viem' import { useWaitForTransactionReceipt } from 'wagmi' interface TransactionButtonProps extends ButtonProps { + chainId?: ChainsIds confirmations?: number + fallback?: ReactElement labelSending?: string onMined?: (receipt: TransactionReceipt) => void + switchChainLabel?: string transaction: { (): Promise methodId?: string @@ -30,6 +37,9 @@ interface TransactionButtonProps extends ButtonProps { * @param {string} [props.labelSending='Sending...'] - Button label during pending transaction. * @param {number} [props.confirmations=1] - Number of confirmations to wait for. * @param {ReactNode} [props.children='Send Transaction'] - Button content. + * @param {ChainsIds} [props.chainId] - Target chain ID for wallet status verification. + * @param {ReactElement} [props.fallback] - Custom fallback when wallet needs connection. + * @param {string} [props.switchChainLabel='Switch to'] - Label for the switch chain button. * @param {ButtonProps} props.restProps - Additional props inherited from Chakra UI ButtonProps. * * @example @@ -44,60 +54,75 @@ interface TransactionButtonProps extends ButtonProps { * * ``` */ -const TransactionButton = withWalletStatusVerifier( - ({ - children = 'Send Transaction', - confirmations = 1, - disabled, - labelSending = 'Sending...', - onMined, - transaction, - ...restProps - }) => { - const [hash, setHash] = useState() - const [isPending, setIsPending] = useState(false) +function TransactionButton({ + chainId, + children = 'Send Transaction', + confirmations = 1, + disabled, + fallback = , + labelSending = 'Sending...', + onMined, + switchChainLabel = 'Switch to', + transaction, + ...restProps +}: TransactionButtonProps) { + const { needsConnect, needsChainSwitch, targetChain, switchChain } = useWalletStatus({ chainId }) - const { watchTx } = useTransactionNotification() - const { data: receipt } = useWaitForTransactionReceipt({ - hash: hash, - confirmations, - }) + const [hash, setHash] = useState() + const [isPending, setIsPending] = useState(false) - useEffect(() => { - const handleMined = async () => { - if (receipt && isPending) { - await onMined?.(receipt) - setIsPending(false) - setHash(undefined) - } - } - - handleMined() - }, [isPending, onMined, receipt]) + const { watchTx } = useTransactionNotification() + const { data: receipt } = useWaitForTransactionReceipt({ + hash: hash, + confirmations, + }) - const handleSendTransaction = async () => { - setIsPending(true) - try { - const txPromise = transaction() - watchTx({ txPromise, methodId: transaction.methodId }) - const hash = await txPromise - setHash(hash) - } catch (error: unknown) { - console.error('Error sending transaction', error instanceof Error ? error.message : error) + useEffect(() => { + const handleMined = async () => { + if (receipt && isPending) { + await onMined?.(receipt) setIsPending(false) + setHash(undefined) } } + handleMined() + }, [isPending, onMined, receipt]) + + if (needsConnect) { + return fallback + } + + if (needsChainSwitch) { return ( - - {isPending ? labelSending : children} - + switchChain(targetChain.id as ChainsIds)}> + {switchChainLabel} {targetChain.name} + ) - }, -) + } + + const handleSendTransaction = async () => { + setIsPending(true) + try { + const txPromise = transaction() + watchTx({ txPromise, methodId: transaction.methodId }) + const hash = await txPromise + setHash(hash) + } catch (error: unknown) { + console.error('Error sending transaction', error instanceof Error ? error.message : error) + setIsPending(false) + } + } + + return ( + + {isPending ? labelSending : children} + + ) +} export default TransactionButton From ba1d48253c1aff1a7d5c4391a98495f1c8ee20fc Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 30 Mar 2026 13:38:28 +0200 Subject: [PATCH 04/11] refactor: SignButton internalizes wallet verification SignButton now uses useWalletStatus hook directly instead of withWalletStatusVerifier HoC. Adds optional chainId, fallback, and switchChainLabel props. Closes part of #303 --- .../sharedComponents/SignButton.test.tsx | 130 ++++++++++++++++++ .../sharedComponents/SignButton.tsx | 95 ++++++++----- 2 files changed, 189 insertions(+), 36 deletions(-) create mode 100644 src/components/sharedComponents/SignButton.test.tsx diff --git a/src/components/sharedComponents/SignButton.test.tsx b/src/components/sharedComponents/SignButton.test.tsx new file mode 100644 index 00000000..0b826f52 --- /dev/null +++ b/src/components/sharedComponents/SignButton.test.tsx @@ -0,0 +1,130 @@ +import { ChakraProvider, createSystem, defaultConfig } from '@chakra-ui/react' +import { render, screen } from '@testing-library/react' +import type { ReactNode } from 'react' +import { createElement } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mockSwitchChain = vi.fn() +const mockSignMessageAsync = vi.fn() +const mockWatchSignature = vi.fn() + +vi.mock('@/src/hooks/useWalletStatus', () => ({ + useWalletStatus: vi.fn(() => ({ + isReady: false, + needsConnect: true, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' }, + switchChain: mockSwitchChain, + })), +})) + +vi.mock('@/src/providers/Web3Provider', () => ({ + ConnectWalletButton: () => + createElement( + 'button', + { type: 'button', 'data-testid': 'connect-wallet-button' }, + 'Connect Wallet', + ), +})) + +vi.mock('@/src/providers/TransactionNotificationProvider', () => ({ + useTransactionNotification: vi.fn(() => ({ + watchSignature: mockWatchSignature, + })), +})) + +vi.mock('wagmi', () => ({ + useSignMessage: vi.fn(() => ({ + isPending: false, + signMessageAsync: mockSignMessageAsync, + })), +})) + +const { useWalletStatus } = await import('@/src/hooks/useWalletStatus') +const mockedUseWalletStatus = vi.mocked(useWalletStatus) + +const system = createSystem(defaultConfig) + +const renderWithChakra = (ui: ReactNode) => + render({ui}) + +describe('SignButton', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders connect button when wallet needs connect', async () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: false, + needsConnect: true, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + switchChain: mockSwitchChain, + }) + + const { default: SignButton } = await import('./SignButton') + + renderWithChakra() + + expect(screen.getByTestId('connect-wallet-button')).toBeInTheDocument() + expect(screen.queryByText('Sign Message')).toBeNull() + }) + + it('renders custom fallback when provided and wallet needs connect', async () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: false, + needsConnect: true, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + switchChain: mockSwitchChain, + }) + + const { default: SignButton } = await import('./SignButton') + + renderWithChakra( + , + ) + + expect(screen.getByTestId('custom-fallback')).toBeInTheDocument() + expect(screen.queryByText('Sign Message')).toBeNull() + }) + + it('renders switch chain button when wallet needs chain switch', async () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: false, + needsConnect: false, + needsChainSwitch: true, + targetChain: { id: 10, name: 'OP Mainnet' } as ReturnType< + typeof useWalletStatus + >['targetChain'], + switchChain: mockSwitchChain, + }) + + const { default: SignButton } = await import('./SignButton') + + renderWithChakra() + + expect(screen.getByText(/Switch to/)).toBeInTheDocument() + expect(screen.getByText(/OP Mainnet/)).toBeInTheDocument() + expect(screen.queryByText('Sign Message')).toBeNull() + }) + + it('renders sign button when wallet is ready', async () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: true, + needsConnect: false, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + switchChain: mockSwitchChain, + }) + + const { default: SignButton } = await import('./SignButton') + + renderWithChakra() + + expect(screen.getByText('Sign Message')).toBeInTheDocument() + }) +}) diff --git a/src/components/sharedComponents/SignButton.tsx b/src/components/sharedComponents/SignButton.tsx index d548ab4a..b27eefaf 100644 --- a/src/components/sharedComponents/SignButton.tsx +++ b/src/components/sharedComponents/SignButton.tsx @@ -1,15 +1,21 @@ -import { withWalletStatusVerifier } from '@/src/components/sharedComponents/WalletStatusVerifier' +import SwitchChainButton from '@/src/components/sharedComponents/ui/SwitchChainButton' +import { useWalletStatus } from '@/src/hooks/useWalletStatus' +import type { ChainsIds } from '@/src/lib/networks.config' import { useTransactionNotification } from '@/src/providers/TransactionNotificationProvider' +import { ConnectWalletButton } from '@/src/providers/Web3Provider' import { type ButtonProps, chakra } from '@chakra-ui/react' -import type { FC } from 'react' +import type { FC, ReactElement } from 'react' import { useSignMessage } from 'wagmi' interface SignButtonProps extends Omit { + chainId?: ChainsIds + fallback?: ReactElement label?: string labelSigning?: string message: string onError?: (error: Error) => void onSign?: (signature: string) => void + switchChainLabel?: string } /** @@ -23,6 +29,9 @@ interface SignButtonProps extends Omit { * @param {(error: Error) => void} [props.onError] - Callback function called when an error occurs. * @param {string} [props.label='Sign Message'] - The label for the button (alternative to children). * @param {string} [props.labelSigning='Signing...'] - The label for the button when the message is being signed. + * @param {ChainsIds} [props.chainId] - Target chain ID for wallet status verification. + * @param {ReactElement} [props.fallback] - Custom fallback when wallet needs connect. + * @param {string} [props.switchChainLabel='Switch to'] - Label for the switch chain button. * @param {ButtonProps} [props.restProps] - Additional props inherited from Chakra UI ButtonProps. * * @example @@ -34,44 +43,58 @@ interface SignButtonProps extends Omit { * /> * ``` */ -const SignButton: FC = withWalletStatusVerifier( - ({ - children = 'Sign Message', - disabled, - labelSigning = 'Signing...', - message, - onError, - onSign, - ...restProps - }: SignButtonProps) => { - const { watchSignature } = useTransactionNotification() +const SignButton: FC = ({ + chainId, + children = 'Sign Message', + disabled, + fallback = , + labelSigning = 'Signing...', + message, + onError, + onSign, + switchChainLabel = 'Switch to', + ...restProps +}) => { + const { needsConnect, needsChainSwitch, targetChain, switchChain } = useWalletStatus({ chainId }) + const { watchSignature } = useTransactionNotification() - const { isPending, signMessageAsync } = useSignMessage({ - mutation: { - onSuccess(data) { - onSign?.(data) - }, - onError(error) { - onError?.(error) - }, + const { isPending, signMessageAsync } = useSignMessage({ + mutation: { + onSuccess(data) { + onSign?.(data) }, - }) + onError(error) { + onError?.(error) + }, + }, + }) + + if (needsConnect) { + return fallback + } + if (needsChainSwitch) { return ( - { - watchSignature({ - message: 'Signing message...', - signaturePromise: signMessageAsync({ message }), - }) - }} - {...restProps} - > - {isPending ? labelSigning : children} - + switchChain(targetChain.id as ChainsIds)}> + {switchChainLabel} {targetChain.name} + ) - }, -) + } + + return ( + { + watchSignature({ + message: 'Signing message...', + signaturePromise: signMessageAsync({ message }), + }) + }} + {...restProps} + > + {isPending ? labelSigning : children} + + ) +} export default SignButton From 544e17f87b4d5c0abe5ced7ab7456253e831ddd0 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 30 Mar 2026 13:38:36 +0200 Subject: [PATCH 05/11] refactor: demo components use WalletStatusVerifier wrapper instead of HoC NativeToken, ERC20ApproveAndTransferButton, and OptimismCrossDomainMessenger now use the declarative wrapper instead of withWalletStatusVerifier. Suspense HoCs (withSuspense, withSuspenseAndRetry) are preserved. Closes #303 --- .../OptimismCrossDomainMessenger/index.tsx | 43 ++++----- .../ERC20ApproveAndTransferButton/index.tsx | 95 +++++++++---------- .../demos/TransactionButton/NativeToken.tsx | 57 ++++++----- tsconfig.json | 2 +- 4 files changed, 95 insertions(+), 102 deletions(-) diff --git a/src/components/pageComponents/home/Examples/demos/OptimismCrossDomainMessenger/index.tsx b/src/components/pageComponents/home/Examples/demos/OptimismCrossDomainMessenger/index.tsx index 00b518bf..ee53d487 100644 --- a/src/components/pageComponents/home/Examples/demos/OptimismCrossDomainMessenger/index.tsx +++ b/src/components/pageComponents/home/Examples/demos/OptimismCrossDomainMessenger/index.tsx @@ -2,7 +2,7 @@ import Icon from '@/src/components/pageComponents/home/Examples/demos/OptimismCr import Wrapper from '@/src/components/pageComponents/home/Examples/wrapper' import Hash from '@/src/components/sharedComponents/Hash' import TransactionButton from '@/src/components/sharedComponents/TransactionButton' -import { withWalletStatusVerifier } from '@/src/components/sharedComponents/WalletStatusVerifier' +import { WalletStatusVerifier } from '@/src/components/sharedComponents/WalletStatusVerifier' import { getContract } from '@/src/constants/contracts/contracts' import { useL1CrossDomainMessengerProxy } from '@/src/hooks/useOPL1CrossDomainMessengerProxy' import { useWeb3StatusConnected } from '@/src/hooks/useWeb3Status' @@ -15,27 +15,27 @@ import { parseEther } from 'viem' import { optimismSepolia, sepolia } from 'viem/chains' import { extractTransactionDepositedLogs, getL2TransactionHash } from 'viem/op-stack' -const OptimismCrossDomainMessenger = withWalletStatusVerifier( - withSuspenseAndRetry(() => { - // https://sepolia-optimism.etherscan.io/address/0xb50201558b00496a145fe76f7424749556e326d8 - const AAVEProxy = '0xb50201558b00496a145fe76f7424749556e326d8' - const { address: walletAddress, readOnlyClient } = useWeb3StatusConnected() +const OptimismCrossDomainMessenger = withSuspenseAndRetry(() => { + // https://sepolia-optimism.etherscan.io/address/0xb50201558b00496a145fe76f7424749556e326d8 + const AAVEProxy = '0xb50201558b00496a145fe76f7424749556e326d8' + const { address: walletAddress, readOnlyClient } = useWeb3StatusConnected() - const contract = getContract('AAVEWeth', optimismSepolia.id) - const depositValue = parseEther('0.01') + const contract = getContract('AAVEWeth', optimismSepolia.id) + const depositValue = parseEther('0.01') - const [l2Hash, setL2Hash] = useState

(null) + const [l2Hash, setL2Hash] = useState
(null) - const sendCrossChainMessage = useL1CrossDomainMessengerProxy({ - fromChain: sepolia, - contractName: 'AAVEWeth', - functionName: 'depositETH', - l2ContractAddress: contract.address, - args: [AAVEProxy, walletAddress, 0], - value: depositValue, - }) + const sendCrossChainMessage = useL1CrossDomainMessengerProxy({ + fromChain: sepolia, + contractName: 'AAVEWeth', + functionName: 'depositETH', + l2ContractAddress: contract.address, + args: [AAVEProxy, walletAddress, 0], + value: depositValue, + }) - return ( + return ( +

Deposit 0.01 ETH in{' '} @@ -76,10 +76,9 @@ const OptimismCrossDomainMessenger = withWalletStatusVerifier( )} - ) - }), - { chainId: sepolia.id }, -) + + ) +}) const optimismCrossdomainMessenger = { demo: , diff --git a/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/index.tsx b/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/index.tsx index 66fec3ba..32ade80f 100644 --- a/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/index.tsx +++ b/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/index.tsx @@ -1,7 +1,7 @@ import BaseERC20ApproveAndTransferButton from '@/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/ERC20ApproveAndTransferButton' import MintUSDC from '@/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/MintUSDC' import Wrapper from '@/src/components/pageComponents/home/Examples/demos/TransactionButton/Wrapper' -import { withWalletStatusVerifier } from '@/src/components/sharedComponents/WalletStatusVerifier' +import { WalletStatusVerifier } from '@/src/components/sharedComponents/WalletStatusVerifier' import { useSuspenseReadErc20BalanceOf } from '@/src/hooks/generated' import { useWeb3StatusConnected } from '@/src/hooks/useWeb3Status' import type { Token } from '@/src/types/token' @@ -57,59 +57,56 @@ const ABIExample = [ * * Works only on Sepolia chain. */ -const ERC20ApproveAndTransferButton = withWalletStatusVerifier( - withSuspense(() => { - const { address } = useWeb3StatusConnected() - const { writeContractAsync } = useWriteContract() +const ERC20ApproveAndTransferButton = withSuspense(() => { + const { address } = useWeb3StatusConnected() + const { writeContractAsync } = useWriteContract() - const { data: balance, refetch: refetchBalance } = useSuspenseReadErc20BalanceOf({ - address: tokenUSDC_sepolia.address as Address, - args: [address], - }) + const { data: balance, refetch: refetchBalance } = useSuspenseReadErc20BalanceOf({ + address: tokenUSDC_sepolia.address as Address, + args: [address], + }) - // AAVE staging contract pool address - const spender = '0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951' + // AAVE staging contract pool address + const spender = '0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951' - const amount = 10000000000n // 10,000.00 USDC + const amount = 10000000000n // 10,000.00 USDC - const handleTransaction = () => - writeContractAsync({ - abi: ABIExample, - address: spender, - functionName: 'supply', - args: [tokenUSDC_sepolia.address as Address, amount, address, 0], - }) - handleTransaction.methodId = 'Supply USDC' + const handleTransaction = () => + writeContractAsync({ + abi: ABIExample, + address: spender, + functionName: 'supply', + args: [tokenUSDC_sepolia.address as Address, amount, address, 0], + }) + handleTransaction.methodId = 'Supply USDC' - const formattedAmount = formatNumberOrString( - formatUnits(amount, tokenUSDC_sepolia.decimals), - NumberType.TokenTx, - ) + const formattedAmount = formatNumberOrString( + formatUnits(amount, tokenUSDC_sepolia.decimals), + NumberType.TokenTx, + ) - return ( - <> - {balance < amount ? ( - - - - ) : ( - refetchBalance} - spender={spender} - token={tokenUSDC_sepolia} - transaction={handleTransaction} - /> - )} - - ) - }), - { chainId: sepolia.id }, // this DEMO component only works on sepolia chain -) + return ( + + {balance < amount ? ( + + + + ) : ( + refetchBalance} + spender={spender} + token={tokenUSDC_sepolia} + transaction={handleTransaction} + /> + )} + + ) +}) export default ERC20ApproveAndTransferButton diff --git a/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx b/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx index a1894296..13c32cdd 100644 --- a/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx +++ b/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx @@ -1,6 +1,6 @@ import Wrapper from '@/src/components/pageComponents/home/Examples/demos/TransactionButton/Wrapper' import TransactionButton from '@/src/components/sharedComponents/TransactionButton' -import { withWalletStatusVerifier } from '@/src/components/sharedComponents/WalletStatusVerifier' +import { WalletStatusVerifier } from '@/src/components/sharedComponents/WalletStatusVerifier' import { GeneralMessage } from '@/src/components/sharedComponents/ui/GeneralMessage' import PrimaryButton from '@/src/components/sharedComponents/ui/PrimaryButton' import { useWeb3StatusConnected } from '@/src/hooks/useWeb3Status' @@ -15,32 +15,32 @@ import { useSendTransaction } from 'wagmi' * * Works only on Sepolia chain. */ -const NativeToken = withWalletStatusVerifier( - () => { - const [isModalOpen, setIsModalOpen] = useState(false) - const { address } = useWeb3StatusConnected() - const { sendTransactionAsync } = useSendTransaction() - const [minedMessage, setMinedMessage] = useState() +const NativeToken = () => { + const [isModalOpen, setIsModalOpen] = useState(false) + const { address } = useWeb3StatusConnected() + const { sendTransactionAsync } = useSendTransaction() + const [minedMessage, setMinedMessage] = useState() - const handleOnMined = (receipt: TransactionReceipt) => { - setMinedMessage( - <> - Hash: {receipt.transactionHash} - , - ) - setIsModalOpen(true) - } + const handleOnMined = (receipt: TransactionReceipt) => { + setMinedMessage( + <> + Hash: {receipt.transactionHash} + , + ) + setIsModalOpen(true) + } - const handleSendTransaction = (): Promise => { - // Send native token - return sendTransactionAsync({ - to: address, - value: parseEther('0.1'), - }) - } - handleSendTransaction.methodId = 'sendTransaction' + const handleSendTransaction = (): Promise => { + // Send native token + return sendTransactionAsync({ + to: address, + value: parseEther('0.1'), + }) + } + handleSendTransaction.methodId = 'sendTransaction' - return ( + return ( + - ) - }, - { - chainId: sepolia.id, // this DEMO component only works on sepolia chain - }, -) + + ) +} export default NativeToken diff --git a/tsconfig.json b/tsconfig.json index e1c3e446..df3a50b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,7 @@ "skipLibCheck": true, "strict": true, "target": "ESNext", - "types": ["vitest/globals"], + "types": ["vitest/globals", "@testing-library/jest-dom/vitest"], "useDefineForClassFields": true, "paths": { "@/src/*": ["./src/*"], From c9f54aa475299b20066f4630189f2ef6669abf57 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 30 Mar 2026 14:03:34 +0200 Subject: [PATCH 06/11] chore: fix TypeDoc warnings for TransactionButton and SignButton Add @param, @returns, @example, @throws to TypeDoc blockTags config (they were being treated as unknown). Move param docs to interface properties where TypeDoc can resolve them for destructured params. Reduces TypeDoc warnings from 330 to 9 (remaining are in unrelated files). --- .../sharedComponents/SignButton.tsx | 24 ++++++++--------- .../sharedComponents/TransactionButton.tsx | 26 +++++++------------ typedoc.json | 2 +- 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/components/sharedComponents/SignButton.tsx b/src/components/sharedComponents/SignButton.tsx index b27eefaf..b75e7837 100644 --- a/src/components/sharedComponents/SignButton.tsx +++ b/src/components/sharedComponents/SignButton.tsx @@ -8,31 +8,29 @@ import type { FC, ReactElement } from 'react' import { useSignMessage } from 'wagmi' interface SignButtonProps extends Omit { + /** Target chain ID for wallet status verification. */ chainId?: ChainsIds + /** Custom fallback when wallet needs connection. Defaults to ConnectWalletButton. */ fallback?: ReactElement + /** Alternative label for the button. */ label?: string + /** Button label while signing. Defaults to 'Signing...'. */ labelSigning?: string + /** The message to sign. */ message: string + /** Callback function called when an error occurs. */ onError?: (error: Error) => void + /** Callback function called when the message is signed. */ onSign?: (signature: string) => void + /** Label for the switch chain button. Defaults to 'Switch to'. */ switchChainLabel?: string } /** - * SignButton component that allows users to sign a message. + * Self-contained message signing button with wallet verification. * - * @param {SignButtonProps} props - SignButton component props. - * @param {string} props.message - The message to sign. - * @param {string|ReactNode} [props.children='Sign Message'] - The content to display in the button. - * @param {boolean} [props.disabled] - Whether the button is disabled. - * @param {(signature: string) => void} [props.onSign] - Callback function called when the message is signed. - * @param {(error: Error) => void} [props.onError] - Callback function called when an error occurs. - * @param {string} [props.label='Sign Message'] - The label for the button (alternative to children). - * @param {string} [props.labelSigning='Signing...'] - The label for the button when the message is being signed. - * @param {ChainsIds} [props.chainId] - Target chain ID for wallet status verification. - * @param {ReactElement} [props.fallback] - Custom fallback when wallet needs connect. - * @param {string} [props.switchChainLabel='Switch to'] - Label for the switch chain button. - * @param {ButtonProps} [props.restProps] - Additional props inherited from Chakra UI ButtonProps. + * Handles wallet connection status internally — shows a connect button if not connected, + * a switch chain button if on the wrong chain, or the sign button when ready. * * @example * ```tsx diff --git a/src/components/sharedComponents/TransactionButton.tsx b/src/components/sharedComponents/TransactionButton.tsx index a3e9bb28..17af5fda 100644 --- a/src/components/sharedComponents/TransactionButton.tsx +++ b/src/components/sharedComponents/TransactionButton.tsx @@ -11,12 +11,19 @@ import type { Hash, TransactionReceipt } from 'viem' import { useWaitForTransactionReceipt } from 'wagmi' interface TransactionButtonProps extends ButtonProps { + /** Target chain ID for wallet status verification. */ chainId?: ChainsIds + /** Number of confirmations to wait for. Defaults to 1. */ confirmations?: number + /** Custom fallback when wallet needs connection. Defaults to ConnectWalletButton. */ fallback?: ReactElement + /** Button label during pending transaction. Defaults to 'Sending...'. */ labelSending?: string + /** Callback function called when transaction is mined. */ onMined?: (receipt: TransactionReceipt) => void + /** Label for the switch chain button. Defaults to 'Switch to'. */ switchChainLabel?: string + /** Function that initiates the transaction and returns a hash. */ transaction: { (): Promise methodId?: string @@ -24,23 +31,10 @@ interface TransactionButtonProps extends ButtonProps { } /** - * TransactionButton component that handles blockchain transaction submission and monitoring. + * Self-contained transaction button with wallet verification, submission, and confirmation tracking. * - * Integrates with writeContractSync or sendTransactionSync functions to handle transaction - * submission and wait for confirmation. Displays transaction status and calls the onMined - * callback when the transaction is confirmed. - * - * @param {TransactionButtonProps} props - TransactionButton component props. - * @param {() => Promise} props.transaction - Function that initiates the transaction. - * @param {(receipt: TransactionReceipt) => void} [props.onMined] - Callback function called when transaction is mined. - * @param {boolean} [props.disabled] - Whether the button is disabled. - * @param {string} [props.labelSending='Sending...'] - Button label during pending transaction. - * @param {number} [props.confirmations=1] - Number of confirmations to wait for. - * @param {ReactNode} [props.children='Send Transaction'] - Button content. - * @param {ChainsIds} [props.chainId] - Target chain ID for wallet status verification. - * @param {ReactElement} [props.fallback] - Custom fallback when wallet needs connection. - * @param {string} [props.switchChainLabel='Switch to'] - Label for the switch chain button. - * @param {ButtonProps} props.restProps - Additional props inherited from Chakra UI ButtonProps. + * Handles wallet connection status internally — shows a connect button if not connected, + * a switch chain button if on the wrong chain, or the transaction button when ready. * * @example * ```tsx diff --git a/typedoc.json b/typedoc.json index 41da282c..c2c7e15f 100644 --- a/typedoc.json +++ b/typedoc.json @@ -51,7 +51,7 @@ "visibilityFilters": { "inherited": true }, - "blockTags": ["@dev", "@source", "@name", "@description"], + "blockTags": ["@dev", "@source", "@name", "@description", "@param", "@returns", "@example", "@throws"], "validation": { "invalidLink": true, "notDocumented": false From 2bbdfd486df170054c397aeb0fd74376b944f50c Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 30 Mar 2026 15:10:12 +0200 Subject: [PATCH 07/11] fix: address Copilot review feedback - Add targetChainId to useWalletStatus return, removing unsafe `as ChainsIds` casts from all consumers - Fix onSuccess callback in ERC20ApproveAndTransferButton demo (was returning refetchBalance instead of passing it directly) --- .../ERC20ApproveAndTransferButton/index.tsx | 2 +- src/components/sharedComponents/SignButton.test.tsx | 5 +++++ src/components/sharedComponents/SignButton.tsx | 5 +++-- .../sharedComponents/TransactionButton.test.tsx | 6 ++++++ src/components/sharedComponents/TransactionButton.tsx | 5 +++-- .../sharedComponents/WalletStatusVerifier.test.tsx | 6 ++++++ .../sharedComponents/WalletStatusVerifier.tsx | 5 +++-- src/hooks/useWalletStatus.test.ts | 2 ++ src/hooks/useWalletStatus.ts | 11 +++++------ 9 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/index.tsx b/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/index.tsx index 32ade80f..58b231b8 100644 --- a/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/index.tsx +++ b/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/index.tsx @@ -99,7 +99,7 @@ const ERC20ApproveAndTransferButton = withSuspense(() => { amount={amount} label={`Supply ${formattedAmount} USDC`} labelSending="Sending..." - onSuccess={() => refetchBalance} + onSuccess={() => refetchBalance()} spender={spender} token={tokenUSDC_sepolia} transaction={handleTransaction} diff --git a/src/components/sharedComponents/SignButton.test.tsx b/src/components/sharedComponents/SignButton.test.tsx index 0b826f52..f4e07cae 100644 --- a/src/components/sharedComponents/SignButton.test.tsx +++ b/src/components/sharedComponents/SignButton.test.tsx @@ -14,6 +14,7 @@ vi.mock('@/src/hooks/useWalletStatus', () => ({ needsConnect: true, needsChainSwitch: false, targetChain: { id: 1, name: 'Ethereum' }, + targetChainId: 1, switchChain: mockSwitchChain, })), })) @@ -59,6 +60,7 @@ describe('SignButton', () => { needsConnect: true, needsChainSwitch: false, targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + targetChainId: 1, switchChain: mockSwitchChain, }) @@ -76,6 +78,7 @@ describe('SignButton', () => { needsConnect: true, needsChainSwitch: false, targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + targetChainId: 1, switchChain: mockSwitchChain, }) @@ -100,6 +103,7 @@ describe('SignButton', () => { targetChain: { id: 10, name: 'OP Mainnet' } as ReturnType< typeof useWalletStatus >['targetChain'], + targetChainId: 10, switchChain: mockSwitchChain, }) @@ -118,6 +122,7 @@ describe('SignButton', () => { needsConnect: false, needsChainSwitch: false, targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + targetChainId: 1, switchChain: mockSwitchChain, }) diff --git a/src/components/sharedComponents/SignButton.tsx b/src/components/sharedComponents/SignButton.tsx index b75e7837..5abcdd4c 100644 --- a/src/components/sharedComponents/SignButton.tsx +++ b/src/components/sharedComponents/SignButton.tsx @@ -53,7 +53,8 @@ const SignButton: FC = ({ switchChainLabel = 'Switch to', ...restProps }) => { - const { needsConnect, needsChainSwitch, targetChain, switchChain } = useWalletStatus({ chainId }) + const { needsConnect, needsChainSwitch, targetChain, targetChainId, switchChain } = + useWalletStatus({ chainId }) const { watchSignature } = useTransactionNotification() const { isPending, signMessageAsync } = useSignMessage({ @@ -73,7 +74,7 @@ const SignButton: FC = ({ if (needsChainSwitch) { return ( - switchChain(targetChain.id as ChainsIds)}> + switchChain(targetChainId)}> {switchChainLabel} {targetChain.name} ) diff --git a/src/components/sharedComponents/TransactionButton.test.tsx b/src/components/sharedComponents/TransactionButton.test.tsx index 474fc0fa..deb77f9d 100644 --- a/src/components/sharedComponents/TransactionButton.test.tsx +++ b/src/components/sharedComponents/TransactionButton.test.tsx @@ -14,6 +14,7 @@ vi.mock('@/src/hooks/useWalletStatus', () => ({ needsConnect: true, needsChainSwitch: false, targetChain: { id: 1, name: 'Ethereum' }, + targetChainId: 1, switchChain: mockSwitchChain, })), })) @@ -58,6 +59,7 @@ describe('TransactionButton', () => { needsConnect: true, needsChainSwitch: false, targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + targetChainId: 1, switchChain: mockSwitchChain, }) @@ -73,6 +75,7 @@ describe('TransactionButton', () => { needsConnect: true, needsChainSwitch: false, targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + targetChainId: 1, switchChain: mockSwitchChain, }) @@ -97,6 +100,7 @@ describe('TransactionButton', () => { targetChain: { id: 10, name: 'OP Mainnet' } as ReturnType< typeof useWalletStatus >['targetChain'], + targetChainId: 10, switchChain: mockSwitchChain, }) @@ -115,6 +119,7 @@ describe('TransactionButton', () => { targetChain: { id: 10, name: 'OP Mainnet' } as ReturnType< typeof useWalletStatus >['targetChain'], + targetChainId: 10, switchChain: mockSwitchChain, }) @@ -137,6 +142,7 @@ describe('TransactionButton', () => { needsConnect: false, needsChainSwitch: false, targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + targetChainId: 1, switchChain: mockSwitchChain, }) diff --git a/src/components/sharedComponents/TransactionButton.tsx b/src/components/sharedComponents/TransactionButton.tsx index 17af5fda..fd43ae32 100644 --- a/src/components/sharedComponents/TransactionButton.tsx +++ b/src/components/sharedComponents/TransactionButton.tsx @@ -60,7 +60,8 @@ function TransactionButton({ transaction, ...restProps }: TransactionButtonProps) { - const { needsConnect, needsChainSwitch, targetChain, switchChain } = useWalletStatus({ chainId }) + const { needsConnect, needsChainSwitch, targetChain, targetChainId, switchChain } = + useWalletStatus({ chainId }) const [hash, setHash] = useState() const [isPending, setIsPending] = useState(false) @@ -89,7 +90,7 @@ function TransactionButton({ if (needsChainSwitch) { return ( - switchChain(targetChain.id as ChainsIds)}> + switchChain(targetChainId)}> {switchChainLabel} {targetChain.name} ) diff --git a/src/components/sharedComponents/WalletStatusVerifier.test.tsx b/src/components/sharedComponents/WalletStatusVerifier.test.tsx index dd8d6acd..9f188e66 100644 --- a/src/components/sharedComponents/WalletStatusVerifier.test.tsx +++ b/src/components/sharedComponents/WalletStatusVerifier.test.tsx @@ -13,6 +13,7 @@ vi.mock('@/src/hooks/useWalletStatus', () => ({ needsConnect: true, needsChainSwitch: false, targetChain: { id: 1, name: 'Ethereum' }, + targetChainId: 1, switchChain: mockSwitchChain, })), })) @@ -45,6 +46,7 @@ describe('WalletStatusVerifier', () => { needsConnect: true, needsChainSwitch: false, targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + targetChainId: 1, switchChain: mockSwitchChain, }) @@ -66,6 +68,7 @@ describe('WalletStatusVerifier', () => { needsConnect: true, needsChainSwitch: false, targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + targetChainId: 1, switchChain: mockSwitchChain, }) @@ -89,6 +92,7 @@ describe('WalletStatusVerifier', () => { targetChain: { id: 10, name: 'OP Mainnet' } as ReturnType< typeof useWalletStatus >['targetChain'], + targetChainId: 10, switchChain: mockSwitchChain, }) @@ -111,6 +115,7 @@ describe('WalletStatusVerifier', () => { needsConnect: false, needsChainSwitch: false, targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + targetChainId: 1, switchChain: mockSwitchChain, }) @@ -135,6 +140,7 @@ describe('WalletStatusVerifier', () => { targetChain: { id: 10, name: 'OP Mainnet' } as ReturnType< typeof useWalletStatus >['targetChain'], + targetChainId: 10, switchChain: mockSwitchChain, }) diff --git a/src/components/sharedComponents/WalletStatusVerifier.tsx b/src/components/sharedComponents/WalletStatusVerifier.tsx index c0e29756..d5cfef50 100644 --- a/src/components/sharedComponents/WalletStatusVerifier.tsx +++ b/src/components/sharedComponents/WalletStatusVerifier.tsx @@ -29,7 +29,8 @@ const WalletStatusVerifier: FC = ({ fallback = , labelSwitchChain = 'Switch to', }: WalletStatusVerifierProps) => { - const { needsConnect, needsChainSwitch, targetChain, switchChain } = useWalletStatus({ chainId }) + const { needsConnect, needsChainSwitch, targetChain, targetChainId, switchChain } = + useWalletStatus({ chainId }) if (needsConnect) { return fallback @@ -37,7 +38,7 @@ const WalletStatusVerifier: FC = ({ if (needsChainSwitch) { return ( - switchChain(targetChain.id as ChainsIds)}> + switchChain(targetChainId)}> {labelSwitchChain} {targetChain.name} ) diff --git a/src/hooks/useWalletStatus.test.ts b/src/hooks/useWalletStatus.test.ts index 396ca1ec..ce5bd0fd 100644 --- a/src/hooks/useWalletStatus.test.ts +++ b/src/hooks/useWalletStatus.test.ts @@ -93,6 +93,7 @@ describe('useWalletStatus', () => { expect(result.current.needsChainSwitch).toBe(true) expect(result.current.isReady).toBe(false) expect(result.current.targetChain).toEqual({ id: 1, name: 'Ethereum' }) + expect(result.current.targetChainId).toBe(1) }) it('returns isReady when connected and on correct chain', () => { @@ -125,6 +126,7 @@ describe('useWalletStatus', () => { expect(result.current.needsChainSwitch).toBe(true) expect(result.current.isReady).toBe(false) expect(result.current.targetChain).toEqual({ id: 10, name: 'OP Mainnet' }) + expect(result.current.targetChainId).toBe(10) }) it('falls back to chains[0].id when no chainId or appChainId', () => { diff --git a/src/hooks/useWalletStatus.ts b/src/hooks/useWalletStatus.ts index f51c9ef4..3afae7cc 100644 --- a/src/hooks/useWalletStatus.ts +++ b/src/hooks/useWalletStatus.ts @@ -13,6 +13,7 @@ interface WalletStatus { needsConnect: boolean needsChainSwitch: boolean targetChain: Chain + targetChainId: ChainsIds switchChain: (chainId: ChainsIds) => void } @@ -20,14 +21,11 @@ export const useWalletStatus = (options?: UseWalletStatusOptions): WalletStatus const { appChainId, isWalletConnected, isWalletSynced, switchChain, walletChainId } = useWeb3Status() - const targetChain = extractChain({ - chains, - id: options?.chainId || appChainId || chains[0].id, - }) + const targetChainId = options?.chainId || appChainId || chains[0].id + const targetChain = extractChain({ chains, id: targetChainId }) const needsConnect = !isWalletConnected - const needsChainSwitch = - isWalletConnected && (!isWalletSynced || walletChainId !== targetChain.id) + const needsChainSwitch = isWalletConnected && (!isWalletSynced || walletChainId !== targetChainId) const isReady = isWalletConnected && !needsChainSwitch return { @@ -35,6 +33,7 @@ export const useWalletStatus = (options?: UseWalletStatusOptions): WalletStatus needsConnect, needsChainSwitch, targetChain, + targetChainId, switchChain, } } From b4607fc125acdcd0682beb7f8e72851cac1fe276 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 31 Mar 2026 18:10:58 +0200 Subject: [PATCH 08/11] fix: remove unused label prop from SignButtonProps --- src/components/sharedComponents/SignButton.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/sharedComponents/SignButton.tsx b/src/components/sharedComponents/SignButton.tsx index 5abcdd4c..84b09906 100644 --- a/src/components/sharedComponents/SignButton.tsx +++ b/src/components/sharedComponents/SignButton.tsx @@ -12,8 +12,6 @@ interface SignButtonProps extends Omit { chainId?: ChainsIds /** Custom fallback when wallet needs connection. Defaults to ConnectWalletButton. */ fallback?: ReactElement - /** Alternative label for the button. */ - label?: string /** Button label while signing. Defaults to 'Signing...'. */ labelSigning?: string /** The message to sign. */ From 6d08c287571aacde890960f269a75ac023cd3e72 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 31 Mar 2026 18:11:25 +0200 Subject: [PATCH 09/11] feat: enforce useWeb3StatusConnected via WalletStatusVerifier context WalletStatusVerifier now provides a React Context with the connected wallet data. useWeb3StatusConnected reads from that context and throws a DeveloperError if called outside the tree, so error boundaries show the message without a "Try Again" button. - Rename labelSwitchChain to switchChainLabel for consistency - useOPL1CrossDomainMessengerProxy accepts walletAddress parameter - Demo components wrap with WalletStatusVerifier from outside - Add DeveloperError class for non-retriable structural errors --- .../OptimismCrossDomainMessenger/index.tsx | 89 ++++++++++--------- .../ERC20ApproveAndTransferButton.tsx | 3 +- .../MintUSDC.tsx | 2 +- .../ERC20ApproveAndTransferButton/index.tsx | 41 ++++----- .../demos/TransactionButton/NativeToken.tsx | 74 ++++++++------- .../WalletStatusVerifier.test.tsx | 39 +++++++- .../sharedComponents/WalletStatusVerifier.tsx | 37 ++++++-- src/hooks/useOPL1CrossDomainMessengerProxy.ts | 6 +- src/hooks/useWeb3Status.test.ts | 48 ++++++++-- src/hooks/useWeb3Status.tsx | 9 -- src/utils/DeveloperError.ts | 7 ++ src/utils/suspenseWrapper.tsx | 12 ++- 12 files changed, 234 insertions(+), 133 deletions(-) create mode 100644 src/utils/DeveloperError.ts diff --git a/src/components/pageComponents/home/Examples/demos/OptimismCrossDomainMessenger/index.tsx b/src/components/pageComponents/home/Examples/demos/OptimismCrossDomainMessenger/index.tsx index ee53d487..8a92f454 100644 --- a/src/components/pageComponents/home/Examples/demos/OptimismCrossDomainMessenger/index.tsx +++ b/src/components/pageComponents/home/Examples/demos/OptimismCrossDomainMessenger/index.tsx @@ -3,9 +3,9 @@ import Wrapper from '@/src/components/pageComponents/home/Examples/wrapper' import Hash from '@/src/components/sharedComponents/Hash' import TransactionButton from '@/src/components/sharedComponents/TransactionButton' import { WalletStatusVerifier } from '@/src/components/sharedComponents/WalletStatusVerifier' +import { useWeb3StatusConnected } from '@/src/components/sharedComponents/WalletStatusVerifier' import { getContract } from '@/src/constants/contracts/contracts' import { useL1CrossDomainMessengerProxy } from '@/src/hooks/useOPL1CrossDomainMessengerProxy' -import { useWeb3StatusConnected } from '@/src/hooks/useWeb3Status' import { getExplorerLink } from '@/src/utils/getExplorerLink' import { withSuspenseAndRetry } from '@/src/utils/suspenseWrapper' import { Flex, Span } from '@chakra-ui/react' @@ -32,56 +32,59 @@ const OptimismCrossDomainMessenger = withSuspenseAndRetry(() => { l2ContractAddress: contract.address, args: [AAVEProxy, walletAddress, 0], value: depositValue, + walletAddress, }) return ( - - -

- Deposit 0.01 ETH in{' '} - - Optimism Sepolia AAVE market - {' '} - from Sepolia. -

- { - setL2Hash(null) - const hash = await sendCrossChainMessage() - const receipt = await readOnlyClient.waitForTransactionReceipt({ hash }) - const [log] = extractTransactionDepositedLogs(receipt) - const l2Hash = getL2TransactionHash({ log }) - setL2Hash(l2Hash) - return hash - }} + +

+ Deposit 0.01 ETH in{' '} + - Deposit ETH - - {l2Hash && ( - - OpSepolia tx - - - )} - - + Optimism Sepolia AAVE market + {' '} + from Sepolia. +

+ { + setL2Hash(null) + const hash = await sendCrossChainMessage() + const receipt = await readOnlyClient.waitForTransactionReceipt({ hash }) + const [log] = extractTransactionDepositedLogs(receipt) + const l2Hash = getL2TransactionHash({ log }) + setL2Hash(l2Hash) + return hash + }} + > + Deposit ETH + + {l2Hash && ( + + OpSepolia tx + + + )} +
) }) const optimismCrossdomainMessenger = { - demo: , + demo: ( + + + + ), href: 'https://bootnodedev.github.io/dAppBooster/functions/hooks_useOPL1CrossDomainMessengerProxy.useL1CrossDomainMessengerProxy.html', icon: , text: ( diff --git a/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/ERC20ApproveAndTransferButton.tsx b/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/ERC20ApproveAndTransferButton.tsx index 93db6525..9cb5fdcb 100644 --- a/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/ERC20ApproveAndTransferButton.tsx +++ b/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/ERC20ApproveAndTransferButton.tsx @@ -1,7 +1,8 @@ import Wrapper from '@/src/components/pageComponents/home/Examples/demos/TransactionButton/Wrapper' import TransactionButton from '@/src/components/sharedComponents/TransactionButton' +import { useWeb3StatusConnected } from '@/src/components/sharedComponents/WalletStatusVerifier' import { useSuspenseReadErc20Allowance } from '@/src/hooks/generated' -import { useWeb3Status, useWeb3StatusConnected } from '@/src/hooks/useWeb3Status' +import { useWeb3Status } from '@/src/hooks/useWeb3Status' import type { Token } from '@/src/types/token' import { getExplorerLink } from '@/src/utils/getExplorerLink' import type { FC } from 'react' diff --git a/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/MintUSDC.tsx b/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/MintUSDC.tsx index 7dbad8f9..c17124e2 100644 --- a/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/MintUSDC.tsx +++ b/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/MintUSDC.tsx @@ -1,7 +1,7 @@ import TransactionButton from '@/src/components/sharedComponents/TransactionButton' +import { useWeb3StatusConnected } from '@/src/components/sharedComponents/WalletStatusVerifier' import { AaveFaucetABI } from '@/src/constants/contracts/abis/AaveFaucet' import { getContract } from '@/src/constants/contracts/contracts' -import { useWeb3StatusConnected } from '@/src/hooks/useWeb3Status' import { sepolia } from 'viem/chains' import { useWriteContract } from 'wagmi' diff --git a/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/index.tsx b/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/index.tsx index 58b231b8..dad63df9 100644 --- a/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/index.tsx +++ b/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/index.tsx @@ -1,9 +1,8 @@ import BaseERC20ApproveAndTransferButton from '@/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/ERC20ApproveAndTransferButton' import MintUSDC from '@/src/components/pageComponents/home/Examples/demos/TransactionButton/ERC20ApproveAndTransferButton/MintUSDC' import Wrapper from '@/src/components/pageComponents/home/Examples/demos/TransactionButton/Wrapper' -import { WalletStatusVerifier } from '@/src/components/sharedComponents/WalletStatusVerifier' +import { useWeb3StatusConnected } from '@/src/components/sharedComponents/WalletStatusVerifier' import { useSuspenseReadErc20BalanceOf } from '@/src/hooks/generated' -import { useWeb3StatusConnected } from '@/src/hooks/useWeb3Status' import type { Token } from '@/src/types/token' import { NumberType, formatNumberOrString } from '@/src/utils/numberFormat' import { withSuspense } from '@/src/utils/suspenseWrapper' @@ -85,27 +84,23 @@ const ERC20ApproveAndTransferButton = withSuspense(() => { NumberType.TokenTx, ) - return ( - - {balance < amount ? ( - - - - ) : ( - refetchBalance()} - spender={spender} - token={tokenUSDC_sepolia} - transaction={handleTransaction} - /> - )} - + return balance < amount ? ( + + + + ) : ( + refetchBalance()} + spender={spender} + token={tokenUSDC_sepolia} + transaction={handleTransaction} + /> ) }) diff --git a/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx b/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx index 13c32cdd..99505f53 100644 --- a/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx +++ b/src/components/pageComponents/home/Examples/demos/TransactionButton/NativeToken.tsx @@ -1,13 +1,11 @@ import Wrapper from '@/src/components/pageComponents/home/Examples/demos/TransactionButton/Wrapper' import TransactionButton from '@/src/components/sharedComponents/TransactionButton' -import { WalletStatusVerifier } from '@/src/components/sharedComponents/WalletStatusVerifier' +import { useWeb3StatusConnected } from '@/src/components/sharedComponents/WalletStatusVerifier' import { GeneralMessage } from '@/src/components/sharedComponents/ui/GeneralMessage' import PrimaryButton from '@/src/components/sharedComponents/ui/PrimaryButton' -import { useWeb3StatusConnected } from '@/src/hooks/useWeb3Status' import { Dialog } from '@chakra-ui/react' import { type ReactElement, useState } from 'react' import { type Hash, type TransactionReceipt, parseEther } from 'viem' -import { sepolia } from 'viem/chains' import { useSendTransaction } from 'wagmi' /** @@ -40,44 +38,42 @@ const NativeToken = () => { handleSendTransaction.methodId = 'sendTransaction' return ( - - + - - - Send 0.1 Sepolia ETH - - - - - - { - setIsModalOpen(false) - setMinedMessage('') - }} - > - Close - - } - message={minedMessage} - title={'Transaction completed!'} - /> - - - - + Send 0.1 Sepolia ETH +
+
+ + + + { + setIsModalOpen(false) + setMinedMessage('') + }} + > + Close + + } + message={minedMessage} + title={'Transaction completed!'} + /> + + + ) } diff --git a/src/components/sharedComponents/WalletStatusVerifier.test.tsx b/src/components/sharedComponents/WalletStatusVerifier.test.tsx index 9f188e66..6633c3b0 100644 --- a/src/components/sharedComponents/WalletStatusVerifier.test.tsx +++ b/src/components/sharedComponents/WalletStatusVerifier.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { type ReactNode, createElement } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { WalletStatusVerifier } from './WalletStatusVerifier' +import { WalletStatusVerifier, useWeb3StatusConnected } from './WalletStatusVerifier' const mockSwitchChain = vi.fn() @@ -18,6 +18,23 @@ vi.mock('@/src/hooks/useWalletStatus', () => ({ })), })) +vi.mock('@/src/hooks/useWeb3Status', () => ({ + useWeb3Status: vi.fn(() => ({ + readOnlyClient: {}, + appChainId: 1, + address: '0xdeadbeef', + balance: undefined, + connectingWallet: false, + switchingChain: false, + isWalletConnected: true, + walletClient: undefined, + isWalletSynced: true, + walletChainId: 1, + switchChain: vi.fn(), + disconnect: vi.fn(), + })), +})) + vi.mock('@/src/providers/Web3Provider', () => ({ ConnectWalletButton: () => createElement( @@ -153,4 +170,24 @@ describe('WalletStatusVerifier', () => { expect(mockSwitchChain).toHaveBeenCalledWith(10) }) + + it('provides web3 status context to children when wallet is ready', () => { + mockedUseWalletStatus.mockReturnValue({ + isReady: true, + needsConnect: false, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + targetChainId: 1, + switchChain: mockSwitchChain, + }) + + const ChildComponent = () => { + const { address } = useWeb3StatusConnected() + return createElement('div', { 'data-testid': 'address' }, address) + } + + renderWithChakra(createElement(WalletStatusVerifier, null, createElement(ChildComponent))) + + expect(screen.getByTestId('address')).toHaveTextContent('0xdeadbeef') + }) }) diff --git a/src/components/sharedComponents/WalletStatusVerifier.tsx b/src/components/sharedComponents/WalletStatusVerifier.tsx index d5cfef50..c9902807 100644 --- a/src/components/sharedComponents/WalletStatusVerifier.tsx +++ b/src/components/sharedComponents/WalletStatusVerifier.tsx @@ -1,20 +1,42 @@ import SwitchChainButton from '@/src/components/sharedComponents/ui/SwitchChainButton' import { useWalletStatus } from '@/src/hooks/useWalletStatus' +import { type Web3Status, useWeb3Status } from '@/src/hooks/useWeb3Status' import type { ChainsIds } from '@/src/lib/networks.config' import { ConnectWalletButton } from '@/src/providers/Web3Provider' -import type { FC, ReactElement } from 'react' +import type { RequiredNonNull } from '@/src/types/utils' +import { DeveloperError } from '@/src/utils/DeveloperError' +import { type FC, type ReactElement, createContext, useContext } from 'react' + +const WalletStatusVerifierContext = createContext | null>(null) + +/** + * Returns the connected wallet's Web3 status. + * + * Must be called inside a `` component tree. + * Throws if called outside one. + */ +export const useWeb3StatusConnected = () => { + const context = useContext(WalletStatusVerifierContext) + if (context === null) { + throw new DeveloperError( + 'useWeb3StatusConnected must be used inside a component.', + ) + } + return context +} interface WalletStatusVerifierProps { chainId?: ChainsIds children?: ReactElement fallback?: ReactElement - labelSwitchChain?: string + switchChainLabel?: string } /** * Wrapper component that gates content on wallet connection and chain status. * * This is the primary API for protecting UI that requires a connected wallet. + * Components that call `useWeb3StatusConnected` must be rendered inside this component. * * @example * ```tsx @@ -27,10 +49,11 @@ const WalletStatusVerifier: FC = ({ chainId, children, fallback = , - labelSwitchChain = 'Switch to', + switchChainLabel = 'Switch to', }: WalletStatusVerifierProps) => { const { needsConnect, needsChainSwitch, targetChain, targetChainId, switchChain } = useWalletStatus({ chainId }) + const web3Status = useWeb3Status() if (needsConnect) { return fallback @@ -39,12 +62,16 @@ const WalletStatusVerifier: FC = ({ if (needsChainSwitch) { return ( switchChain(targetChainId)}> - {labelSwitchChain} {targetChain.name} + {switchChainLabel} {targetChain.name} ) } - return children + return ( + }> + {children} + + ) } export { WalletStatusVerifier } diff --git a/src/hooks/useOPL1CrossDomainMessengerProxy.ts b/src/hooks/useOPL1CrossDomainMessengerProxy.ts index f0756615..89dc4475 100644 --- a/src/hooks/useOPL1CrossDomainMessengerProxy.ts +++ b/src/hooks/useOPL1CrossDomainMessengerProxy.ts @@ -11,7 +11,6 @@ import { type ContractNames, getContract, } from '@/src/constants/contracts/contracts' -import { useWeb3StatusConnected } from '@/src/hooks/useWeb3Status' import { transports } from '@/src/lib/networks.config' async function l2ContractCallInfo({ @@ -132,6 +131,7 @@ export function useL1CrossDomainMessengerProxy({ functionName, args, value, + walletAddress, }: { fromChain: typeof sepolia | typeof mainnet l2ContractAddress: Address @@ -139,8 +139,8 @@ export function useL1CrossDomainMessengerProxy({ functionName: ContractFunctionName args: ContractFunctionArgs value: bigint -}) { - const { address: walletAddress } = useWeb3StatusConnected() + walletAddress: Address +}): () => Promise { const contract = getContract('OPL1CrossDomainMessengerProxy', fromChain.id) const { writeContractAsync } = useWriteContract() diff --git a/src/hooks/useWeb3Status.test.ts b/src/hooks/useWeb3Status.test.ts index 1010ea0b..c9afb54f 100644 --- a/src/hooks/useWeb3Status.test.ts +++ b/src/hooks/useWeb3Status.test.ts @@ -1,7 +1,9 @@ +import { useWeb3StatusConnected } from '@/src/components/sharedComponents/WalletStatusVerifier' import { renderHook } from '@testing-library/react' +import { createElement } from 'react' import type { Address } from 'viem' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { useWeb3Status, useWeb3StatusConnected } from './useWeb3Status' +import { useWeb3Status } from './useWeb3Status' const mockDisconnect = vi.fn() const mockSwitchChain = vi.fn() @@ -21,8 +23,28 @@ vi.mock('wagmi', () => ({ useDisconnect: vi.fn(() => ({ disconnect: mockDisconnect })), })) +vi.mock('@/src/hooks/useWalletStatus', () => ({ + useWalletStatus: vi.fn(() => ({ + isReady: false, + needsConnect: true, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' }, + targetChainId: 1, + switchChain: vi.fn(), + })), +})) + +vi.mock('@/src/providers/Web3Provider', () => ({ + ConnectWalletButton: () => + createElement('button', { type: 'button', 'data-testid': 'connect-wallet-button' }, 'Connect'), +})) + +import { WalletStatusVerifier } from '@/src/components/sharedComponents/WalletStatusVerifier' import * as wagmi from 'wagmi' +const { useWalletStatus } = await import('@/src/hooks/useWalletStatus') +const mockedUseWalletStatus = vi.mocked(useWalletStatus) + type MockAccount = ReturnType type MockSwitchChain = ReturnType @@ -107,20 +129,32 @@ describe('useWeb3Status', () => { describe('useWeb3StatusConnected', () => { it('throws when wallet is not connected', () => { expect(() => renderHook(() => useWeb3StatusConnected())).toThrow( - 'Use useWeb3StatusConnected only when a wallet is connected', + 'useWeb3StatusConnected must be used inside a component.', ) }) it('returns status when wallet is connected', () => { - const mock = { + mockedUseWalletStatus.mockReturnValue({ + isReady: true, + needsConnect: false, + needsChainSwitch: false, + targetChain: { id: 1, name: 'Ethereum' } as ReturnType['targetChain'], + targetChainId: 1, + switchChain: vi.fn(), + }) + + vi.mocked(wagmi.useAccount).mockReturnValueOnce({ address: '0xdeadbeef' as Address, chainId: 1, isConnected: true, isConnecting: false, - } as unknown as MockAccount - // useWeb3StatusConnected calls useWeb3Status twice; both calls must see connected state - vi.mocked(wagmi.useAccount).mockReturnValueOnce(mock).mockReturnValueOnce(mock) - const { result } = renderHook(() => useWeb3StatusConnected()) + } as unknown as ReturnType) + + const wrapper = ({ children }: { children: React.ReactNode }) => + createElement(WalletStatusVerifier, null, children) + + const { result } = renderHook(() => useWeb3StatusConnected(), { wrapper }) + expect(result.current.address).toBe('0xdeadbeef') expect(result.current.isWalletConnected).toBe(true) }) }) diff --git a/src/hooks/useWeb3Status.tsx b/src/hooks/useWeb3Status.tsx index af95dd25..0690ee13 100644 --- a/src/hooks/useWeb3Status.tsx +++ b/src/hooks/useWeb3Status.tsx @@ -13,7 +13,6 @@ import { } from 'wagmi' import { type ChainsIds, chains } from '@/src/lib/networks.config' -import type { RequiredNonNull } from '@/src/types/utils' export type AppWeb3Status = { readOnlyClient: UsePublicClientReturnType @@ -135,11 +134,3 @@ export const useWeb3Status = () => { return web3Connection } - -export const useWeb3StatusConnected = () => { - const context = useWeb3Status() - if (!context.isWalletConnected) { - throw new Error('Use useWeb3StatusConnected only when a wallet is connected') - } - return useWeb3Status() as RequiredNonNull -} diff --git a/src/utils/DeveloperError.ts b/src/utils/DeveloperError.ts new file mode 100644 index 00000000..86b7af0a --- /dev/null +++ b/src/utils/DeveloperError.ts @@ -0,0 +1,7 @@ +/** Error for structural/developer mistakes that cannot be fixed by retrying at runtime. */ +export class DeveloperError extends Error { + constructor(message: string) { + super(message) + this.name = 'DeveloperError' + } +} diff --git a/src/utils/suspenseWrapper.tsx b/src/utils/suspenseWrapper.tsx index 38253c23..e8aba0d2 100644 --- a/src/utils/suspenseWrapper.tsx +++ b/src/utils/suspenseWrapper.tsx @@ -1,5 +1,6 @@ import { GeneralMessage } from '@/src/components/sharedComponents/ui/GeneralMessage' import PrimaryButton from '@/src/components/sharedComponents/ui/PrimaryButton' +import { DeveloperError } from '@/src/utils/DeveloperError' import { Flex, Spinner } from '@chakra-ui/react' import { Dialog, Portal } from '@chakra-ui/react' import { QueryErrorResetBoundary } from '@tanstack/react-query' @@ -101,6 +102,10 @@ const defaultFallbackRender: ErrorBoundaryPropsWithRender['fallbackRender'] = ({ }: FallbackProps): ReactNode => { const message = error instanceof Error ? error.message : 'Something went wrong.' + if (error instanceof DeveloperError) { + return
{message}
+ } + return ( <>
{message}
@@ -122,6 +127,7 @@ const defaultFallbackRenderDialog: ErrorBoundaryPropsWithRender['fallbackRender' resetErrorBoundary, }: FallbackProps): ReactNode => { const message = error instanceof Error ? error.message : 'Something went wrong.' + const isDeveloperError = error instanceof DeveloperError return ( Try again} + actionButton={ + isDeveloperError ? undefined : ( + Try again + ) + } message={message} /> From b6e563255b13aa2fe896f54fc3408038125d0c3c Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 31 Mar 2026 18:12:02 +0200 Subject: [PATCH 10/11] fix: prevent .env.local RPC values from leaking into test env --- .env.test | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.env.test b/.env.test index f4bf82d2..4fc516d0 100644 --- a/.env.test +++ b/.env.test @@ -5,3 +5,7 @@ PUBLIC_WALLETCONNECT_PROJECT_ID=test-project-id PUBLIC_SUBGRAPHS_API_KEY=test-api-key PUBLIC_SUBGRAPHS_CHAINS_RESOURCE_IDS=1:test:test-resource-id PUBLIC_SUBGRAPHS_ENVIRONMENT=production + +# Explicitly unset optional RPC vars so .env.local values don't leak into tests +PUBLIC_RPC_MAINNET= +PUBLIC_RPC_SEPOLIA= From a614bea019564a4bf7183d34b18632b375741820 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 31 Mar 2026 18:13:22 +0200 Subject: [PATCH 11/11] chore: bump pnpm to 10.33.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 29e5067f..11625c97 100644 --- a/package.json +++ b/package.json @@ -91,5 +91,5 @@ "vitest": "^3.1.3", "vocs": "1.0.11" }, - "packageManager": "pnpm@10.30.2" + "packageManager": "pnpm@10.33.0" }