= ({
fallback = ,
labelSwitchChain = 'Switch to',
}: WalletStatusVerifierProps) => {
- const { appChainId, isWalletConnected, isWalletSynced, switchChain, walletChainId } =
- useWeb3Status()
+ const { needsConnect, needsChainSwitch, targetChain, targetChainId, 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(targetChainId)}>
+ {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
diff --git a/src/hooks/useWalletStatus.test.ts b/src/hooks/useWalletStatus.test.ts
new file mode 100644
index 00000000..ce5bd0fd
--- /dev/null
+++ b/src/hooks/useWalletStatus.test.ts
@@ -0,0 +1,160 @@
+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' })
+ expect(result.current.targetChainId).toBe(1)
+ })
+
+ 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' })
+ expect(result.current.targetChainId).toBe(10)
+ })
+
+ 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..3afae7cc
--- /dev/null
+++ b/src/hooks/useWalletStatus.ts
@@ -0,0 +1,39 @@
+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
+ targetChainId: ChainsIds
+ switchChain: (chainId: ChainsIds) => void
+}
+
+export const useWalletStatus = (options?: UseWalletStatusOptions): WalletStatus => {
+ const { appChainId, isWalletConnected, isWalletSynced, switchChain, walletChainId } =
+ useWeb3Status()
+
+ const targetChainId = options?.chainId || appChainId || chains[0].id
+ const targetChain = extractChain({ chains, id: targetChainId })
+
+ const needsConnect = !isWalletConnected
+ const needsChainSwitch = isWalletConnected && (!isWalletSynced || walletChainId !== targetChainId)
+ const isReady = isWalletConnected && !needsChainSwitch
+
+ return {
+ isReady,
+ needsConnect,
+ needsChainSwitch,
+ targetChain,
+ targetChainId,
+ switchChain,
+ }
+}
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/*"],
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