Skip to content

Commit 09bc3d5

Browse files
committed
chore: merge upstream develop changes (tooling, CI, config)
1 parent 1c3795d commit 09bc3d5

2 files changed

Lines changed: 278 additions & 0 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { ChakraProvider, createSystem, defaultConfig } from '@chakra-ui/react'
2+
import { render, screen } from '@testing-library/react'
3+
import userEvent from '@testing-library/user-event'
4+
import { createElement, type ReactNode } from 'react'
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
import { DeveloperError } from '@/src/core/utils/DeveloperError'
7+
import { useWeb3StatusConnected, WalletStatusVerifier } from './WalletStatusVerifier'
8+
9+
const mockSwitchChain = vi.fn()
10+
11+
vi.mock('@/src/wallet/hooks/useWalletStatus', () => ({
12+
useWalletStatus: vi.fn(() => ({
13+
isReady: false,
14+
needsConnect: true,
15+
needsChainSwitch: false,
16+
targetChain: { id: 1, name: 'Ethereum' },
17+
targetChainId: 1,
18+
switchChain: mockSwitchChain,
19+
})),
20+
}))
21+
22+
vi.mock('@/src/wallet/hooks/useWeb3Status', () => ({
23+
useWeb3Status: vi.fn(() => ({
24+
address: '0x1234567890abcdef1234567890abcdef12345678',
25+
appChainId: 1,
26+
balance: undefined,
27+
connectingWallet: false,
28+
disconnect: vi.fn(),
29+
isWalletConnected: true,
30+
isWalletSynced: true,
31+
readOnlyClient: {},
32+
switchChain: vi.fn(),
33+
switchingChain: false,
34+
walletChainId: 1,
35+
walletClient: {},
36+
})),
37+
}))
38+
39+
vi.mock('@/src/wallet/providers', () => ({
40+
ConnectWalletButton: () =>
41+
createElement(
42+
'button',
43+
{ type: 'button', 'data-testid': 'connect-wallet-button' },
44+
'Connect Wallet',
45+
),
46+
}))
47+
48+
const { useWalletStatus } = await import('@/src/wallet/hooks/useWalletStatus')
49+
const mockedUseWalletStatus = vi.mocked(useWalletStatus)
50+
51+
const system = createSystem(defaultConfig)
52+
53+
const renderWithChakra = (ui: ReactNode) =>
54+
render(<ChakraProvider value={system}>{ui}</ChakraProvider>)
55+
56+
describe('WalletStatusVerifier', () => {
57+
beforeEach(() => {
58+
vi.clearAllMocks()
59+
})
60+
61+
it('renders default fallback (ConnectWalletButton) when wallet needs connect', () => {
62+
mockedUseWalletStatus.mockReturnValue({
63+
isReady: false,
64+
needsConnect: true,
65+
needsChainSwitch: false,
66+
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
67+
targetChainId: 1,
68+
switchChain: mockSwitchChain,
69+
})
70+
71+
renderWithChakra(
72+
createElement(
73+
WalletStatusVerifier,
74+
null,
75+
createElement('div', { 'data-testid': 'protected-content' }, 'Protected'),
76+
),
77+
)
78+
79+
expect(screen.getByTestId('connect-wallet-button')).toBeInTheDocument()
80+
expect(screen.queryByTestId('protected-content')).toBeNull()
81+
})
82+
83+
it('renders custom fallback when provided and wallet needs connect', () => {
84+
mockedUseWalletStatus.mockReturnValue({
85+
isReady: false,
86+
needsConnect: true,
87+
needsChainSwitch: false,
88+
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
89+
targetChainId: 1,
90+
switchChain: mockSwitchChain,
91+
})
92+
93+
renderWithChakra(
94+
createElement(
95+
WalletStatusVerifier,
96+
{ fallback: createElement('div', { 'data-testid': 'custom-fallback' }, 'Custom') },
97+
createElement('div', { 'data-testid': 'protected-content' }, 'Protected'),
98+
),
99+
)
100+
101+
expect(screen.getByTestId('custom-fallback')).toBeInTheDocument()
102+
expect(screen.queryByTestId('protected-content')).toBeNull()
103+
})
104+
105+
it('renders switch chain button when wallet needs chain switch', () => {
106+
mockedUseWalletStatus.mockReturnValue({
107+
isReady: false,
108+
needsConnect: false,
109+
needsChainSwitch: true,
110+
targetChain: { id: 10, name: 'OP Mainnet' } as ReturnType<
111+
typeof useWalletStatus
112+
>['targetChain'],
113+
targetChainId: 10,
114+
switchChain: mockSwitchChain,
115+
})
116+
117+
renderWithChakra(
118+
createElement(
119+
WalletStatusVerifier,
120+
null,
121+
createElement('div', { 'data-testid': 'protected-content' }, 'Protected'),
122+
),
123+
)
124+
125+
expect(screen.getByText(/Switch to/)).toBeInTheDocument()
126+
expect(screen.getByText(/OP Mainnet/)).toBeInTheDocument()
127+
expect(screen.queryByTestId('protected-content')).toBeNull()
128+
})
129+
130+
it('renders children when wallet is ready', () => {
131+
mockedUseWalletStatus.mockReturnValue({
132+
isReady: true,
133+
needsConnect: false,
134+
needsChainSwitch: false,
135+
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
136+
targetChainId: 1,
137+
switchChain: mockSwitchChain,
138+
})
139+
140+
renderWithChakra(
141+
createElement(
142+
WalletStatusVerifier,
143+
null,
144+
createElement('div', { 'data-testid': 'protected-content' }, 'Protected'),
145+
),
146+
)
147+
148+
expect(screen.getByTestId('protected-content')).toBeInTheDocument()
149+
})
150+
151+
it('calls switchChain when switch button is clicked', async () => {
152+
const user = userEvent.setup()
153+
154+
mockedUseWalletStatus.mockReturnValue({
155+
isReady: false,
156+
needsConnect: false,
157+
needsChainSwitch: true,
158+
targetChain: { id: 10, name: 'OP Mainnet' } as ReturnType<
159+
typeof useWalletStatus
160+
>['targetChain'],
161+
targetChainId: 10,
162+
switchChain: mockSwitchChain,
163+
})
164+
165+
renderWithChakra(
166+
createElement(WalletStatusVerifier, null, createElement('div', null, 'Protected')),
167+
)
168+
169+
const switchButton = screen.getByText(/Switch to/)
170+
await user.click(switchButton)
171+
172+
expect(mockSwitchChain).toHaveBeenCalledWith(10)
173+
})
174+
175+
it('provides web3 status via context when wallet is ready', () => {
176+
mockedUseWalletStatus.mockReturnValue({
177+
isReady: true,
178+
needsConnect: false,
179+
needsChainSwitch: false,
180+
targetChain: { id: 1, name: 'Ethereum' } as ReturnType<typeof useWalletStatus>['targetChain'],
181+
targetChainId: 1,
182+
switchChain: mockSwitchChain,
183+
})
184+
185+
const Consumer = () => {
186+
const { address } = useWeb3StatusConnected()
187+
return createElement('div', { 'data-testid': 'address' }, address)
188+
}
189+
190+
renderWithChakra(createElement(WalletStatusVerifier, null, createElement(Consumer)))
191+
192+
expect(screen.getByTestId('address')).toHaveTextContent(
193+
'0x1234567890abcdef1234567890abcdef12345678',
194+
)
195+
})
196+
197+
it('throws DeveloperError when useWeb3StatusConnected is used outside WalletStatusVerifier', () => {
198+
const Consumer = () => {
199+
const { address } = useWeb3StatusConnected()
200+
return createElement('div', null, address)
201+
}
202+
203+
expect(() => renderWithChakra(createElement(Consumer))).toThrow(DeveloperError)
204+
})
205+
})
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { FC, ReactElement } from 'react'
2+
import { createContext, useContext } from 'react'
3+
import type { ChainsIds, RequiredNonNull } from '@/src/core/types'
4+
import { DeveloperError } from '@/src/core/utils/DeveloperError'
5+
import { useWalletStatus } from '../hooks/useWalletStatus'
6+
import { useWeb3Status, type Web3Status } from '../hooks/useWeb3Status'
7+
import { ConnectWalletButton } from '../providers'
8+
import SwitchChainButton from './SwitchChainButton'
9+
10+
type ConnectedWeb3Status = RequiredNonNull<Web3Status>
11+
12+
const WalletStatusVerifierContext = createContext<ConnectedWeb3Status | null>(null)
13+
14+
interface WalletStatusVerifierProps {
15+
chainId?: ChainsIds
16+
children?: ReactElement
17+
fallback?: ReactElement
18+
switchChainLabel?: string
19+
}
20+
21+
/**
22+
* Wrapper component that gates content on wallet connection and chain status.
23+
*
24+
* This is the primary API for protecting UI that requires a connected wallet.
25+
*
26+
* @deprecated Use {@link WalletGuard} from `@/src/sdk/react` instead.
27+
*
28+
* @example
29+
* ```tsx
30+
* <WalletStatusVerifier>
31+
* <MyProtectedComponent />
32+
* </WalletStatusVerifier>
33+
* ```
34+
*/
35+
const WalletStatusVerifier: FC<WalletStatusVerifierProps> = ({
36+
chainId,
37+
children,
38+
fallback = <ConnectWalletButton />,
39+
switchChainLabel = 'Switch to',
40+
}: WalletStatusVerifierProps) => {
41+
const web3Status = useWeb3Status()
42+
const { needsConnect, needsChainSwitch, targetChain, targetChainId, switchChain } =
43+
useWalletStatus({ chainId })
44+
45+
if (needsConnect) {
46+
return fallback
47+
}
48+
49+
if (needsChainSwitch) {
50+
return (
51+
<SwitchChainButton onClick={() => switchChain(targetChainId)}>
52+
{switchChainLabel} {targetChain.name}
53+
</SwitchChainButton>
54+
)
55+
}
56+
57+
return (
58+
<WalletStatusVerifierContext.Provider value={web3Status as ConnectedWeb3Status}>
59+
{children}
60+
</WalletStatusVerifierContext.Provider>
61+
)
62+
}
63+
64+
/** Reads the connected Web3 status from WalletStatusVerifier context. */
65+
const useWeb3StatusConnected = (): ConnectedWeb3Status => {
66+
const context = useContext(WalletStatusVerifierContext)
67+
if (context === null) {
68+
throw new DeveloperError('useWeb3StatusConnected must be used inside <WalletStatusVerifier>')
69+
}
70+
return context
71+
}
72+
73+
export { useWeb3StatusConnected, WalletStatusVerifier }

0 commit comments

Comments
 (0)