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' ?
: icon}
-
- );
- };
+ return (
+
+ {typeof icon === 'string' ?
: 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`] = `
-
-
-

-
-
-
-