Skip to content

Commit d6ff9bc

Browse files
authored
Merge pull request #407 from sochima2/main
Add helper for detecting ad blocker interference with wallet connection
2 parents dbad403 + 7dc9326 commit d6ff9bc

4 files changed

Lines changed: 160 additions & 0 deletions

File tree

src/components/common/ConnectWalletButton.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { useAccount, useConnect, useDisconnect } from 'wagmi';
22
import { shortenAddress } from '@/lib/web3/format';
3+
import {
4+
WALLET_CONNECTION_AD_BLOCKER_MESSAGE,
5+
useWalletConnectionStallDetection,
6+
} from '@/hooks/useWalletConnectionStallDetection';
37

48
function ConnectWalletButton() {
59
const { address, isConnected } = useAccount();
610
const { connect, connectors, error, isPending } = useConnect();
711
const { disconnect } = useDisconnect();
812

913
const primaryConnector = connectors[0];
14+
const showAdBlockerSuggestion = useWalletConnectionStallDetection({
15+
isAwaitingWalletResponse: isPending,
16+
hasWalletResponse: isConnected || Boolean(error),
17+
});
1018

1119
if (isConnected && address) {
1220
return (
@@ -35,6 +43,11 @@ function ConnectWalletButton() {
3543
{error ? (
3644
<p className="text-sm text-red-600">{error.message}</p>
3745
) : null}
46+
{showAdBlockerSuggestion ? (
47+
<p role="status" className="max-w-sm text-sm text-amber-700">
48+
{WALLET_CONNECTION_AD_BLOCKER_MESSAGE}
49+
</p>
50+
) : null}
3851
</div>
3952
);
4053
}

src/components/common/WalletConnectCalloutBanner.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { useState } from 'react';
22
import { Wallet } from 'lucide-react';
33
import { useAccount, useConnect, useReconnect } from 'wagmi';
44
import { cn } from '@/lib/utils';
5+
import {
6+
WALLET_CONNECTION_AD_BLOCKER_MESSAGE,
7+
useWalletConnectionStallDetection,
8+
} from '@/hooks/useWalletConnectionStallDetection';
59
import showToast from '@/utils/toast.util';
610

711
interface WalletConnectCalloutBannerProps {
@@ -21,6 +25,10 @@ const WalletConnectCalloutBanner: React.FC<WalletConnectCalloutBannerProps> = ({
2125
const [isReconnecting, setIsReconnecting] = useState(false);
2226

2327
const retryConnector = reconnectConnectors[0] ?? connectConnectors[0];
28+
const showAdBlockerSuggestion = useWalletConnectionStallDetection({
29+
isAwaitingWalletResponse: isReconnecting,
30+
hasWalletResponse: isConnected,
31+
});
2432

2533
const handleReconnect = async () => {
2634
if (isReconnecting) {
@@ -95,6 +103,11 @@ const WalletConnectCalloutBanner: React.FC<WalletConnectCalloutBannerProps> = ({
95103
</button>
96104
</div>
97105
</div>
106+
{showAdBlockerSuggestion ? (
107+
<p role="status" className="mt-3 text-xs text-amber-100/85">
108+
{WALLET_CONNECTION_AD_BLOCKER_MESSAGE}
109+
</p>
110+
) : null}
98111
</div>
99112
);
100113
};
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
import {
4+
WALLET_CONNECTION_STALL_TIMEOUT_MS,
5+
useWalletConnectionStallDetection,
6+
} from '@/hooks/useWalletConnectionStallDetection';
7+
8+
describe('useWalletConnectionStallDetection', () => {
9+
beforeEach(() => {
10+
vi.useFakeTimers();
11+
});
12+
13+
afterEach(() => {
14+
vi.useRealTimers();
15+
});
16+
17+
it('reports a stalled wallet connection after the named timeout elapses', () => {
18+
const { result } = renderHook(() =>
19+
useWalletConnectionStallDetection({
20+
isAwaitingWalletResponse: true,
21+
})
22+
);
23+
24+
expect(result.current).toBe(false);
25+
26+
act(() => {
27+
vi.advanceTimersByTime(WALLET_CONNECTION_STALL_TIMEOUT_MS - 1);
28+
});
29+
30+
expect(result.current).toBe(false);
31+
32+
act(() => {
33+
vi.advanceTimersByTime(1);
34+
});
35+
36+
expect(result.current).toBe(true);
37+
});
38+
39+
it('does not report a stall when the connection succeeds before the timeout', () => {
40+
const { result, rerender } = renderHook(
41+
({ hasWalletResponse, isAwaitingWalletResponse }) =>
42+
useWalletConnectionStallDetection({
43+
hasWalletResponse,
44+
isAwaitingWalletResponse,
45+
}),
46+
{
47+
initialProps: {
48+
hasWalletResponse: false,
49+
isAwaitingWalletResponse: true,
50+
},
51+
}
52+
);
53+
54+
act(() => {
55+
vi.advanceTimersByTime(WALLET_CONNECTION_STALL_TIMEOUT_MS / 2);
56+
});
57+
58+
rerender({ hasWalletResponse: true, isAwaitingWalletResponse: false });
59+
60+
act(() => {
61+
vi.advanceTimersByTime(WALLET_CONNECTION_STALL_TIMEOUT_MS);
62+
});
63+
64+
expect(result.current).toBe(false);
65+
});
66+
67+
it('does not report a stall when the connection fails before the timeout', () => {
68+
const { result, rerender } = renderHook(
69+
({ hasWalletResponse, isAwaitingWalletResponse }) =>
70+
useWalletConnectionStallDetection({
71+
hasWalletResponse,
72+
isAwaitingWalletResponse,
73+
}),
74+
{
75+
initialProps: {
76+
hasWalletResponse: false,
77+
isAwaitingWalletResponse: true,
78+
},
79+
}
80+
);
81+
82+
rerender({ hasWalletResponse: true, isAwaitingWalletResponse: true });
83+
84+
act(() => {
85+
vi.advanceTimersByTime(WALLET_CONNECTION_STALL_TIMEOUT_MS);
86+
});
87+
88+
expect(result.current).toBe(false);
89+
});
90+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useEffect, useState } from 'react';
2+
3+
export const WALLET_CONNECTION_STALL_TIMEOUT_MS = 10_000;
4+
5+
export const WALLET_CONNECTION_AD_BLOCKER_MESSAGE =
6+
'No wallet response yet. If your wallet prompt did not open, an ad blocker or privacy extension may be blocking the wallet script. Disable it for this site, then try connecting again.';
7+
8+
interface UseWalletConnectionStallDetectionOptions {
9+
/** True while the app is waiting for a wallet connector to respond. */
10+
isAwaitingWalletResponse: boolean;
11+
/** True once the wallet attempt has connected, failed, or otherwise returned. */
12+
hasWalletResponse?: boolean;
13+
/** Override mainly for tests or specialized wallet flows. */
14+
timeoutMs?: number;
15+
}
16+
17+
/**
18+
* Flags wallet connection attempts that remain pending long enough to suggest
19+
* browser extensions may have blocked the injected wallet script.
20+
*/
21+
export function useWalletConnectionStallDetection({
22+
isAwaitingWalletResponse,
23+
hasWalletResponse = false,
24+
timeoutMs = WALLET_CONNECTION_STALL_TIMEOUT_MS,
25+
}: UseWalletConnectionStallDetectionOptions): boolean {
26+
const [hasStalled, setHasStalled] = useState(false);
27+
28+
useEffect(() => {
29+
if (!isAwaitingWalletResponse || hasWalletResponse) {
30+
setHasStalled(false);
31+
return undefined;
32+
}
33+
34+
const timeoutId = window.setTimeout(() => {
35+
setHasStalled(true);
36+
}, timeoutMs);
37+
38+
return () => {
39+
window.clearTimeout(timeoutId);
40+
};
41+
}, [hasWalletResponse, isAwaitingWalletResponse, timeoutMs]);
42+
43+
return hasStalled;
44+
}

0 commit comments

Comments
 (0)