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(); + }); +});