diff --git a/examples/next-wagmi-app-router/src/app/layout.tsx b/examples/next-wagmi-app-router/src/app/layout.tsx index 9b363bc6b5..5492f2d11b 100755 --- a/examples/next-wagmi-app-router/src/app/layout.tsx +++ b/examples/next-wagmi-app-router/src/app/layout.tsx @@ -1,5 +1,7 @@ import { Metadata } from 'next' +import { headers } from 'next/headers' +import { wagmiAdapter } from '@/config' import ContextProvider from '@/context' import './globals.css' @@ -33,11 +35,18 @@ export const metadata: Metadata = { } } -export default function RootLayout({ children }: { children: React.ReactNode }) { +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const cookies = (await headers()).get('cookie') + + // Reset SSR state when no cookies to prevent cross-request state leakage + if (!cookies) { + wagmiAdapter.resetSSRState() + } + return ( - {children} + {children} ) diff --git a/examples/next-wagmi-app-router/src/config/index.ts b/examples/next-wagmi-app-router/src/config/index.ts index 9c4a0aab22..0a49d67d6f 100755 --- a/examples/next-wagmi-app-router/src/config/index.ts +++ b/examples/next-wagmi-app-router/src/config/index.ts @@ -22,7 +22,8 @@ export const networks = [mainnet, polygon, arbitrum, optimism] as [ // Setup wagmi adapter export const wagmiAdapter = new WagmiAdapter({ networks, - projectId + projectId, + ssr: true }) // Create modal diff --git a/packages/adapters/wagmi/src/client.ts b/packages/adapters/wagmi/src/client.ts index 745f07a8bd..c907a93005 100644 --- a/packages/adapters/wagmi/src/client.ts +++ b/packages/adapters/wagmi/src/client.ts @@ -116,6 +116,46 @@ export class WagmiAdapter extends AdapterBlueprint { this.setupWatchers() } + /** + * Resets the wagmi config state for SSR (Server-Side Rendering). + * + * In Next.js App Router with SSR, the WagmiAdapter is typically created as a module-level + * singleton. This can cause cross-request state leakage where one user's wallet connection + * state bleeds into another user's server-rendered HTML. + * + * Call this method before each SSR request when `cookieToInitialState` returns undefined + * (i.e., when the user has no wallet cookies) to reset the config state and prevent + * showing another user's wallet address. + * + * @example + * ```tsx + * // In your layout.tsx (server component) + * const cookies = (await headers()).get('cookie') + * const initialState = cookieToInitialState(wagmiAdapter.wagmiConfig, cookies) + * + * // Reset state if no cookies to prevent state leakage + * if (!initialState) { + * wagmiAdapter.resetSSRState() + * } + * ``` + */ + public resetSSRState(): void { + if (!this.wagmiConfig) { + return + } + + // Reset the wagmi config state to disconnected + this.wagmiConfig.setState(state => ({ + ...state, + connections: new Map(), + current: null, + status: 'disconnected' + })) + + // Clear the connections map + this.wagmiConfig.state.connections.clear() + } + override async getAccounts( params: AdapterBlueprint.GetAccountsParams ): Promise { diff --git a/packages/adapters/wagmi/src/tests/client.test.ts b/packages/adapters/wagmi/src/tests/client.test.ts index 3345fd07f6..9df7d6c46f 100644 --- a/packages/adapters/wagmi/src/tests/client.test.ts +++ b/packages/adapters/wagmi/src/tests/client.test.ts @@ -1734,3 +1734,105 @@ describe('WagmiAdapter - BaseAccount lazy initialization', () => { expect(result.provider).toBe(providedProvider as any) }) }) + +describe('WagmiAdapter - resetSSRState', () => { + it('should reset wagmi config state to disconnected', () => { + const mockConnections = new Map([ + ['connector1', { connector: { id: 'connector1' }, accounts: ['0x123'] }] + ]) + + const setStateSpy = vi.fn((fn: (state: any) => any) => { + const newState = fn({ + connections: mockConnections, + current: 'connector1', + status: 'connected' + }) + expect(newState.connections).toBeInstanceOf(Map) + expect(newState.connections.size).toBe(0) + expect(newState.current).toBeNull() + expect(newState.status).toBe('disconnected') + }) + + const mockConfig = { + chains: mockCaipNetworks, + connectors: [], + state: { + connections: mockConnections + }, + setState: setStateSpy, + _internal: { + connectors: { + setup: vi.fn(connector => connector), + setState: vi.fn() + } + } + } as unknown as Config + + vi.spyOn(wagmiCore, 'createConfig').mockReturnValue(mockConfig) + + const adapter = new WagmiAdapter({ + networks: mockNetworks, + projectId: mockProjectId, + ssr: true + }) + + adapter.wagmiConfig = mockConfig + + adapter.resetSSRState() + + expect(setStateSpy).toHaveBeenCalled() + expect(mockConnections.size).toBe(0) + }) + + it('should return early when wagmiConfig is not set', () => { + const adapter = new WagmiAdapter({ + networks: mockNetworks, + projectId: mockProjectId, + ssr: true + }) + + // Set wagmiConfig to undefined to test early return + adapter.wagmiConfig = undefined as unknown as Config + + // This should not throw + expect(() => adapter.resetSSRState()).not.toThrow() + }) + + it('should clear connections map after setState', () => { + const mockConnections = new Map([ + ['connector1', { connector: { id: 'connector1' }, accounts: ['0x123'] }], + ['connector2', { connector: { id: 'connector2' }, accounts: ['0x456'] }] + ]) + + const mockConfig = { + chains: mockCaipNetworks, + connectors: [], + state: { + connections: mockConnections + }, + setState: vi.fn(), + _internal: { + connectors: { + setup: vi.fn(connector => connector), + setState: vi.fn() + } + } + } as unknown as Config + + vi.spyOn(wagmiCore, 'createConfig').mockReturnValue(mockConfig) + + const adapter = new WagmiAdapter({ + networks: mockNetworks, + projectId: mockProjectId, + ssr: true + }) + + adapter.wagmiConfig = mockConfig + + expect(mockConnections.size).toBe(2) + + adapter.resetSSRState() + + expect(mockConnections.size).toBe(0) + }) +})