diff --git a/.changeset/quiet-rocks-listen.md b/.changeset/quiet-rocks-listen.md
new file mode 100644
index 0000000000..048e709d7f
--- /dev/null
+++ b/.changeset/quiet-rocks-listen.md
@@ -0,0 +1,5 @@
+---
+'@rainbow-me/rainbowkit': patch
+---
+
+Fix WalletConnect disconnect after page refresh by clearing every restored connection in one click.
diff --git a/packages/rainbowkit/src/components/AccountModal/AccountModal.tsx b/packages/rainbowkit/src/components/AccountModal/AccountModal.tsx
index fa0e3babf2..9c06c98676 100644
--- a/packages/rainbowkit/src/components/AccountModal/AccountModal.tsx
+++ b/packages/rainbowkit/src/components/AccountModal/AccountModal.tsx
@@ -1,5 +1,6 @@
import React from 'react';
-import { useAccount, useDisconnect } from 'wagmi';
+import { useAccount } from 'wagmi';
+import { useDisconnectAll } from '../../hooks/useDisconnectAll';
import { useProfile } from '../../hooks/useProfile';
import { Dialog } from '../Dialog/Dialog';
import { DialogContent } from '../Dialog/DialogContent';
@@ -16,7 +17,7 @@ export function AccountModal({ onClose, open }: AccountModalProps) {
address,
includeBalance: open,
});
- const { disconnect } = useDisconnect();
+ const disconnectAll = useDisconnectAll();
if (!address) {
return null;
@@ -35,7 +36,9 @@ export function AccountModal({ onClose, open }: AccountModalProps) {
ensName={ensName}
balance={balance}
onClose={onClose}
- onDisconnect={disconnect}
+ onDisconnect={() => {
+ void disconnectAll();
+ }}
/>
diff --git a/packages/rainbowkit/src/components/ChainModal/ChainModal.tsx b/packages/rainbowkit/src/components/ChainModal/ChainModal.tsx
index b143470a08..5ccffc1055 100644
--- a/packages/rainbowkit/src/components/ChainModal/ChainModal.tsx
+++ b/packages/rainbowkit/src/components/ChainModal/ChainModal.tsx
@@ -1,5 +1,6 @@
import React, { useContext, useState } from 'react';
-import { useAccount, useConfig, useDisconnect, useSwitchChain } from 'wagmi';
+import { useAccount, useConfig, useSwitchChain } from 'wagmi';
+import { useDisconnectAll } from '../../hooks/useDisconnectAll';
import { isMobile } from '../../utils/isMobile';
import { Box } from '../Box/Box';
import { CloseButton } from '../CloseButton/CloseButton';
@@ -44,7 +45,7 @@ export function ChainModal({ onClose, open }: ChainModalProps) {
const { i18n } = useContext(I18nContext);
- const { disconnect } = useDisconnect();
+ const disconnectAll = useDisconnectAll();
const titleId = 'rk_chain_modal_title';
const mobile = isMobile();
const isCurrentChainSupported = chains.some((chain) => chain.id === chainId);
@@ -115,7 +116,9 @@ export function ChainModal({ onClose, open }: ChainModalProps) {
<>
disconnect()}
+ onClick={() => {
+ void disconnectAll();
+ }}
testId="chain-option-disconnect"
>
{
onClose();
- disconnect();
- }, [onClose, disconnect]);
+ void disconnectAll();
+ }, [onClose, disconnectAll]);
const onConnectModalCancel = React.useCallback(() => {
// We use this for the WalletButton. If the QR code shows up and
// the user closes it, we need to know the wallet isn't connecting anymore.
// So if it's connecting, we disconnect it.
- if (isConnecting) disconnect();
+ if (isConnecting) void disconnectAll();
onClose();
- }, [onClose, disconnect, isConnecting]);
+ }, [onClose, disconnectAll, isConnecting]);
if (connectionStatus === 'disconnected') {
return (
diff --git a/packages/rainbowkit/src/hooks/useDisconnectAll.test.tsx b/packages/rainbowkit/src/hooks/useDisconnectAll.test.tsx
new file mode 100644
index 0000000000..28652f2cee
--- /dev/null
+++ b/packages/rainbowkit/src/hooks/useDisconnectAll.test.tsx
@@ -0,0 +1,184 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import React from 'react';
+import type { Address } from 'viem';
+import { http } from 'viem';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import {
+ createConfig,
+ useAccount,
+ useConnections,
+ useDisconnect,
+ WagmiProvider,
+} from 'wagmi';
+import { mainnet } from 'wagmi/chains';
+import { connectorsForWallets } from '../wallets/connectorsForWallets';
+import {
+ metaMaskWallet,
+ rainbowWallet,
+ walletConnectWallet,
+} from '../wallets/walletConnectors';
+import { useDisconnectAll } from './useDisconnectAll';
+
+const exampleProjectId = '21fef48091f12692cad574a6f7753643';
+const account = '0x1111111111111111111111111111111111111111' as Address;
+
+function createWalletConnectConfig() {
+ return createConfig({
+ chains: [mainnet],
+ connectors: connectorsForWallets(
+ [
+ {
+ groupName: 'Popular',
+ wallets: [rainbowWallet, metaMaskWallet, walletConnectWallet],
+ },
+ ],
+ {
+ projectId: exampleProjectId,
+ appName: 'rainbowkit.com',
+ appUrl: 'https://rainbowkit.com',
+ },
+ ),
+ transports: {
+ [mainnet.id]: http(),
+ },
+ ssr: true,
+ });
+}
+
+function seedRestoredWalletConnectConnections(
+ config: ReturnType,
+) {
+ const walletConnectConnectors = config.connectors.filter(
+ (connector) => connector.id === 'walletConnect',
+ );
+
+ expect(walletConnectConnectors.length).toBeGreaterThan(1);
+
+ const [currentConnector] = walletConnectConnectors;
+ if (!currentConnector) {
+ throw new Error('Expected at least one WalletConnect connector');
+ }
+
+ for (const connector of walletConnectConnectors) {
+ connector.disconnect = vi.fn().mockResolvedValue(undefined);
+ }
+
+ const connections = new Map(
+ walletConnectConnectors.map((connector) => [
+ connector.uid,
+ {
+ accounts: [account] as const,
+ chainId: mainnet.id,
+ connector,
+ },
+ ]),
+ );
+
+ config.setState({
+ chainId: mainnet.id,
+ connections,
+ current: currentConnector.uid,
+ status: 'connected',
+ });
+
+ return walletConnectConnectors.length;
+}
+
+function DisconnectHarness({ mode }: { mode: 'current' | 'all' }) {
+ const { status, isConnected } = useAccount();
+ const connections = useConnections();
+ const { disconnect } = useDisconnect();
+ const disconnectAll = useDisconnectAll();
+
+ return (
+
+
{status}
+
{String(isConnected)}
+
{connections.length}
+
+
+ );
+}
+
+describe('useDisconnectAll', () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+
+ afterEach(() => {
+ queryClient.clear();
+ });
+
+ it('shows why a single wagmi disconnect leaves restored walletconnect sessions connected', async () => {
+ const config = createWalletConnectConfig();
+ const restoredCount = seedRestoredWalletConnectConnections(config);
+
+ render(
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('connection-count').textContent).toBe(
+ String(restoredCount),
+ );
+ expect(screen.getByTestId('connected').textContent).toBe('true');
+
+ fireEvent.click(screen.getByTestId('disconnect'));
+
+ await waitFor(() => {
+ expect(screen.getByTestId('connection-count').textContent).toBe(
+ String(restoredCount - 1),
+ );
+ });
+
+ // This is the #2401 failure mode: one Disconnect click is not enough.
+ expect(screen.getByTestId('connected').textContent).toBe('true');
+ expect(screen.getByTestId('status').textContent).toBe('connected');
+ });
+
+ it('clears every restored walletconnect connection in one click', async () => {
+ const config = createWalletConnectConfig();
+ const restoredCount = seedRestoredWalletConnectConnections(config);
+
+ render(
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('connection-count').textContent).toBe(
+ String(restoredCount),
+ );
+
+ fireEvent.click(screen.getByTestId('disconnect'));
+
+ await waitFor(() => {
+ expect(screen.getByTestId('connection-count').textContent).toBe('0');
+ expect(screen.getByTestId('connected').textContent).toBe('false');
+ expect(screen.getByTestId('status').textContent).toBe('disconnected');
+ });
+ });
+});
diff --git a/packages/rainbowkit/src/hooks/useDisconnectAll.ts b/packages/rainbowkit/src/hooks/useDisconnectAll.ts
new file mode 100644
index 0000000000..399641e6e3
--- /dev/null
+++ b/packages/rainbowkit/src/hooks/useDisconnectAll.ts
@@ -0,0 +1,17 @@
+import { useCallback } from 'react';
+import { useConfig, useDisconnect } from 'wagmi';
+
+// RainbowKit registers multiple WalletConnect connectors. After refresh,
+// reconnect can restore more than one, and wagmi disconnect only clears current.
+export function useDisconnectAll() {
+ const config = useConfig();
+ const { disconnectAsync } = useDisconnect();
+
+ return useCallback(async () => {
+ const connections = Array.from(config.state.connections.values());
+
+ for (const { connector } of connections) {
+ await disconnectAsync({ connector });
+ }
+ }, [config, disconnectAsync]);
+}