Skip to content

Commit 810f319

Browse files
committed
[SDK] onTimeout for useAutoConnect | TOOL-2673 (#5879)
TOOL-2673 <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces an `onTimeout` callback to the `useAutoConnect` hook, enhancing its functionality by allowing users to define custom behavior when a connection attempt times out. It also includes tests for the new feature and refactors some wallet connection logic. ### Detailed summary - Added `onTimeout` callback to `useAutoConnect`. - Updated `useAutoConnectCore` to handle wallet connections with the new `onTimeout` feature. - Refactored `handleWalletConnection` function to improve clarity. - Added unit tests for `getUrlToken` covering various scenarios. - Updated test configurations for better matching of test files. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 2a9d996 commit 810f319

File tree

6 files changed

+331
-13
lines changed

6 files changed

+331
-13
lines changed

.changeset/little-beds-dress.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Add onTimeout callback to useAutoConnect

packages/thirdweb/src/react/core/hooks/connection/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,9 @@ export type AutoConnectProps = {
113113
* Optional chain to autoconnect to
114114
*/
115115
chain?: Chain;
116+
117+
/**
118+
* Callback to be called when the connection is timeout-ed
119+
*/
120+
onTimeout?: () => void;
116121
};

packages/thirdweb/src/react/core/hooks/wallets/useAutoConnect.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"use client";
22

33
import { useQuery } from "@tanstack/react-query";
4+
import type { Chain } from "../../../../chains/types.js";
5+
import type { ThirdwebClient } from "../../../../client/client.js";
46
import type { AsyncStorage } from "../../../../utils/storage/AsyncStorage.js";
57
import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js";
68
import { ClientScopedStorage } from "../../../../wallets/in-app/core/authentication/client-scoped-storage.js";
9+
import type { AuthStoredTokenWithCookieReturnType } from "../../../../wallets/in-app/core/authentication/types.js";
710
import { getUrlToken } from "../../../../wallets/in-app/web/lib/get-url-token.js";
811
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
912
import {
@@ -83,14 +86,6 @@ export function useAutoConnectCore(
8386
const lastConnectedChain =
8487
(await getLastConnectedChain(storage)) || props.chain;
8588

86-
async function handleWalletConnection(wallet: Wallet) {
87-
return wallet.autoConnect({
88-
client: props.client,
89-
chain: lastConnectedChain ?? undefined,
90-
authResult,
91-
});
92-
}
93-
9489
const availableWallets = [...wallets, ...(getInstalledWallets?.() ?? [])];
9590
const activeWallet =
9691
lastActiveWalletId &&
@@ -100,9 +95,22 @@ export function useAutoConnectCore(
10095
if (activeWallet) {
10196
try {
10297
setConnectionStatus("connecting"); // only set connecting status if we are connecting the last active EOA
103-
await timeoutPromise(handleWalletConnection(activeWallet), {
104-
ms: timeout,
105-
message: `AutoConnect timeout: ${timeout}ms limit exceeded.`,
98+
await timeoutPromise(
99+
handleWalletConnection({
100+
wallet: activeWallet,
101+
client: props.client,
102+
lastConnectedChain,
103+
authResult,
104+
}),
105+
{
106+
ms: timeout,
107+
message: `AutoConnect timeout: ${timeout}ms limit exceeded.`,
108+
},
109+
).catch((err) => {
110+
console.warn(err.message);
111+
if (props.onTimeout) {
112+
props.onTimeout();
113+
}
106114
});
107115

108116
// connected wallet could be activeWallet or smart wallet
@@ -138,7 +146,12 @@ export function useAutoConnectCore(
138146

139147
for (const wallet of otherWallets) {
140148
try {
141-
await handleWalletConnection(wallet);
149+
await handleWalletConnection({
150+
wallet,
151+
client: props.client,
152+
lastConnectedChain,
153+
authResult,
154+
});
142155
manager.addConnectedWallet(wallet);
143156
} catch {
144157
// no-op
@@ -158,3 +171,19 @@ export function useAutoConnectCore(
158171

159172
return query;
160173
}
174+
175+
/**
176+
* @internal
177+
*/
178+
export async function handleWalletConnection(props: {
179+
wallet: Wallet;
180+
client: ThirdwebClient;
181+
authResult: AuthStoredTokenWithCookieReturnType | undefined;
182+
lastConnectedChain: Chain | undefined;
183+
}) {
184+
return props.wallet.autoConnect({
185+
client: props.client,
186+
chain: props.lastConnectedChain,
187+
authResult: props.authResult,
188+
});
189+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { renderHook, waitFor } from "@testing-library/react";
2+
import type { ReactNode } from "react";
3+
import { describe, expect, it, vi } from "vitest";
4+
import { MockStorage } from "~test/mocks/storage.js";
5+
import { TEST_CLIENT } from "~test/test-clients.js";
6+
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
7+
import { createWalletAdapter } from "../../../../adapters/wallet-adapter.js";
8+
import { ethereum } from "../../../../chains/chain-definitions/ethereum.js";
9+
import { isAddress } from "../../../../utils/address.js";
10+
import { createConnectionManager } from "../../../../wallets/manager/index.js";
11+
import type { WalletId } from "../../../../wallets/wallet-types.js";
12+
import { ThirdwebProvider } from "../../../web/providers/thirdweb-provider.js";
13+
import { ConnectionManagerCtx } from "../../providers/connection-manager.js";
14+
import {
15+
handleWalletConnection,
16+
useAutoConnectCore,
17+
} from "./useAutoConnect.js";
18+
19+
describe("useAutoConnectCore", () => {
20+
const mockStorage = new MockStorage();
21+
const manager = createConnectionManager(mockStorage);
22+
23+
// Create a wrapper component with the mocked context
24+
const wrapper = ({ children }: { children: ReactNode }) => {
25+
return (
26+
<ThirdwebProvider>
27+
<ConnectionManagerCtx.Provider value={manager}>
28+
{children}
29+
</ConnectionManagerCtx.Provider>
30+
</ThirdwebProvider>
31+
);
32+
};
33+
34+
it("should return a useQuery result", async () => {
35+
const wallet = createWalletAdapter({
36+
adaptedAccount: TEST_ACCOUNT_A,
37+
client: TEST_CLIENT,
38+
chain: ethereum,
39+
onDisconnect: () => {},
40+
switchChain: () => {},
41+
});
42+
const { result } = renderHook(
43+
() =>
44+
useAutoConnectCore(
45+
mockStorage,
46+
{
47+
wallets: [wallet],
48+
client: TEST_CLIENT,
49+
},
50+
(id: WalletId) =>
51+
createWalletAdapter({
52+
adaptedAccount: TEST_ACCOUNT_A,
53+
client: TEST_CLIENT,
54+
chain: ethereum,
55+
onDisconnect: () => {
56+
console.warn(id);
57+
},
58+
switchChain: () => {},
59+
}),
60+
),
61+
{ wrapper },
62+
);
63+
expect("data" in result.current).toBeTruthy();
64+
await waitFor(() => {
65+
expect(typeof result.current.data).toBe("boolean");
66+
});
67+
});
68+
69+
it("should return `false` if there's no lastConnectedWalletIds", async () => {
70+
const wallet = createWalletAdapter({
71+
adaptedAccount: TEST_ACCOUNT_A,
72+
client: TEST_CLIENT,
73+
chain: ethereum,
74+
onDisconnect: () => {},
75+
switchChain: () => {},
76+
});
77+
const { result } = renderHook(
78+
() =>
79+
useAutoConnectCore(
80+
mockStorage,
81+
{
82+
wallets: [wallet],
83+
client: TEST_CLIENT,
84+
},
85+
(id: WalletId) =>
86+
createWalletAdapter({
87+
adaptedAccount: TEST_ACCOUNT_A,
88+
client: TEST_CLIENT,
89+
chain: ethereum,
90+
onDisconnect: () => {
91+
console.warn(id);
92+
},
93+
switchChain: () => {},
94+
}),
95+
),
96+
{ wrapper },
97+
);
98+
await waitFor(
99+
() => {
100+
expect(result.current.data).toBe(false);
101+
},
102+
{ timeout: 1000 },
103+
);
104+
});
105+
106+
it("should call onTimeout on ... timeout", async () => {
107+
const wallet = createWalletAdapter({
108+
adaptedAccount: TEST_ACCOUNT_A,
109+
client: TEST_CLIENT,
110+
chain: ethereum,
111+
onDisconnect: () => {},
112+
switchChain: () => {},
113+
});
114+
mockStorage.setItem("thirdweb:active-wallet-id", wallet.id);
115+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
116+
const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
117+
// Purposefully mock the wallet.autoConnect method to test the timeout logic
118+
wallet.autoConnect = () =>
119+
new Promise((resolve) => {
120+
setTimeout(() => {
121+
// @ts-ignore Mock purpose
122+
resolve("Connection successful");
123+
}, 2100);
124+
});
125+
renderHook(
126+
() =>
127+
useAutoConnectCore(
128+
mockStorage,
129+
{
130+
wallets: [wallet],
131+
client: TEST_CLIENT,
132+
onTimeout: () => console.info("TIMEOUTTED"),
133+
timeout: 0,
134+
},
135+
(id: WalletId) =>
136+
createWalletAdapter({
137+
adaptedAccount: TEST_ACCOUNT_A,
138+
client: TEST_CLIENT,
139+
chain: ethereum,
140+
onDisconnect: () => {
141+
console.warn(id);
142+
},
143+
switchChain: () => {},
144+
}),
145+
),
146+
{ wrapper },
147+
);
148+
await waitFor(
149+
() => {
150+
expect(warnSpy).toHaveBeenCalled();
151+
expect(warnSpy).toHaveBeenCalledWith(
152+
"AutoConnect timeout: 0ms limit exceeded.",
153+
);
154+
expect(infoSpy).toHaveBeenCalled();
155+
expect(infoSpy).toHaveBeenCalledWith("TIMEOUTTED");
156+
warnSpy.mockRestore();
157+
},
158+
{ timeout: 2000 },
159+
);
160+
});
161+
});
162+
163+
describe("handleWalletConnection", () => {
164+
const wallet = createWalletAdapter({
165+
adaptedAccount: TEST_ACCOUNT_A,
166+
client: TEST_CLIENT,
167+
chain: ethereum,
168+
onDisconnect: () => {},
169+
switchChain: () => {},
170+
});
171+
it("should return the correct result", async () => {
172+
const result = await handleWalletConnection({
173+
client: TEST_CLIENT,
174+
lastConnectedChain: ethereum,
175+
authResult: undefined,
176+
wallet,
177+
});
178+
179+
expect("address" in result).toBe(true);
180+
expect(isAddress(result.address)).toBe(true);
181+
expect("sendTransaction" in result).toBe(true);
182+
expect(typeof result.sendTransaction).toBe("function");
183+
expect("signMessage" in result).toBe(true);
184+
expect("signTypedData" in result).toBe(true);
185+
expect("signTransaction" in result).toBe(true);
186+
});
187+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
2+
import { getUrlToken } from "./get-url-token.js";
3+
4+
describe("getUrlToken", () => {
5+
let originalLocation: Location;
6+
7+
beforeEach(() => {
8+
originalLocation = window.location;
9+
10+
Object.defineProperty(window, "location", {
11+
value: {
12+
...originalLocation,
13+
search: "",
14+
},
15+
writable: true,
16+
});
17+
});
18+
19+
afterEach(() => {
20+
// Restore the original location object after each test
21+
Object.defineProperty(window, "location", {
22+
value: originalLocation,
23+
writable: true,
24+
});
25+
});
26+
27+
it("should return an empty object if not in web context", () => {
28+
const originalWindow = window;
29+
// biome-ignore lint/suspicious/noExplicitAny: Test
30+
(global as any).window = undefined;
31+
32+
const result = getUrlToken();
33+
// biome-ignore lint/suspicious/noExplicitAny: Test
34+
(global as any).window = originalWindow;
35+
36+
expect(result).toEqual({});
37+
});
38+
39+
it("should return an empty object if no parameters are present", () => {
40+
const result = getUrlToken();
41+
expect(result).toEqual({});
42+
});
43+
44+
it("should parse walletId and authResult correctly", () => {
45+
window.location.search =
46+
"?walletId=123&authResult=%7B%22token%22%3A%22abc%22%7D";
47+
48+
const result = getUrlToken();
49+
50+
expect(result).toEqual({
51+
walletId: "123",
52+
authResult: { token: "abc" },
53+
authProvider: null,
54+
authCookie: null,
55+
});
56+
});
57+
58+
it("should handle authCookie and update URL correctly", () => {
59+
window.location.search = "?walletId=123&authCookie=myCookie";
60+
61+
const result = getUrlToken();
62+
63+
expect(result).toEqual({
64+
walletId: "123",
65+
authResult: undefined,
66+
authProvider: null,
67+
authCookie: "myCookie",
68+
});
69+
70+
// Check if URL has been updated correctly
71+
expect(window.location.search).toBe("?walletId=123&authCookie=myCookie");
72+
});
73+
74+
it("should handle all parameters correctly", () => {
75+
window.location.search =
76+
"?walletId=123&authResult=%7B%22token%22%3A%22xyz%22%7D&authProvider=provider1&authCookie=myCookie";
77+
78+
const result = getUrlToken();
79+
80+
expect(result).toEqual({
81+
walletId: "123",
82+
authResult: { token: "xyz" },
83+
authProvider: "provider1",
84+
authCookie: "myCookie",
85+
});
86+
87+
// Check if URL has been updated correctly
88+
expect(window.location.search).toBe(
89+
"?walletId=123&authResult=%7B%22token%22%3A%22xyz%22%7D&authProvider=provider1&authCookie=myCookie",
90+
);
91+
});
92+
});

packages/thirdweb/test/vitest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default defineConfig({
3131
],
3232
include: ["src/**"],
3333
},
34-
environmentMatchGlobs: [["src/react/**/*.test.tsx", "happy-dom"]],
34+
environmentMatchGlobs: [["src/**/*.test.tsx", "happy-dom"]],
3535
environment: "node",
3636
include: ["src/**/*.test.{ts,tsx}"],
3737
setupFiles: [join(__dirname, "./reactSetup.ts")],

0 commit comments

Comments
 (0)