diff --git a/src/components/common/ConnectWalletButton.tsx b/src/components/common/ConnectWalletButton.tsx
index e7cc474..c72d852 100644
--- a/src/components/common/ConnectWalletButton.tsx
+++ b/src/components/common/ConnectWalletButton.tsx
@@ -1,4 +1,16 @@
+import { useState } from 'react';
import { useAccount, useConnect, useDisconnect } from 'wagmi';
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
import { shortenAddress } from '@/lib/web3/format';
import {
WALLET_CONNECTION_AD_BLOCKER_MESSAGE,
@@ -6,6 +18,7 @@ import {
} from '@/hooks/useWalletConnectionStallDetection';
function ConnectWalletButton() {
+ const [showDisconnectDialog, setShowDisconnectDialog] = useState(false);
const { address, isConnected } = useAccount();
const { connect, connectors, error, isPending } = useConnect();
const { disconnect } = useDisconnect();
@@ -18,13 +31,43 @@ function ConnectWalletButton() {
if (isConnected && address) {
return (
-
+
+
+
+
+
+ Disconnect wallet?
+
+ Disconnecting clears your current wallet session and any
+ pending wallet state. You will need to reconnect to continue.
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/components/common/__tests__/ConnectWalletButton.test.tsx b/src/components/common/__tests__/ConnectWalletButton.test.tsx
new file mode 100644
index 0000000..261204f
--- /dev/null
+++ b/src/components/common/__tests__/ConnectWalletButton.test.tsx
@@ -0,0 +1,81 @@
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+
+import ConnectWalletButton from '@/components/common/ConnectWalletButton';
+import { useAccount, useConnect, useDisconnect } from 'wagmi';
+
+vi.mock('wagmi', () => ({
+ useAccount: vi.fn(),
+ useConnect: vi.fn(),
+ useDisconnect: vi.fn(),
+}));
+
+const mockUseAccount = vi.mocked(useAccount);
+const mockUseConnect = vi.mocked(useConnect);
+const mockUseDisconnect = vi.mocked(useDisconnect);
+
+describe('ConnectWalletButton wallet disconnect confirmation', () => {
+ function renderConnectedWallet(disconnect = vi.fn()) {
+ mockUseAccount.mockReturnValue({
+ address: '0x1234567890abcdef1234567890abcdef12345678',
+ isConnected: true,
+ } as ReturnType);
+ mockUseConnect.mockReturnValue({
+ connect: vi.fn(),
+ connectors: [],
+ error: null,
+ isPending: false,
+ } as unknown as ReturnType);
+ mockUseDisconnect.mockReturnValue({
+ disconnect,
+ } as unknown as ReturnType);
+
+ render();
+
+ return { disconnect };
+ }
+
+ it('opens a confirmation dialog before disconnecting', () => {
+ const { disconnect } = renderConnectedWallet();
+
+ fireEvent.click(screen.getByRole('button', { name: /0x1234/i }));
+
+ expect(
+ screen.getByRole('dialog', { name: /disconnect wallet/i })
+ ).toBeInTheDocument();
+ expect(disconnect).not.toHaveBeenCalled();
+ });
+
+ it('disconnects when the confirmation action is clicked', () => {
+ const { disconnect } = renderConnectedWallet();
+
+ fireEvent.click(screen.getByRole('button', { name: /0x1234/i }));
+ fireEvent.click(screen.getByRole('button', { name: /^disconnect$/i }));
+
+ expect(disconnect).toHaveBeenCalledTimes(1);
+ });
+
+ it('cancels without disconnecting', async () => {
+ const { disconnect } = renderConnectedWallet();
+
+ fireEvent.click(screen.getByRole('button', { name: /0x1234/i }));
+ fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
+
+ await waitFor(() => {
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
+ expect(disconnect).not.toHaveBeenCalled();
+ });
+
+ it('dismisses with Escape without disconnecting', async () => {
+ const { disconnect } = renderConnectedWallet();
+
+ fireEvent.click(screen.getByRole('button', { name: /0x1234/i }));
+ fireEvent.keyDown(document, { key: 'Escape' });
+
+ await waitFor(() => {
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
+ expect(disconnect).not.toHaveBeenCalled();
+ });
+});