Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-beds-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Add onTimeout callback to useAutoConnect
5 changes: 5 additions & 0 deletions packages/thirdweb/src/react/core/hooks/connection/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,9 @@ export type AutoConnectProps = {
* Optional chain to autoconnect to
*/
chain?: Chain;

/**
* Callback to be called when the connection is timeout-ed
*/
onTimeout?: () => void;
};
53 changes: 41 additions & 12 deletions packages/thirdweb/src/react/core/hooks/wallets/useAutoConnect.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"use client";

import { useQuery } from "@tanstack/react-query";
import type { Chain } from "../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../client/client.js";
import type { AsyncStorage } from "../../../../utils/storage/AsyncStorage.js";
import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js";
import { ClientScopedStorage } from "../../../../wallets/in-app/core/authentication/client-scoped-storage.js";
import type { AuthStoredTokenWithCookieReturnType } from "../../../../wallets/in-app/core/authentication/types.js";
import { getUrlToken } from "../../../../wallets/in-app/web/lib/get-url-token.js";
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
import {
Expand Down Expand Up @@ -83,14 +86,6 @@
const lastConnectedChain =
(await getLastConnectedChain(storage)) || props.chain;

async function handleWalletConnection(wallet: Wallet) {
return wallet.autoConnect({
client: props.client,
chain: lastConnectedChain ?? undefined,
authResult,
});
}

const availableWallets = [...wallets, ...(getInstalledWallets?.() ?? [])];
const activeWallet =
lastActiveWalletId &&
Expand All @@ -100,9 +95,22 @@
if (activeWallet) {
try {
setConnectionStatus("connecting"); // only set connecting status if we are connecting the last active EOA
await timeoutPromise(handleWalletConnection(activeWallet), {
ms: timeout,
message: `AutoConnect timeout: ${timeout}ms limit exceeded.`,
await timeoutPromise(
handleWalletConnection({
wallet: activeWallet,
client: props.client,
lastConnectedChain,
authResult,
}),
{
ms: timeout,
message: `AutoConnect timeout: ${timeout}ms limit exceeded.`,
},
).catch((err) => {
console.warn(err.message);
if (props.onTimeout) {
props.onTimeout();
}
});

// connected wallet could be activeWallet or smart wallet
Expand Down Expand Up @@ -138,7 +146,12 @@

for (const wallet of otherWallets) {
try {
await handleWalletConnection(wallet);
await handleWalletConnection({
wallet,
client: props.client,
lastConnectedChain,
authResult,
});

Check warning on line 154 in packages/thirdweb/src/react/core/hooks/wallets/useAutoConnect.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/wallets/useAutoConnect.ts#L149-L154

Added lines #L149 - L154 were not covered by tests
manager.addConnectedWallet(wallet);
} catch {
// no-op
Expand All @@ -158,3 +171,19 @@

return query;
}

/**
* @internal
*/
export async function handleWalletConnection(props: {
wallet: Wallet;
client: ThirdwebClient;
authResult: AuthStoredTokenWithCookieReturnType | undefined;
lastConnectedChain: Chain | undefined;
}) {
return props.wallet.autoConnect({
client: props.client,
chain: props.lastConnectedChain,
authResult: props.authResult,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { renderHook, waitFor } from "@testing-library/react";
import type { ReactNode } from "react";
import { describe, expect, it, vi } from "vitest";
import { MockStorage } from "~test/mocks/storage.js";
import { TEST_CLIENT } from "~test/test-clients.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { createWalletAdapter } from "../../../../adapters/wallet-adapter.js";
import { ethereum } from "../../../../chains/chain-definitions/ethereum.js";
import { isAddress } from "../../../../utils/address.js";
import { createConnectionManager } from "../../../../wallets/manager/index.js";
import type { WalletId } from "../../../../wallets/wallet-types.js";
import { ThirdwebProvider } from "../../../web/providers/thirdweb-provider.js";
import { ConnectionManagerCtx } from "../../providers/connection-manager.js";
import {
handleWalletConnection,
useAutoConnectCore,
} from "./useAutoConnect.js";

describe("useAutoConnectCore", () => {
const mockStorage = new MockStorage();
const manager = createConnectionManager(mockStorage);

// Create a wrapper component with the mocked context
const wrapper = ({ children }: { children: ReactNode }) => {
return (
<ThirdwebProvider>
<ConnectionManagerCtx.Provider value={manager}>
{children}
</ConnectionManagerCtx.Provider>
</ThirdwebProvider>
);
};

it("should return a useQuery result", async () => {
const wallet = createWalletAdapter({
adaptedAccount: TEST_ACCOUNT_A,
client: TEST_CLIENT,
chain: ethereum,
onDisconnect: () => {},
switchChain: () => {},
});
const { result } = renderHook(
() =>
useAutoConnectCore(
mockStorage,
{
wallets: [wallet],
client: TEST_CLIENT,
},
(id: WalletId) =>
createWalletAdapter({
adaptedAccount: TEST_ACCOUNT_A,
client: TEST_CLIENT,
chain: ethereum,
onDisconnect: () => {
console.warn(id);
},
switchChain: () => {},
}),
),
{ wrapper },
);
expect("data" in result.current).toBeTruthy();
await waitFor(() => {
expect(typeof result.current.data).toBe("boolean");
});
});

it("should return `false` if there's no lastConnectedWalletIds", async () => {
const wallet = createWalletAdapter({
adaptedAccount: TEST_ACCOUNT_A,
client: TEST_CLIENT,
chain: ethereum,
onDisconnect: () => {},
switchChain: () => {},
});
const { result } = renderHook(
() =>
useAutoConnectCore(
mockStorage,
{
wallets: [wallet],
client: TEST_CLIENT,
},
(id: WalletId) =>
createWalletAdapter({
adaptedAccount: TEST_ACCOUNT_A,
client: TEST_CLIENT,
chain: ethereum,
onDisconnect: () => {
console.warn(id);
},
switchChain: () => {},
}),
),
{ wrapper },
);
await waitFor(
() => {
expect(result.current.data).toBe(false);
},
{ timeout: 1000 },
);
});

it("should call onTimeout on ... timeout", async () => {
const wallet = createWalletAdapter({
adaptedAccount: TEST_ACCOUNT_A,
client: TEST_CLIENT,
chain: ethereum,
onDisconnect: () => {},
switchChain: () => {},
});
mockStorage.setItem("thirdweb:active-wallet-id", wallet.id);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
// Purposefully mock the wallet.autoConnect method to test the timeout logic
wallet.autoConnect = () =>
new Promise((resolve) => {
setTimeout(() => {
// @ts-ignore Mock purpose
resolve("Connection successful");
}, 2100);
});
renderHook(
() =>
useAutoConnectCore(
mockStorage,
{
wallets: [wallet],
client: TEST_CLIENT,
onTimeout: () => console.info("TIMEOUTTED"),
timeout: 0,
},
(id: WalletId) =>
createWalletAdapter({
adaptedAccount: TEST_ACCOUNT_A,
client: TEST_CLIENT,
chain: ethereum,
onDisconnect: () => {
console.warn(id);
},
switchChain: () => {},
}),
),
{ wrapper },
);
await waitFor(
() => {
expect(warnSpy).toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith(
"AutoConnect timeout: 0ms limit exceeded.",
);
expect(infoSpy).toHaveBeenCalled();
expect(infoSpy).toHaveBeenCalledWith("TIMEOUTTED");
warnSpy.mockRestore();
},
{ timeout: 2000 },
);
});
});

describe("handleWalletConnection", () => {
const wallet = createWalletAdapter({
adaptedAccount: TEST_ACCOUNT_A,
client: TEST_CLIENT,
chain: ethereum,
onDisconnect: () => {},
switchChain: () => {},
});
it("should return the correct result", async () => {
const result = await handleWalletConnection({
client: TEST_CLIENT,
lastConnectedChain: ethereum,
authResult: undefined,
wallet,
});

expect("address" in result).toBe(true);
expect(isAddress(result.address)).toBe(true);
expect("sendTransaction" in result).toBe(true);
expect(typeof result.sendTransaction).toBe("function");
expect("signMessage" in result).toBe(true);
expect("signTypedData" in result).toBe(true);
expect("signTransaction" in result).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { getUrlToken } from "./get-url-token.js";

describe("getUrlToken", () => {
let originalLocation: Location;

beforeEach(() => {
originalLocation = window.location;

Object.defineProperty(window, "location", {
value: {
...originalLocation,
search: "",
},
writable: true,
});
});

afterEach(() => {
// Restore the original location object after each test
Object.defineProperty(window, "location", {
value: originalLocation,
writable: true,
});
});

it("should return an empty object if not in web context", () => {
const originalWindow = window;
// biome-ignore lint/suspicious/noExplicitAny: Test
(global as any).window = undefined;

const result = getUrlToken();
// biome-ignore lint/suspicious/noExplicitAny: Test
(global as any).window = originalWindow;

expect(result).toEqual({});
});

it("should return an empty object if no parameters are present", () => {
const result = getUrlToken();
expect(result).toEqual({});
});

it("should parse walletId and authResult correctly", () => {
window.location.search =
"?walletId=123&authResult=%7B%22token%22%3A%22abc%22%7D";

const result = getUrlToken();

expect(result).toEqual({
walletId: "123",
authResult: { token: "abc" },
authProvider: null,
authCookie: null,
});
});

it("should handle authCookie and update URL correctly", () => {
window.location.search = "?walletId=123&authCookie=myCookie";

const result = getUrlToken();

expect(result).toEqual({
walletId: "123",
authResult: undefined,
authProvider: null,
authCookie: "myCookie",
});

// Check if URL has been updated correctly
expect(window.location.search).toBe("?walletId=123&authCookie=myCookie");
});

it("should handle all parameters correctly", () => {
window.location.search =
"?walletId=123&authResult=%7B%22token%22%3A%22xyz%22%7D&authProvider=provider1&authCookie=myCookie";

const result = getUrlToken();

expect(result).toEqual({
walletId: "123",
authResult: { token: "xyz" },
authProvider: "provider1",
authCookie: "myCookie",
});

// Check if URL has been updated correctly
expect(window.location.search).toBe(
"?walletId=123&authResult=%7B%22token%22%3A%22xyz%22%7D&authProvider=provider1&authCookie=myCookie",
);
});
});
2 changes: 1 addition & 1 deletion packages/thirdweb/test/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default defineConfig({
],
include: ["src/**"],
},
environmentMatchGlobs: [["src/react/**/*.test.tsx", "happy-dom"]],
environmentMatchGlobs: [["src/**/*.test.tsx", "happy-dom"]],
environment: "node",
include: ["src/**/*.test.{ts,tsx}"],
setupFiles: [join(__dirname, "./reactSetup.ts")],
Expand Down
Loading