Skip to content

Commit 6e37b3b

Browse files
committed
fix(wallet-adapter-react): support async autoConnect prop values
1 parent 876d532 commit 6e37b3b

File tree

5 files changed

+114
-2
lines changed

5 files changed

+114
-2
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@aptos-labs/wallet-adapter-react": patch
3+
---
4+
5+
fix: support async autoConnect prop values
6+
7+
Previously, if `autoConnect` started as `false` and later became `true` (e.g., after fetching user preferences from an API), the auto-connect would never trigger because the attempt was marked as "done" on the first render.
8+
9+
Now, the auto-connect attempt is only marked as complete when `autoConnect` is actually truthy, allowing dApps to set `autoConnect` asynchronously.
10+

apps/nextjs-example/src/components/AutoConnectProvider.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createContext,
77
useContext,
88
useEffect,
9+
useRef,
910
useState,
1011
} from "react";
1112

@@ -28,22 +29,29 @@ export const AutoConnectProvider: FC<{ children: ReactNode }> = ({
2829
children,
2930
}) => {
3031
const [autoConnect, setAutoConnect] = useState(false);
32+
// Track if we've read from localStorage to avoid deleting it on initial mount
33+
const hasInitializedRef = useRef(false);
3134

3235
useEffect(() => {
3336
// Wait until the app hydrates before populating `autoConnect` from local storage
3437
try {
3538
const isAutoConnect = localStorage.getItem(
3639
AUTO_CONNECT_LOCAL_STORAGE_KEY,
3740
);
41+
hasInitializedRef.current = true;
3842
if (isAutoConnect) return setAutoConnect(JSON.parse(isAutoConnect));
3943
} catch (e) {
44+
hasInitializedRef.current = true;
4045
if (typeof window !== "undefined") {
4146
console.error(e);
4247
}
4348
}
4449
}, []);
4550

4651
useEffect(() => {
52+
// Don't write to localStorage until we've read the initial value
53+
if (!hasInitializedRef.current) return;
54+
4755
try {
4856
if (!autoConnect) {
4957
localStorage.removeItem(AUTO_CONNECT_LOCAL_STORAGE_KEY);

apps/nextjs-x-chain/src/components/AutoConnectProvider.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createContext,
77
useContext,
88
useEffect,
9+
useRef,
910
useState,
1011
} from "react";
1112

@@ -28,22 +29,29 @@ export const AutoConnectProvider: FC<{ children: ReactNode }> = ({
2829
children,
2930
}) => {
3031
const [autoConnect, setAutoConnect] = useState(false);
32+
// Track if we've read from localStorage to avoid deleting it on initial mount
33+
const hasInitializedRef = useRef(false);
3134

3235
useEffect(() => {
3336
// Wait until the app hydrates before populating `autoConnect` from local storage
3437
try {
3538
const isAutoConnect = localStorage.getItem(
3639
AUTO_CONNECT_LOCAL_STORAGE_KEY,
3740
);
41+
hasInitializedRef.current = true;
3842
if (isAutoConnect) return setAutoConnect(JSON.parse(isAutoConnect));
3943
} catch (e) {
44+
hasInitializedRef.current = true;
4045
if (typeof window !== "undefined") {
4146
console.error(e);
4247
}
4348
}
4449
}, []);
4550

4651
useEffect(() => {
52+
// Don't write to localStorage until we've read the initial value
53+
if (!hasInitializedRef.current) return;
54+
4755
try {
4856
if (!autoConnect) {
4957
localStorage.removeItem(AUTO_CONNECT_LOCAL_STORAGE_KEY);

packages/wallet-adapter-react/src/WalletProvider.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,17 @@ export const AptosWalletAdapterProvider: FC<AptosWalletProviderProps> = ({
9494
if (didAttemptAutoConnectRef.current || !walletCore?.wallets.length) {
9595
return;
9696
}
97-
didAttemptAutoConnectRef.current = true;
9897

99-
// If auto connect is not set or is false, ignore the attempt
98+
// If auto connect is not set or is false, don't mark as attempted yet
99+
// This allows retry when autoConnect becomes true asynchronously
100100
if (!autoConnect) {
101101
setIsLoading(false);
102102
return;
103103
}
104104

105+
// Only mark as attempted when autoConnect is truthy
106+
didAttemptAutoConnectRef.current = true;
107+
105108
// Make sure the user has a previously connected wallet
106109
const walletName = localStorage.getItem("AptosWalletName");
107110
if (!walletName) {

packages/wallet-adapter-react/tests/WalletProvider.test.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,89 @@ describe("AptosWalletAdapterProvider", () => {
178178

179179
expect(mockWalletCore.connect).not.toHaveBeenCalled();
180180
});
181+
182+
it("should auto-connect when autoConnect changes from false to true asynchronously", async () => {
183+
// Store a wallet name to enable auto-connect
184+
localStorage.setItem("AptosWalletName", "TestWallet");
185+
186+
// Component that simulates async autoConnect prop
187+
function AsyncAutoConnectWrapper() {
188+
const [autoConnect, setAutoConnect] = React.useState(false);
189+
190+
React.useEffect(() => {
191+
// Simulate async operation (e.g., fetching user preferences)
192+
const timer = setTimeout(() => {
193+
setAutoConnect(true);
194+
}, 50);
195+
return () => clearTimeout(timer);
196+
}, []);
197+
198+
return (
199+
<AptosWalletAdapterProvider autoConnect={autoConnect} disableTelemetry>
200+
<TestConsumer />
201+
</AptosWalletAdapterProvider>
202+
);
203+
}
204+
205+
render(<AsyncAutoConnectWrapper />);
206+
207+
// Initially, connect should not have been called
208+
expect(mockWalletCore.connect).not.toHaveBeenCalled();
209+
210+
// Wait for autoConnect to become true and trigger connect
211+
await waitFor(
212+
() => {
213+
expect(mockWalletCore.connect).toHaveBeenCalledWith("TestWallet");
214+
},
215+
{ timeout: 3000 }
216+
);
217+
});
218+
219+
it("should only attempt auto-connect once when autoConnect becomes true", async () => {
220+
localStorage.setItem("AptosWalletName", "TestWallet");
221+
222+
function MultipleUpdateWrapper() {
223+
const [autoConnect, setAutoConnect] = React.useState(false);
224+
const [, forceUpdate] = React.useState(0);
225+
226+
React.useEffect(() => {
227+
// Set autoConnect to true
228+
const timer1 = setTimeout(() => setAutoConnect(true), 30);
229+
// Trigger additional re-renders after autoConnect is true
230+
const timer2 = setTimeout(() => forceUpdate((n) => n + 1), 80);
231+
const timer3 = setTimeout(() => forceUpdate((n) => n + 1), 130);
232+
return () => {
233+
clearTimeout(timer1);
234+
clearTimeout(timer2);
235+
clearTimeout(timer3);
236+
};
237+
}, []);
238+
239+
return (
240+
<AptosWalletAdapterProvider autoConnect={autoConnect} disableTelemetry>
241+
<TestConsumer />
242+
</AptosWalletAdapterProvider>
243+
);
244+
}
245+
246+
render(<MultipleUpdateWrapper />);
247+
248+
// Wait for auto-connect to be called
249+
await waitFor(
250+
() => {
251+
expect(mockWalletCore.connect).toHaveBeenCalled();
252+
},
253+
{ timeout: 3000 }
254+
);
255+
256+
// Wait for additional re-renders to complete
257+
await act(async () => {
258+
await new Promise((resolve) => setTimeout(resolve, 200));
259+
});
260+
261+
// Should only be called once
262+
expect(mockWalletCore.connect).toHaveBeenCalledTimes(1);
263+
});
181264
});
182265

183266
describe("event handlers", () => {

0 commit comments

Comments
 (0)