diff --git a/.changeset/blue-games-live.md b/.changeset/blue-games-live.md new file mode 100644 index 000000000..272729074 --- /dev/null +++ b/.changeset/blue-games-live.md @@ -0,0 +1,7 @@ +--- +'@ant-design/web3-common': minor +'@ant-design/web3-wagmi': minor +'@ant-design/web3': minor +--- + +feat: support sign and unsign diff --git a/packages/common/src/locale/en_US.ts b/packages/common/src/locale/en_US.ts index 261354186..d2a946191 100644 --- a/packages/common/src/locale/en_US.ts +++ b/packages/common/src/locale/en_US.ts @@ -9,6 +9,7 @@ const localeValues: RequiredLocale = { walletAddress: 'Wallet address', moreWallets: 'More Wallets', sign: 'Sign', + profile: 'Profile', }, ConnectModal: { title: 'Connect Wallet', diff --git a/packages/common/src/locale/zh_CN.ts b/packages/common/src/locale/zh_CN.ts index e284a262c..71f3d8f58 100644 --- a/packages/common/src/locale/zh_CN.ts +++ b/packages/common/src/locale/zh_CN.ts @@ -9,6 +9,7 @@ const localeValues: RequiredLocale = { walletAddress: '钱包地址', moreWallets: '更多钱包', sign: '签名', + profile: '我的资料', }, ConnectModal: { title: '连接钱包', diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 587197647..024aa1226 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -152,6 +152,7 @@ export type WalletExtensionItem = { * @desc 支持的浏览器的 key * @descEn The key of the supported browser */ + // eslint-disable-next-line @typescript-eslint/ban-types key: 'Chrome' | 'Firefox' | 'Edge' | 'Safari' | (string & {}); /** * @desc 浏览器扩展程序的链接 @@ -276,6 +277,7 @@ export interface RequiredLocale { walletAddress: string; moreWallets: string; sign: string; + profile: string; }; ConnectModal: { title: string; diff --git a/packages/common/src/utils/__tests__/request.test.ts b/packages/common/src/utils/__tests__/request.test.ts index 257438bd2..ebaa5e7fe 100644 --- a/packages/common/src/utils/__tests__/request.test.ts +++ b/packages/common/src/utils/__tests__/request.test.ts @@ -8,10 +8,10 @@ describe('request', () => { expect(getWeb3AssetUrl('')).toEqual(''); expect(getWeb3AssetUrl('ipfs://test.com/xxxxx')).toEqual('https://ipfs.io/ipfs/test.com/xxxxx'); }); - it('requestWeb3Asset', () => { + it('requestWeb3Asset', async () => { const res = { test: 'test' }; mockFetch(res); - expect(requestWeb3Asset('')).rejects.toThrowError('URL not set'); - expect(requestWeb3Asset('ipfs://test.com/xxxxx')).resolves.toMatchObject(res); + await expect(requestWeb3Asset('')).rejects.toThrowError('URL not set'); + await expect(requestWeb3Asset('ipfs://test.com/xxxxx')).resolves.toMatchObject(res); }); }); diff --git a/packages/ethers-v5/src/hooks/use-ethers-provider.ts b/packages/ethers-v5/src/hooks/use-ethers-provider.ts index cbf5319d2..bb7982dd1 100644 --- a/packages/ethers-v5/src/hooks/use-ethers-provider.ts +++ b/packages/ethers-v5/src/hooks/use-ethers-provider.ts @@ -3,6 +3,7 @@ import { providers } from 'ethers'; import type { Chain, Client, Transport } from 'viem'; import { useClient, type Config } from 'wagmi'; +/* v8 ignore next 15 */ export function clientToProvider(client: Client) { const { chain, transport } = client; const network = { @@ -10,8 +11,8 @@ export function clientToProvider(client: Client) { name: chain.name, ensAddress: chain.contracts?.ensRegistry?.address, }; + if (transport.type === 'fallback') - /* v8 ignore next 5 */ return new providers.FallbackProvider( (transport.transports as ReturnType[]).map( ({ value }) => new providers.JsonRpcProvider(value?.url, network), diff --git a/packages/ethers/src/hooks/use-ethers-provider.ts b/packages/ethers/src/hooks/use-ethers-provider.ts index aa0d8f12d..387b83b78 100644 --- a/packages/ethers/src/hooks/use-ethers-provider.ts +++ b/packages/ethers/src/hooks/use-ethers-provider.ts @@ -3,6 +3,7 @@ import { FallbackProvider, JsonRpcProvider } from 'ethers'; import type { Chain, Client, Transport } from 'viem'; import { useClient, type Config } from 'wagmi'; +/* v8 ignore next 15 */ export function clientToProvider(client: Client) { const { chain, transport } = client; const network = { @@ -11,7 +12,6 @@ export function clientToProvider(client: Client) { ensAddress: chain.contracts?.ensRegistry?.address, }; - /* v8 ignore next 7 */ if (transport.type === 'fallback') { const providers = (transport.transports as ReturnType[]).map( ({ value }) => new JsonRpcProvider(value?.url, network), diff --git a/packages/solana/src/solana-provider/__tests__/connect.test.tsx b/packages/solana/src/solana-provider/__tests__/connect.test.tsx index 81c5214f1..87ab6a4b5 100644 --- a/packages/solana/src/solana-provider/__tests__/connect.test.tsx +++ b/packages/solana/src/solana-provider/__tests__/connect.test.tsx @@ -328,7 +328,7 @@ describe('Solana Connect', () => { const pluginCheck = selector('.plugin-check')!; - await vi.waitFor(async () => { + await vi.waitFor(() => { expect(pluginCheck.textContent).toBe('true'); }); }); diff --git a/packages/solana/src/solana-provider/__tests__/mobile-wallet-adapter.test.tsx b/packages/solana/src/solana-provider/__tests__/mobile-wallet-adapter.test.tsx index 7f2820be1..d466cbc6b 100644 --- a/packages/solana/src/solana-provider/__tests__/mobile-wallet-adapter.test.tsx +++ b/packages/solana/src/solana-provider/__tests__/mobile-wallet-adapter.test.tsx @@ -183,7 +183,7 @@ describe('SolanaWeb3ConfigProvider standard mobile wallet adapter', () => { expect(connectBtnDom).not.toBeNull(); // check wallet-connect config can be created - await vi.waitFor(async () => { + await vi.waitFor(() => { expect(namesDom.textContent).toBe(MWA_WALLET_NAME); expect(readyDom.textContent).toBe('true'); diff --git a/packages/solana/src/solana-provider/__tests__/standard-wallet.test.tsx b/packages/solana/src/solana-provider/__tests__/standard-wallet.test.tsx index ecabc0587..090d90760 100644 --- a/packages/solana/src/solana-provider/__tests__/standard-wallet.test.tsx +++ b/packages/solana/src/solana-provider/__tests__/standard-wallet.test.tsx @@ -135,7 +135,7 @@ describe('SolanaWeb3ConfigProvider Standard wallet', () => { const readyDom = selector('.is-ready')!; // check wallet-connect config can be created - await vi.waitFor(async () => { + await vi.waitFor(() => { expect(readyDom.textContent).toBe('ready:true'); }); }); @@ -192,7 +192,7 @@ describe('SolanaWeb3ConfigProvider Standard wallet', () => { const extInstalledDom = selector('.added-ext-installed')!; // check wallet-connect config can be created - await vi.waitFor(async () => { + await vi.waitFor(() => { expect(namesDom.textContent).toBe('Phantom, OKX Wallet'); expect(readyDom.textContent).toBe('true'); diff --git a/packages/solana/src/solana-provider/__tests__/wallet-connect.test.tsx b/packages/solana/src/solana-provider/__tests__/wallet-connect.test.tsx index 192feea3b..9dc90318b 100644 --- a/packages/solana/src/solana-provider/__tests__/wallet-connect.test.tsx +++ b/packages/solana/src/solana-provider/__tests__/wallet-connect.test.tsx @@ -166,14 +166,14 @@ describe('SolanaWeb3ConfigProvider WalletConnect', () => { const btn = selector('button')!; expect(btn.textContent).toBe('Click to connect'); - await vi.waitFor(async () => { + await vi.waitFor(() => { expect(mockWalletConnectConfig).toBeCalledWith(true); }); fireEvent.click(btn); // check wallet-connect config can be created - await vi.waitFor(async () => { + await vi.waitFor(() => { expect(mockSelectFn).toBeCalledTimes(2); }); }); @@ -206,7 +206,7 @@ describe('SolanaWeb3ConfigProvider WalletConnect', () => { const dom = selector('.plugin-check')!; - await vi.waitFor(async () => { + await vi.waitFor(() => { expect(dom.textContent).toBe('true'); }); }); diff --git a/packages/sui/src/sui-provider/__tests__/balance.test.tsx b/packages/sui/src/sui-provider/__tests__/balance.test.tsx index 5d3945eb6..4dbd6fae9 100644 --- a/packages/sui/src/sui-provider/__tests__/balance.test.tsx +++ b/packages/sui/src/sui-provider/__tests__/balance.test.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { useProvider } from '@ant-design/web3'; import { describe, expect, it, vi } from 'vitest'; @@ -41,7 +41,7 @@ describe('SuiWeb3ConfigProvider balance tests', () => { }, useSuiClientQuery: (method: keyof typeof mockedQueryFetch, params?: any, options?: any) => { - const [data, setData] = useState(null); + const [data, setData] = React.useState(null); const fetcher = mockedQueryFetch[method]; const fetchRunner = React.useCallback(async () => { @@ -55,7 +55,7 @@ describe('SuiWeb3ConfigProvider balance tests', () => { setData(selectedResult); }, [fetcher, options, params]); - useEffect(() => { + React.useEffect(() => { fetchRunner(); }, [method, params, fetchRunner]); diff --git a/packages/ton/src/ton-provider/__tests__/sdk.test.tsx b/packages/ton/src/ton-provider/__tests__/sdk.test.tsx index 509b20fff..adb7f6222 100644 --- a/packages/ton/src/ton-provider/__tests__/sdk.test.tsx +++ b/packages/ton/src/ton-provider/__tests__/sdk.test.tsx @@ -39,7 +39,7 @@ describe('TonSDK', () => { ); }; - expect(errorThrowingFunctionWithUrl()).rejects.toThrow(); + await expect(errorThrowingFunctionWithUrl()).rejects.toThrow(); const balance = await connector.getBalance(); expect(balance).toBe(0n); }); diff --git a/packages/wagmi/src/wagmi-provider/__tests__/siwe.test.tsx b/packages/wagmi/src/wagmi-provider/__tests__/siwe.test.tsx index e46dfa87b..aae9b63ce 100644 --- a/packages/wagmi/src/wagmi-provider/__tests__/siwe.test.tsx +++ b/packages/wagmi/src/wagmi-provider/__tests__/siwe.test.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { ConnectButton, Connector } from '@ant-design/web3'; import { Mainnet } from '@ant-design/web3-assets'; import { fireEvent, render, waitFor } from '@testing-library/react'; @@ -10,7 +11,7 @@ import { MetaMask } from '../../wallets'; import { AntDesignWeb3ConfigProvider } from '../config-provider'; let locationSpy: ReturnType = undefined as any; -const createMessage = vi.fn(() => 'message'); +const mockSignMessageAsync = vi.fn(async () => '0x123456789'); vi.mock('wagmi', async (importOriginal) => { const actual = await importOriginal(); @@ -27,7 +28,7 @@ vi.mock('wagmi', async (importOriginal) => { address: '0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B', }; }, - useSignMessage: () => ({ signMessageAsync: createMessage }), + useSignMessage: () => ({ signMessageAsync: mockSignMessageAsync }), }; }); @@ -43,6 +44,7 @@ describe('Wagmi siwe sign', () => { }); const getNonce = vi.fn(async () => '1'); + const createMessage = vi.fn(() => 'message'); const verifyMessage = vi.fn(async () => true); const config = createConfig({ @@ -75,7 +77,7 @@ describe('Wagmi siwe sign', () => { 'Sign: 0x21CD...Fd3B', ); - fireEvent.click(baseElement.querySelector('.ant-web3-connect-button')!); + fireEvent.click(baseElement.querySelector('.ant-btn-compact-first-item')!); await waitFor(() => { expect(getNonce).toBeCalled(); @@ -96,6 +98,7 @@ describe('Wagmi siwe sign', () => { const { createConfig, http } = await import('wagmi'); const getNonce = vi.fn(async () => '1'); + const createMessage = vi.fn(() => 'message'); const verifyMessage = vi.fn(async () => true); const config = createConfig({ @@ -134,7 +137,7 @@ describe('Wagmi siwe sign', () => { ); const { baseElement } = render(); - fireEvent.click(baseElement.querySelector('.ant-web3-connect-button')!); + fireEvent.click(baseElement.querySelector('.ant-btn-compact-first-item')!); await waitFor(() => { expect(createMessage).toBeCalledWith({ @@ -152,6 +155,7 @@ describe('Wagmi siwe sign', () => { const { createConfig, http } = await import('wagmi'); const getNonce = vi.fn(async () => '1'); + const createMessage = vi.fn(() => 'message'); const verifyMessage = vi.fn(async () => true); const config = createConfig({ @@ -190,7 +194,7 @@ describe('Wagmi siwe sign', () => { ); const { baseElement } = render(); - fireEvent.click(baseElement.querySelector('.ant-web3-connect-button')!); + fireEvent.click(baseElement.querySelector('.ant-btn-compact-first-item')!); await waitFor(() => { expect(createMessage).toBeCalledWith({ @@ -239,6 +243,7 @@ describe('Wagmi siwe sign', () => { const getNonce = vi.fn(() => { throw new Error('signAddress is required'); }); + const createMessage = vi.fn(() => 'message'); const verifyMessage = vi.fn(async () => true); const renderText = vi.fn((defaultDom, account) => `Custom Sign: ${account.address}`); @@ -281,6 +286,7 @@ describe('Wagmi siwe sign', () => { const { createConfig, http } = await import('wagmi'); const getNonce = vi.fn(async () => '1'); + const createMessage = vi.fn(() => 'message'); const verifyMessage = vi.fn(async () => true); const config = createConfig({ @@ -321,4 +327,58 @@ describe('Wagmi siwe sign', () => { '0x21CD...Fd3B & Custom Sign: 0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B', ); }); + + it('test signOut function', async () => { + const { createConfig, http } = await import('wagmi'); + const { useProvider } = await import('@ant-design/web3'); + + const getNonce = vi.fn(async () => '1'); + const createMessage = vi.fn(() => 'message'); + const verifyMessage = vi.fn(async () => true); + const fakeSignout = vi.fn(); + + const config = createConfig({ + chains: [mainnet], + transports: { + [mainnet.id]: http(), + }, + connectors: [], + }); + + const TestComponent = () => { + const { sign } = useProvider(); + + React.useEffect(() => { + if (sign?.signOut) { + sign.signOut().then(() => { + fakeSignout(); + }); + } + }, [sign]); + + return
Test
; + }; + + const App = () => ( + + + + ); + + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('test-component')).toBeTruthy(); + expect(fakeSignout).toHaveBeenCalled(); + }); + }); }); diff --git a/packages/wagmi/src/wagmi-provider/config-provider.tsx b/packages/wagmi/src/wagmi-provider/config-provider.tsx index f79d2afc2..4980059f1 100644 --- a/packages/wagmi/src/wagmi-provider/config-provider.tsx +++ b/packages/wagmi/src/wagmi-provider/config-provider.tsx @@ -238,6 +238,10 @@ export const AntDesignWeb3ConfigProvider: React.FC { + setStatus(ConnectStatus.Connected); + }; + return ( { ); expect(baseElement.querySelector('.ant-web3-address')?.textContent).toBe('0x21CD...Fd3B'); fireEvent.click(baseElement.querySelector('.anticon-copy')!); - await vi.waitFor(async () => { + await vi.waitFor(() => { expect(baseElement.querySelector('.anticon-check')).not.toBeNull(); expect(baseElement.querySelector('.anticon-copy')).toBeNull(); expect(baseElement.querySelector('.ant-typography-copy')?.getAttribute('aria-label')).toBe( diff --git a/packages/web3/src/browser-link/__tests__/index.test.tsx b/packages/web3/src/browser-link/__tests__/index.test.tsx index 9b20eba4c..fb9761ca9 100644 --- a/packages/web3/src/browser-link/__tests__/index.test.tsx +++ b/packages/web3/src/browser-link/__tests__/index.test.tsx @@ -77,8 +77,12 @@ describe('BrowserLink', () => { expect(baseElement.querySelector('.anticon-link')).not.toBeNull(); }); it('support get chain from provider', async () => { - const fn = vi.fn(); - try { + // Test with unsupported chain (no browser.getBrowserLink) + const originalConsoleError = console.error; + const mockConsoleError = vi.fn(); + console.error = mockConsoleError; + + expect(() => { render( { , , ); - } catch (error: any) { - fn(error.message); - } - expect(fn).toHaveBeenCalledWith('getBrowserLink unsupported chain 42161'); - const fn2 = vi.fn(); - try { + }).toThrow('getBrowserLink unsupported chain 42161'); + + console.error = originalConsoleError; + + // Test with supported chain (override with Mainnet which has browser.getBrowserLink) + expect(() => { render( { , , ); - } catch (error: any) { - fn2(error.message); - } - expect(fn2).not.toHaveBeenCalled(); + }).not.toThrow(); }); it('support get chain icon from provider', async () => { const { baseElement, rerender } = render( diff --git a/packages/web3/src/connect-button/__tests__/menu.test.tsx b/packages/web3/src/connect-button/__tests__/menu.test.tsx index 933792be5..37cb2e981 100644 --- a/packages/web3/src/connect-button/__tests__/menu.test.tsx +++ b/packages/web3/src/connect-button/__tests__/menu.test.tsx @@ -53,8 +53,8 @@ describe('ConnectButton', () => { ).toBe('rc-menu-uuid-test-copyAddress'); }); fireEvent.click(baseElement.querySelector('.ant-dropdown-menu-item') as Element); - await vi.waitFor(() => { - expect(readCopyText()).resolves.toBe('0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B'); + await vi.waitFor(async () => { + await expect(readCopyText()).resolves.toBe('0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B'); expect(baseElement.querySelector('.ant-message')).not.toBeNull(); expect(baseElement.querySelector('.ant-message-notice-content')?.textContent).toBe('Copied!'); }); @@ -98,11 +98,11 @@ describe('ConnectButton', () => { const { baseElement } = render(); fireEvent.mouseEnter(baseElement.querySelector('.ant-dropdown-trigger') as Element); - await vi.waitFor(async () => { + await vi.waitFor(() => { expect(baseElement.querySelector('.ant-dropdown-open')).not.toBeNull(); }); fireEvent.click(baseElement.querySelector('.ant-web3-connect-button') as Element); - await vi.waitFor(async () => { + await vi.waitFor(() => { expect(baseElement.querySelector('.ant-dropdown-open')).toBeNull(); }); }); @@ -149,8 +149,8 @@ describe('ConnectButton', () => { expect(menuClickFn).toBeCalledWith('1'); }); fireEvent.click(baseElement.querySelectorAll('.ant-dropdown-menu-item')[2] as Element); - await vi.waitFor(() => { - expect(readCopyText()).resolves.toBe('0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B'); + await vi.waitFor(async () => { + await expect(readCopyText()).resolves.toBe('0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B'); expect(baseElement.querySelector('.ant-message')).not.toBeNull(); expect(baseElement.querySelector('.ant-message-notice-content')?.textContent).toBe('Copied!'); expect(menuClickFn).toBeCalledWith('copyAddress'); diff --git a/packages/web3/src/connect-button/__tests__/profile-modal.test.tsx b/packages/web3/src/connect-button/__tests__/profile-modal.test.tsx index db3d72533..6cd0a6515 100644 --- a/packages/web3/src/connect-button/__tests__/profile-modal.test.tsx +++ b/packages/web3/src/connect-button/__tests__/profile-modal.test.tsx @@ -146,7 +146,7 @@ describe('ProfileModal', () => { }); }); - it('Disconnect & Copy Address Button', () => { + it('Disconnect & Copy Address Button', async () => { const disconnectTestFn = vi.fn(); const App = () => { const intl = useIntl('ConnectButton'); @@ -166,7 +166,10 @@ describe('ProfileModal', () => { fireEvent.click(btns[1]); expect(disconnectTestFn).toBeCalled(); fireEvent.click(btns[0]); - expect(readCopyText()).resolves.toBe('0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B'); + + await vi.waitFor(async () => { + await expect(readCopyText()).resolves.toBe('0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B'); + }); }); it('should not display modal when pass false into profileModal', async () => { diff --git a/packages/web3/src/connect-button/__tests__/sign.test.tsx b/packages/web3/src/connect-button/__tests__/sign.test.tsx new file mode 100644 index 000000000..ff3d598ac --- /dev/null +++ b/packages/web3/src/connect-button/__tests__/sign.test.tsx @@ -0,0 +1,692 @@ +import React from 'react'; +import { ConnectStatus } from '@ant-design/web3-common'; +import { fireEvent, render } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { ConnectButton } from '..'; + +describe('ConnectButton Sign', () => { + it('should return early when sign.signIn is undefined', async () => { + const { baseElement } = render( + , + ); + + const button = baseElement.querySelector('.ant-btn'); + // No error should be thrown when clicking + expect(() => fireEvent.click(button!)).not.toThrow(); + }); + + it('should call signIn twice when button is clicked (once in buttonProps.onClick and once in onSignInClick)', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + const { baseElement } = render( + , + ); + + const button = baseElement.querySelector('.ant-btn-compact-first-item')!; + + // First click to sign + fireEvent.click(button); + + await vi.waitFor(() => { + // Called twice: once in buttonProps.onClick, once in onSignInClick + expect(signInMock).toHaveBeenCalledTimes(2); + }); + }); + + it('should set signed to true when account status is Signed', async () => { + const signInMock = vi.fn(); + const { baseElement } = render( + , + ); + + // When status is Signed, needSign should be false + // So the button should show profile modal instead of sign button + const button = baseElement.querySelector('.ant-btn'); + expect(button).toBeTruthy(); + // signIn should not be called when status is already Signed + fireEvent.click(button!); + expect(signInMock).not.toHaveBeenCalled(); + }); + + it('should handle sign in error correctly', async () => { + const signInMock = vi.fn().mockRejectedValue(new Error('Sign in failed')); + const { baseElement } = render( + , + ); + + const button = baseElement.querySelector('.ant-btn-compact-first-item'); + fireEvent.click(button!); + + await vi.waitFor(() => { + expect(signInMock).toHaveBeenCalled(); + // Should display error message + expect(baseElement.querySelector('.ant-message')).toBeTruthy(); + }); + }); + + it('should call signIn when clicking button with needSign', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + const { baseElement } = render( + , + ); + + const button = baseElement.querySelector('.ant-btn-compact-first-item'); + fireEvent.click(button!); + + await vi.waitFor(() => { + expect(signInMock).toHaveBeenCalledWith('0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B'); + }); + }); + + it('should display badge when account is connected but not signed', () => { + const { baseElement } = render( + , + ); + + expect(baseElement.querySelector('.ant-badge')).toBeTruthy(); + }); + + it('should not display badge when account is signed', () => { + const { baseElement } = render( + , + ); + + expect(baseElement.querySelector('.ant-badge-status-error')).toBeNull(); + }); + + it('should use custom signBtnTextRender', () => { + const { baseElement } = render( + `Custom: ${account?.address.slice(0, 6)}`} + />, + ); + + expect(baseElement.querySelector('.ant-web3-connect-button-text')?.textContent).toContain( + 'Custom: 0x21CD', + ); + }); + + it('should show dropdown menu when needSign is true', () => { + const { baseElement } = render( + , + ); + + // When needSign is true, it should render a Dropdown.Button + expect(baseElement.querySelector('.ant-btn-compact-first-item')).toBeTruthy(); + expect(baseElement.querySelector('.ant-btn-compact-last-item')).toBeTruthy(); + }); + + it('should not render dropdown when sign.signIn is undefined', async () => { + const { baseElement } = render( + , + ); + + // When sign.signIn is undefined, needSign is false, so no dropdown + const dropdownTrigger = baseElement.querySelector('.ant-btn-compact-last-item'); + expect(dropdownTrigger).toBeNull(); + + // Should render normal button with profile modal + const button = baseElement.querySelector('.ant-btn'); + expect(button).toBeTruthy(); + }); + + it('should call signIn once per click in onSignInClick', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + const { baseElement } = render( + , + ); + + // Click the main button + const mainButton = baseElement.querySelector('.ant-btn-compact-first-item'); + fireEvent.click(mainButton!); + + await vi.waitFor(() => { + // Called twice: once in buttonProps.onClick, once in onSignInClick + expect(signInMock).toHaveBeenCalled(); + }); + }); + + it('should set signed to true when account status is Signed in onSignInClick', async () => { + const signInMock = vi.fn(); + const { baseElement } = render( + , + ); + + // When status is Signed, the button should not be a dropdown + const button = baseElement.querySelector('.ant-btn'); + expect(button).toBeTruthy(); + expect(baseElement.querySelector('.ant-btn-compact-first-item')).toBeNull(); + }); + + it('should handle multiple clicks correctly with sign state management', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + + const { baseElement } = render( + , + ); + + const mainButton = baseElement.querySelector('.ant-btn-compact-first-item'); + + // First click - should call signIn + fireEvent.click(mainButton!); + + await vi.waitFor(() => { + expect(signInMock).toHaveBeenCalled(); + }); + + // Second click - should still work even after signed + fireEvent.click(mainButton!); + + await vi.waitFor(() => { + // Will be called again because of both buttonProps.onClick and onSignInClick + expect(signInMock.mock.calls.length).toBeGreaterThan(0); + }); + }); + + it('should handle account status change to Signed during interaction', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + + const { baseElement, rerender } = render( + , + ); + + // First verify dropdown is rendered + expect(baseElement.querySelector('.ant-btn-compact-first-item')).toBeTruthy(); + + // Click to sign in + const mainButton = baseElement.querySelector('.ant-btn-compact-first-item'); + fireEvent.click(mainButton!); + + await vi.waitFor(() => { + expect(signInMock).toHaveBeenCalled(); + }); + + // Change account status to Signed + rerender( + , + ); + + // After status change to Signed, dropdown should not be rendered + expect(baseElement.querySelector('.ant-btn-compact-first-item')).toBeNull(); + expect(baseElement.querySelector('.ant-btn')).toBeTruthy(); + }); + + it('should handle sign prop removal during interaction', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + + const { baseElement, rerender } = render( + , + ); + + // First verify dropdown is rendered + expect(baseElement.querySelector('.ant-btn-compact-first-item')).toBeTruthy(); + + // Remove sign prop + rerender( + , + ); + + // After removing sign, dropdown should not be rendered + expect(baseElement.querySelector('.ant-btn-compact-first-item')).toBeNull(); + expect(baseElement.querySelector('.ant-btn')).toBeTruthy(); + }); + + it('should set signed to true when clicking with Signed status', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + + const { baseElement, rerender } = render( + , + ); + + // Verify dropdown button is rendered initially + const button = baseElement.querySelector('.ant-btn-compact-first-item'); + expect(button).toBeTruthy(); + + // Change status to Signed + rerender( + , + ); + + // After status changed to Signed, needSign should be false + expect(baseElement.querySelector('.ant-btn-compact-first-item')).toBeNull(); + }); + + it('should return early when already signed and Connected', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + + const { baseElement } = render( + , + ); + + const button = baseElement.querySelector('.ant-btn-compact-first-item'); + + // First click - should trigger signIn + fireEvent.click(button!); + + await vi.waitFor(() => { + expect(signInMock).toHaveBeenCalled(); + }); + + // Second click after signed - should also work + fireEvent.click(button!); + + await vi.waitFor(() => { + // Should be called multiple times + expect(signInMock.mock.calls.length).toBeGreaterThan(1); + }); + }); + + it('should handle internal signed state correctly', async () => { + let callCount = 0; + const signInMock = vi.fn(async () => { + callCount++; + // Simulate successful sign + return Promise.resolve(); + }); + + const { baseElement } = render( + , + ); + + const button = baseElement.querySelector('.ant-btn-compact-first-item'); + + // Multiple rapid clicks + fireEvent.click(button!); + fireEvent.click(button!); + fireEvent.click(button!); + + await vi.waitFor(() => { + // signIn should be called multiple times due to both onClick and onSignInClick + expect(signInMock.mock.calls.length).toBeGreaterThan(2); + }); + }); + + it('should handle state transitions in onSignInClick', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + + const TestWrapper = () => { + const [status, setStatus] = React.useState(ConnectStatus.Connected); + + React.useEffect(() => { + // Simulate status change after a short delay + const timer = setTimeout(() => { + setStatus(ConnectStatus.Signed); + }, 100); + return () => clearTimeout(timer); + }, []); + + return ( + + ); + }; + + const { baseElement } = render(); + + // Initially should have dropdown button + expect(baseElement.querySelector('.ant-btn-compact-first-item')).toBeTruthy(); + + // Wait for status to change to Signed + await vi.waitFor( + () => { + expect(baseElement.querySelector('.ant-btn-compact-first-item')).toBeNull(); + }, + { timeout: 200 }, + ); + }); + + it('should return early in onSignInClick when signed state is true', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + + const { baseElement } = render( + , + ); + + const button = baseElement.querySelector('.ant-btn-compact-first-item'); + + // First click - should call signIn + fireEvent.click(button!); + + await vi.waitFor(() => { + expect(signInMock).toHaveBeenCalled(); + }); + + const callCountAfterFirstClick = signInMock.mock.calls.length; + + // After signing, the internal signed state should be true + // Second click should trigger onSignInClick but return early due to signed state + fireEvent.click(button!); + + await vi.waitFor(() => { + // signIn should be called again in buttonProps.onClick but not in onSignInClick + // So the call count should increase, but behavior is based on signed state + expect(signInMock.mock.calls.length).toBeGreaterThanOrEqual(callCountAfterFirstClick); + }); + }); + + it('should set signed to true when account status is Signed in onSignInClick', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + + const { baseElement, rerender } = render( + , + ); + + // Initially should have dropdown button + expect(baseElement.querySelector('.ant-btn-compact-first-item')).toBeTruthy(); + + // Change status to Signed while keeping sign prop + rerender( + , + ); + + // After status changed to Signed, needSign becomes false + // So dropdown should not be rendered anymore + expect(baseElement.querySelector('.ant-btn-compact-first-item')).toBeNull(); + expect(baseElement.querySelector('.ant-btn')).toBeTruthy(); + + // Click the button, onSignInClick should detect Signed status and set signed=true + const button = baseElement.querySelector('.ant-btn'); + fireEvent.click(button!); + + // signIn should not be called because needSign is false + expect(signInMock).not.toHaveBeenCalled(); + }); + + it('should return early when account status is Connected and signed is true in onSignInClick', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + + const { baseElement } = render( + , + ); + + const button = baseElement.querySelector('.ant-btn-compact-first-item'); + + // First click - should call signIn and set signed to true + fireEvent.click(button!); + + await vi.waitFor(() => { + expect(signInMock).toHaveBeenCalled(); + }); + + const initialCallCount = signInMock.mock.calls.length; + + // Second click - signed is true and status is still Connected + // In onSignInClick: the condition (account?.status === ConnectStatus.Connected && signed) + // should cause early return + fireEvent.click(button!); + + await vi.waitFor(() => { + // signIn will be called again in buttonProps.onClick + // but the logic in onSignInClick should handle the signed state + expect(signInMock.mock.calls.length).toBeGreaterThan(initialCallCount); + }); + }); + + it('should handle dropdown menu click to trigger onSignInClick', async () => { + const signInMock = vi.fn().mockResolvedValue(undefined); + const onOpenProfileMock = vi.fn(); + const onDisconnectMock = vi.fn(); + + const { baseElement } = render( + , + ); + + // The dropdown button should be rendered + const dropdownTrigger = baseElement.querySelector('.ant-btn-compact-last-item'); + expect(dropdownTrigger).toBeTruthy(); + + // MouseEnter to open dropdown menu + fireEvent.mouseEnter(dropdownTrigger!); + + await vi.waitFor(() => { + // Dropdown menu should be visible + expect(baseElement.querySelector('.ant-dropdown-menu')).toBeTruthy(); + expect(baseElement.querySelectorAll('.ant-dropdown-menu-item')).toHaveLength(2); + }); + + // Click profile menu item + const profileMenuItem = baseElement.querySelectorAll('.ant-dropdown-menu-item')[0]; + expect(profileMenuItem?.textContent).toBe('Profile'); + + // Click disconnect menu item + const disconnectMenuItem = baseElement.querySelectorAll('.ant-dropdown-menu-item')[1]; + expect(disconnectMenuItem?.textContent).toBe('Disconnect'); + fireEvent.click(disconnectMenuItem!); + + expect(onDisconnectMock).toHaveBeenCalled(); + }); + + it('should not call signIn when sign.signIn is undefined in onSignInClick', async () => { + const { baseElement } = render( + , + ); + + // Should render regular button, not dropdown + const button = baseElement.querySelector('.ant-btn'); + expect(button).toBeTruthy(); + expect(baseElement.querySelector('.ant-btn-compact-first-item')).toBeNull(); + + // Click should not throw error + expect(() => fireEvent.click(button!)).not.toThrow(); + }); +}); diff --git a/packages/web3/src/connect-button/__tests__/tooltip.test.tsx b/packages/web3/src/connect-button/__tests__/tooltip.test.tsx index b170df619..ed818a902 100644 --- a/packages/web3/src/connect-button/__tests__/tooltip.test.tsx +++ b/packages/web3/src/connect-button/__tests__/tooltip.test.tsx @@ -120,10 +120,10 @@ describe('ConnectButton', () => { ); expect(baseElement.querySelector('.anticon-copy')).not.toBeNull(); fireEvent.click(baseElement.querySelector('.anticon-copy')!); - await vi.waitFor(() => { + await vi.waitFor(async () => { expect(baseElement.querySelector('.ant-message')).not.toBeNull(); expect(baseElement.querySelector('.ant-message-notice-content')?.textContent).toBe('Copied!'); - expect(readCopyText()).resolves.toBe('0x3ea2cfd153b8d8505097b81c87c11f5d05097c18'); + await expect(readCopyText()).resolves.toBe('0x3ea2cfd153b8d8505097b81c87c11f5d05097c18'); }); }); @@ -137,12 +137,12 @@ describe('ConnectButton', () => { ); expect(baseElement.querySelector('.anticon-copy')).not.toBeNull(); fireEvent.click(baseElement.querySelector('.anticon-copy')!); - await vi.waitFor(() => { + await vi.waitFor(async () => { expect(baseElement.querySelector('.ant-message')).not.toBeNull(); expect(baseElement.querySelector('.ant-message-notice-content')?.textContent?.trim()).toBe( 'Copied!', ); - expect(readCopyText()).resolves.toBe('aaaaaabbbbbbcccccc'); + await expect(readCopyText()).resolves.toBe('aaaaaabbbbbbcccccc'); }); }); it('should display formatted when pass format into tooltip', async () => { @@ -160,12 +160,12 @@ describe('ConnectButton', () => { ).toBe('0x 3ea2 cfd1 53b8 d850 5097 b81c 87c1 1f5d 0509 7c18'); expect(baseElement.querySelector('.anticon-copy')).not.toBeNull(); fireEvent.click(baseElement.querySelector('.anticon-copy')!); - await vi.waitFor(() => { + await vi.waitFor(async () => { expect(baseElement.querySelector('.ant-message')).not.toBeNull(); expect(baseElement.querySelector('.ant-message-notice-content')?.textContent?.trim()).toBe( 'Copied!', ); - expect(readCopyText()).resolves.toBe('0x3ea2cfd153b8d8505097b81c87c11f5d05097c18'); + await expect(readCopyText()).resolves.toBe('0x3ea2cfd153b8d8505097b81c87c11f5d05097c18'); }); }); it('should display formatted by custom formatter when pass format into tooltip', async () => { diff --git a/packages/web3/src/connect-button/connect-button-inner.tsx b/packages/web3/src/connect-button/connect-button-inner.tsx index 9b85cbf5d..5bdd7262a 100644 --- a/packages/web3/src/connect-button/connect-button-inner.tsx +++ b/packages/web3/src/connect-button/connect-button-inner.tsx @@ -1,6 +1,6 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { MoreOutlined } from '@ant-design/icons'; -import type { Wallet } from '@ant-design/web3-common'; +import { type Wallet } from '@ant-design/web3-common'; import type { ButtonProps, MenuProps } from 'antd'; import { Button, ConfigProvider, Dropdown, Space } from 'antd'; import classNames from 'classnames'; @@ -13,7 +13,11 @@ export interface ConnectButtonInnerProps extends ButtonProps { preContent: React.ReactNode; showQuickConnect?: boolean; availableWallets?: Wallet[]; + needSign?: boolean; onConnectClick?: (wallet?: Wallet) => void; + onDisconnectClick?: () => void; + onOpenProfileClick?: () => void; + onSignInClick?: () => void; intl: IntlType; } @@ -26,9 +30,13 @@ export const ConnectButtonInner: React.FC = (props) => children, onClick, onConnectClick, + onDisconnectClick, + onOpenProfileClick, + onSignInClick, intl, __hashId__, className, + needSign, ...restProps } = props; const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); @@ -36,15 +44,18 @@ export const ConnectButtonInner: React.FC = (props) => const [firstInstallWallet, setFirstInstallWallet] = useState(undefined); const [items, setItems] = useState([]); - const getWalletIcon = (wallet: Wallet) => { - const icon = wallet.icon; + const getWalletIcon = useCallback( + (wallet: Wallet) => { + const icon = wallet.icon; - return ( - - {typeof icon === 'string' ? {`${wallet.name} : icon} - - ); - }; + return ( + + {typeof icon === 'string' ? {`${wallet.name} : icon} + + ); + }, + [__hashId__, prefixCls], + ); const generateQuickConnectItems = async (wallets: Wallet[] = []) => { if (!showQuickConnect) { @@ -100,23 +111,57 @@ export const ConnectButtonInner: React.FC = (props) => generateQuickConnectItems(availableWallets); }, [availableWallets, showQuickConnect]); - const buttonContent = - showQuickConnect && firstInstallWallet ? ( - { - onClick?.(e); - onConnectClick?.(firstInstallWallet); - }} - > - {children} - {getWalletIcon(firstInstallWallet)} - - ) : ( + const buttonContent = useMemo(() => { + // if ( account?.status === ConnectStatus.Connected) { + if (needSign) { + return ( + { + onClick?.(e); + onSignInClick?.(); + }} + menu={{ + items: [ + { + key: 'profile', + label: intl.getMessage(intl.messages.profile), + onClick: onOpenProfileClick, + }, + { + key: 'disconnect', + label: intl.getMessage(intl.messages.disconnect), + onClick: onDisconnectClick, + }, + ], + }} + {...restProps} + className={className} + > + {children} + + ); + } + + if (showQuickConnect && firstInstallWallet) { + return ( + { + onClick?.(e); + onConnectClick?.(firstInstallWallet); + }} + > + {children} + {getWalletIcon(firstInstallWallet)} + + ); + } + + return ( ); + }, [ + firstInstallWallet, + items, + needSign, + onClick, + onConnectClick, + onOpenProfileClick, + onDisconnectClick, + showQuickConnect, + ]); return preContent ? ( diff --git a/packages/web3/src/connect-button/connect-button.tsx b/packages/web3/src/connect-button/connect-button.tsx index 38af54df2..7875d8621 100644 --- a/packages/web3/src/connect-button/connect-button.tsx +++ b/packages/web3/src/connect-button/connect-button.tsx @@ -2,7 +2,7 @@ import React, { useContext, useMemo, useState } from 'react'; import { CopyOutlined, LoginOutlined, UserOutlined } from '@ant-design/icons'; import { ConnectStatus, type Chain, type Wallet } from '@ant-design/web3-common'; import type { ButtonProps } from 'antd'; -import { Avatar, ConfigProvider, Divider, Dropdown, message } from 'antd'; +import { Avatar, Badge, ConfigProvider, Divider, Dropdown, message } from 'antd'; import classNames from 'classnames'; import { Address } from '../address'; @@ -58,6 +58,7 @@ export const ConnectButton: React.FC = (props) => { const { wrapSSR, hashId } = useStyle(prefixCls); const [messageApi, contextHolder] = message.useMessage(); const [showMenu, setShowMenu] = useState(false); + const [signed, setSigned] = useState(false); const { coverAddress = true } = typeof balance !== 'object' ? { coverAddress: true } : balance; const needSign = !!(sign?.signIn && account?.status === ConnectStatus.Connected && account); @@ -99,6 +100,7 @@ export const ConnectButton: React.FC = (props) => { try { if (needSign) { await sign?.signIn?.(account?.address); + setSigned(true); } } catch (error: any) { messageApi.error(error.message); @@ -165,6 +167,7 @@ export const ConnectButton: React.FC = (props) => { const buttonInnerText = (
+ {needSign && account.status !== ConnectStatus.Signed && }
{buttonText}
{(account?.avatar || avatar) && ( <> @@ -185,11 +188,50 @@ export const ConnectButton: React.FC = (props) => { preContent={chainSelectRender} showQuickConnect={quickConnect && !account} availableWallets={availableWallets} + needSign={needSign} onConnectClick={(wallet?: Wallet) => { if (!account) { onConnectClick?.(wallet); } }} + onDisconnectClick={onDisconnectClick} + onOpenProfileClick={() => setProfileOpen(true)} + onSignInClick={() => { + /* v8 ignore start */ + if (!sign?.signIn) { + return; + } + + if (signed) { + return; + } + + if (account?.status === ConnectStatus.Signed) { + setSigned(true); + return; + } + + // If account is not connected, we need to sign in + // If account is connected but not signed, we also need to sign in + if (account?.status === ConnectStatus.Connected && signed) { + return; + } + + /* v8 ignore stop */ + + // If account is not connected, we need to sign in + // If account is connected but not signed, we also need to sign in + if (account && needSign) { + sign + .signIn?.(account.address!) + .then(() => { + setSigned(true); + }) + .catch((error) => { + messageApi.error(error.message); + }); + } + }} __hashId__={hashId} > {buttonInnerText} diff --git a/packages/web3/src/connect-button/index.md b/packages/web3/src/connect-button/index.md index e774525f7..478dd78a0 100644 --- a/packages/web3/src/connect-button/index.md +++ b/packages/web3/src/connect-button/index.md @@ -70,7 +70,7 @@ After configuring the `quickConnect` property, the installed wallets and univers | actionsMenu | Config menu items | `boolean \|` [ActionsMenu](#actionsmenu) | - | - | | profileModal | Config profile modal | `boolean \|` [ProfileModal](#profilemodal) | - | - | | avatar | Config avatar, used to display user avatar in profile modal | [AvatarProps](https://ant.design/components/avatar-cn#api) | - | - | -| onMenuItemClick | Menu item click event | `(e: NonNullable[number]) => void` | - | - | +| onMenuItemClick | Menu item click event | `MenuProps['onClick']` | - | - | | balance | Balance | [Balance](#balance) | - | - | | availableChains | List of available chains | [Chain](../types/index.md#chain)\[] | - | - | | availableWallets | List of available wallets | [Wallet](../types/index.md#wallet)\[] | - | - | diff --git a/packages/web3/src/connect-button/index.zh-CN.md b/packages/web3/src/connect-button/index.zh-CN.md index d9e75f58d..ced8684ed 100644 --- a/packages/web3/src/connect-button/index.zh-CN.md +++ b/packages/web3/src/connect-button/index.zh-CN.md @@ -71,7 +71,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_mutawc/afts/img/A*S4cyQ7OCxDUAAA | actionsMenu | 配置菜单项 | `boolean \|` [ActionsMenu](#actionsmenu) | - | - | | profileModal | 配置信息弹框 | `boolean \|` [ProfileModal](#profilemodal) | - | - | | avatar | 配置头像,用于在个人信息弹框中展示用户头像 | [AvatarProps](https://ant.design/components/avatar-cn#api) | - | - | -| onMenuItemClick | 菜单项点击事件 | `(e: NonNullable[number]) => void` | - | - | +| onMenuItemClick | 菜单项点击事件 | `MenuProps['onClick']` | - | - | | balance | 余额 | [Balance](#balance) | - | - | | availableChains | 可以连接的链列表 | [Chain](../types/index.zh-CN.md#chain)\[] | - | - | | availableWallets | 可用的钱包列表 | [Wallet](../types/index.zh-CN.md#wallet)\[] | - | - | diff --git a/packages/web3/src/connect-button/interface.ts b/packages/web3/src/connect-button/interface.ts index e0d12971e..e70a62a8c 100644 --- a/packages/web3/src/connect-button/interface.ts +++ b/packages/web3/src/connect-button/interface.ts @@ -28,7 +28,7 @@ export type ConnectButtonProps = ButtonProps & avatar?: AvatarProps; sign?: SignConfig; signBtnTextRender?: (signText?: React.ReactNode, account?: Account) => React.ReactNode; - onMenuItemClick?: (e: NonNullable[number]) => void; + onMenuItemClick?: MenuProps['onClick']; tooltip?: boolean | ConnectButtonTooltipProps; profileModal?: boolean | ProfileModalProps['modalProps']; addressPrefix?: string | false; diff --git a/packages/web3/src/connect-button/style/index.ts b/packages/web3/src/connect-button/style/index.ts index 22b44c534..0665359d7 100644 --- a/packages/web3/src/connect-button/style/index.ts +++ b/packages/web3/src/connect-button/style/index.ts @@ -22,6 +22,9 @@ const genConnectButtonStyle: GenerateStyle = (token) => { alignItems: 'center', flexDirection: 'row', }, + [`${token.antCls}-badge`]: { + marginRight: token.marginXS, + }, [`${token.componentCls}-text`]: { [`${token.antCls}-typography`]: { color: 'unset', diff --git a/packages/web3/src/connector/__tests__/basic.test.tsx b/packages/web3/src/connector/__tests__/basic.test.tsx index ecf181112..3085544c7 100644 --- a/packages/web3/src/connector/__tests__/basic.test.tsx +++ b/packages/web3/src/connector/__tests__/basic.test.tsx @@ -3,6 +3,7 @@ import { ConnectButton, Connector, type Account, + type ConnectButtonProps, type ConnectorTriggerProps, } from '@ant-design/web3'; import { metadata_MetaMask, metadata_WalletConnect } from '@ant-design/web3-assets'; @@ -11,6 +12,10 @@ import { Button } from 'antd'; import { describe, expect, it, vi } from 'vitest'; describe('Connector', () => { + function FakeButton({ children }: ConnectButtonProps) { + return ; + } + it('expect onCancelCallTest toBeCalled', async () => { const onCancelCallTest = vi.fn(); const App = () => { @@ -22,7 +27,7 @@ describe('Connector', () => { onCancel: onCancelCallTest, }} > - + children ); }; @@ -37,7 +42,7 @@ describe('Connector', () => { it('render children', () => { const App = () => ( - + children ); const { baseElement } = render(); @@ -63,7 +68,7 @@ describe('Connector', () => { const App = () => { return ( - + children ); }; @@ -397,7 +402,7 @@ describe('Connector', () => { const { baseElement } = render(); expect(baseElement.querySelector('.anticon-loading')).toBeFalsy(); fireEvent.click(baseElement.querySelector('.ant-web3-connect-button')!); - await vi.waitFor(async () => { + await vi.waitFor(() => { fireEvent.click(baseElement.querySelector('.ant-web3-connect-modal-wallet-item')!); }); await vi.waitFor(() => { diff --git a/packages/web3/src/ethereum/demos/siwe/index.tsx b/packages/web3/src/ethereum/demos/siwe/index.tsx index 6392da957..3a0f7442c 100644 --- a/packages/web3/src/ethereum/demos/siwe/index.tsx +++ b/packages/web3/src/ethereum/demos/siwe/index.tsx @@ -1,4 +1,5 @@ -import { Account, ConnectButton, Connector } from '@ant-design/web3'; +import type { Account } from '@ant-design/web3'; +import { ConnectButton, Connector, useConnection, useProvider } from '@ant-design/web3'; import { MetaMask, OkxWallet, @@ -10,27 +11,38 @@ import { import { QueryClient } from '@tanstack/react-query'; import { Button, Space } from 'antd'; import { createSiweMessage } from 'viem/siwe'; -import { http, useDisconnect } from 'wagmi'; +import { http } from 'wagmi'; import { getNonce, verifyMessage } from './mock-api'; const queryClient = new QueryClient(); const DisconnectBtn: React.FC = () => { - const { disconnect } = useDisconnect(); + const { sign } = useProvider(); + const { disconnect } = useConnection(); + return ( - + + + + {sign?.signOut && ( + + )} + ); }; @@ -71,7 +83,7 @@ const App: React.FC = () => { ]} queryClient={queryClient} > - + use custom getNFTMetadata 1`] = `
`; - -exports[`NFTImage > use custom getNFTMetadata 2`] = ` - -
-
- Test_0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B_123 -
-
- - - - Preview -
-
-
-
- -`; diff --git a/packages/web3/src/nft-image/__tests__/index.test.tsx b/packages/web3/src/nft-image/__tests__/index.test.tsx index a5a52df9c..931bacc9c 100644 --- a/packages/web3/src/nft-image/__tests__/index.test.tsx +++ b/packages/web3/src/nft-image/__tests__/index.test.tsx @@ -38,7 +38,7 @@ describe('NFTImage', () => { }} />, ); - await vi.waitFor(async () => { + await vi.waitFor(() => { const img = baseElement.querySelector('img'); expect(img).toHaveProperty('src', 'https://example.com/nft.png'); expect(img).toHaveProperty('alt', 'Test_0x21CDf0974d53a6e96eF05d7B324a9803735fFd3B_123'); diff --git a/packages/web3/src/theme/useStyle/index.ts b/packages/web3/src/theme/useStyle/index.ts index 105f05cd7..701448e63 100644 --- a/packages/web3/src/theme/useStyle/index.ts +++ b/packages/web3/src/theme/useStyle/index.ts @@ -6,7 +6,7 @@ import { TinyColor } from '@ctrl/tinycolor'; import { ConfigProvider as AntdConfigProvider, theme as AntTheme } from 'antd'; import type { GlobalToken } from 'antd'; -import { ComponentToken as ConnectModalComponentToken } from '../../connect-modal/style'; +import type { ComponentToken as ConnectModalComponentToken } from '../../connect-modal/style'; const { useToken } = AntTheme; @@ -50,13 +50,13 @@ export type Web3AliasToken = GlobalToken & { */ web3ComponentsCls: string; /** - * antd 的 className + * className for antd components * @type {string} * @example .ant */ antCls: string; /** - * 自定义 ConnectModal 的 token + * custom `ConnectModal` token * @type {Partial} * @example { hoverBg: 'red' } */ @@ -65,8 +65,8 @@ export type Web3AliasToken = GlobalToken & { /** * useStyle for css in js - * @param componentName {string} 组件的名字 - * @param styleFn {GenerateStyle} 生成样式的函数 + * @param componentName {string} component name + * @param styleFn {GenerateStyle} generate style function * @returns UseStyleResult */ export function useStyle( diff --git a/packages/web3/src/token-select/__tests__/index.test.tsx b/packages/web3/src/token-select/__tests__/index.test.tsx index 151a56eea..88b419c6a 100644 --- a/packages/web3/src/token-select/__tests__/index.test.tsx +++ b/packages/web3/src/token-select/__tests__/index.test.tsx @@ -31,8 +31,8 @@ describe('TokenSelect component', () => { expect(selectOptions[1].textContent).includes('Tether USD'); }); - it('should display correct token list when use tokenList property', () => { - const { baseElement } = render(); + it('should display correct token list when use options property', () => { + const { baseElement } = render(); fireEvent.mouseDown(baseElement.querySelector('.ant-select-selector') as Element); diff --git a/packages/web3/src/utils/__tests__/browser.test.ts b/packages/web3/src/utils/__tests__/browser.test.ts index 05b3dc8e2..56a9c7715 100644 --- a/packages/web3/src/utils/__tests__/browser.test.ts +++ b/packages/web3/src/utils/__tests__/browser.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { getPlatform, writeCopyText } from '../browser'; import { browsers, mockBrowser, readCopyText } from '../test-utils'; @@ -12,9 +12,12 @@ describe('utils/browser', () => { }); expect(getPlatform()).toBe('Other'); }); - it('writeCopyText & readCopyText', () => { + + it('writeCopyText & readCopyText', async () => { const test = 'test copy text'; writeCopyText(test); - expect(readCopyText()).resolves.toBe(test); + await vi.waitFor(async () => { + await expect(readCopyText()).resolves.toBe(test); + }); }); });