Skip to content

Commit

Permalink
[SDK]: move autoConnect function into it's own standalone function (#…
Browse files Browse the repository at this point in the history
…5889)

---
title: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"
---

https://linear.app/thirdweb/issue/TOOL-2697/add-js-autoconnect

## Notes for the reviewer
Anything important to call out? Be sure to also clarify these in your comments.

## How to test
Unit tests, playground, etc.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring the auto-connect functionality for wallets in the `thirdweb` library. It enhances the modularity and usability of the `autoConnect` feature, allowing it to be used outside of React components and streamlining wallet connection processes.

### Detailed summary
- Modified `if` condition in `timeoutPromise.ts` to check for `typeof window`.
- Updated import paths for `AutoConnectProps` in `AutoConnect.tsx` files.
- Exposed `autoConnect` function in `wallets.ts` for external use.
- Adjusted exports in `react.ts` and `react.native.ts` for `AutoConnectProps`.
- Refactored `useAutoConnectCore` to use `autoConnectCore`.
- Consolidated wallet connection logic in `autoConnect` and `autoConnectCore`.
- Updated tests to reflect changes in wallet connection handling and added new test cases for `autoConnect`.
- Enhanced error handling and timeout logic in wallet connection processes.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
  • Loading branch information
ElasticBottle committed Jan 14, 2025
1 parent e331e43 commit 7a3dff0
Show file tree
Hide file tree
Showing 24 changed files with 767 additions and 217 deletions.
17 changes: 17 additions & 0 deletions .changeset/green-rockets-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"thirdweb": minor
---

Exposes autoConnect as a standalone function for use outside of react.

```tsx
import { autoConnect } from "thirdweb/wallets";

const autoConnected = await autoConnect({
client,
onConnect: (wallet) => {
console.log("wallet", wallet); /// wallet that is have been auto connected.
},
});
console.log('isAutoConnected', isAutoConnected) // true or false
```
2 changes: 1 addition & 1 deletion packages/thirdweb/src/exports/react.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export {

// Components
export { AutoConnect } from "../react/native/ui/AutoConnect/AutoConnect.js";
export type { AutoConnectProps } from "../react/core/hooks/connection/types.js";
export type { AutoConnectProps } from "../wallets/connection/types.js";

export { TransactionButton } from "../react/native/ui/transaction/TransactionButton.js";
export type { TransactionButtonProps } from "../react/core/hooks/transaction/transaction-button-utils.js";
Expand Down
2 changes: 1 addition & 1 deletion packages/thirdweb/src/exports/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export {
} from "../react/core/hooks/pay/usePostOnrampQuote.js";

export { AutoConnect } from "../react/web/ui/AutoConnect/AutoConnect.js";
export type { AutoConnectProps } from "../react/core/hooks/connection/types.js";
export type { AutoConnectProps } from "../wallets/connection/types.js";

// auth
export type { SiweAuthOptions } from "../react/core/hooks/auth/useSiweAuth.js";
Expand Down
2 changes: 2 additions & 0 deletions packages/thirdweb/src/exports/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,6 @@ export { injectedProvider } from "../wallets/injected/mipdStore.js";

export type { ConnectionManager } from "../wallets/manager/index.js";

export type { AutoConnectProps } from "../wallets/connection/types.js";
export { autoConnect } from "../wallets/connection/autoConnect.js";
export { deploySmartAccount } from "../wallets/smart/lib/signing.js";
170 changes: 12 additions & 158 deletions packages/thirdweb/src/react/core/hooks/wallets/useAutoConnect.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
"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 { autoConnectCore } from "../../../../wallets/connection/autoConnectCore.js";
import type { AutoConnectProps } from "../../../../wallets/connection/types.js";
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
import {
getLastConnectedChain,
getStoredActiveWalletId,
getStoredConnectedWalletIds,
} from "../../../../wallets/manager/index.js";
import type { WalletId } from "../../../../wallets/wallet-types.js";
import { useConnectionManagerCtx } from "../../providers/connection-manager.js";
import { setLastAuthProvider } from "../../utils/storage.js";
import { timeoutPromise } from "../../utils/timeoutPromise.js";
import type { AutoConnectProps } from "../connection/types.js";
import { useConnect } from "./useConnect.js";
import { useSetActiveWalletConnectionStatus } from "./useSetActiveWalletConnectionStatus.js";

export function useAutoConnectCore(
storage: AsyncStorage,
Expand All @@ -29,161 +17,27 @@ export function useAutoConnectCore(
getInstalledWallets?: () => Wallet[],
) {
const manager = useConnectionManagerCtx("useAutoConnect");
const setConnectionStatus = useSetActiveWalletConnectionStatus();
const { connect } = useConnect({
client: props.client,
accountAbstraction: props.accountAbstraction,
});
const { isAutoConnecting } = manager;
const { wallets, onConnect } = props;
const timeout = props.timeout ?? 15000;
// get the supported wallets from thirdweb provider
// check the storage for last connected wallets and connect them all
// check the storage for last active wallet and set it as active
const autoConnect = async (): Promise<boolean> => {
let autoConnected = false;
isAutoConnecting.setValue(true);
let [lastConnectedWalletIds, lastActiveWalletId] = await Promise.all([
getStoredConnectedWalletIds(storage),
getStoredActiveWalletId(storage),
]);

const { authResult, walletId, authProvider, authCookie } = getUrlToken();
const wallet = wallets.find((w) => w.id === walletId);

// If an auth cookie is found and this site supports the wallet, we'll set the auth cookie in the client storage
if (authCookie && wallet) {
const clientStorage = new ClientScopedStorage({
storage,
clientId: props.client.clientId,
ecosystem: isEcosystemWallet(wallet)
? {
id: wallet.id,
partnerId: wallet.getConfig()?.partnerId,
}
: undefined,
});
await clientStorage.saveAuthCookie(authCookie);
}

if (walletId) {
lastActiveWalletId = walletId;
lastConnectedWalletIds = lastConnectedWalletIds?.includes(walletId)
? lastConnectedWalletIds
: [walletId, ...(lastConnectedWalletIds || [])];
}
if (authProvider) {
await setLastAuthProvider(authProvider, storage);
}

// if no wallets were last connected or we didn't receive an auth token
if (!lastConnectedWalletIds) {
return autoConnected;
}

// this flow can actually be used for a first connection in the case of a redirect
// in that case, we default to the passed chain to connect to
const lastConnectedChain =
(await getLastConnectedChain(storage)) || props.chain;

const availableWallets = [...wallets, ...(getInstalledWallets?.() ?? [])];
const activeWallet =
lastActiveWalletId &&
(availableWallets.find((w) => w.id === lastActiveWalletId) ||
createWalletFn(lastActiveWalletId));

if (activeWallet) {
try {
setConnectionStatus("connecting"); // only set connecting status if we are connecting the last active EOA
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
const connectedWallet = await connect(activeWallet);

if (connectedWallet) {
if (onConnect) {
try {
onConnect(connectedWallet);
autoConnected = true;
} catch {
// ignore
}
}
} else {
setConnectionStatus("disconnected");
}
} catch (e) {
if (e instanceof Error) {
console.warn("Error auto connecting wallet:", e.message);
}
setConnectionStatus("disconnected");
}
} else {
setConnectionStatus("disconnected");
}

// then connect wallets that were last connected but were not set as active
const otherWallets = availableWallets.filter(
(w) =>
w.id !== lastActiveWalletId && lastConnectedWalletIds.includes(w.id),
);

for (const wallet of otherWallets) {
try {
await handleWalletConnection({
wallet,
client: props.client,
lastConnectedChain,
authResult,
});
manager.addConnectedWallet(wallet);
} catch {
// no-op
}
}
isAutoConnecting.setValue(false);
return autoConnected; // useQuery needs a return value
};

// trigger the auto connect on first mount only
const query = useQuery({
queryKey: ["autoConnect", props.client.clientId],
queryFn: autoConnect,
queryFn: () =>
autoConnectCore({
createWalletFn,
manager,
props,
storage,
connectOverride: connect,
getInstalledWallets,
setLastAuthProvider,
}),
refetchOnMount: false,
refetchOnWindowFocus: false,
});

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
Expand Up @@ -6,15 +6,11 @@ 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";
import { useAutoConnectCore } from "./useAutoConnect.js";

describe("useAutoConnectCore", () => {
const mockStorage = new MockStorage();
Expand Down Expand Up @@ -159,29 +155,3 @@ describe("useAutoConnectCore", () => {
);
});
});

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
@@ -1,6 +1,6 @@
import { nativeLocalStorage } from "../../../../utils/storage/nativeStorage.js";
import type { AutoConnectProps } from "../../../../wallets/connection/types.js";
import { createWallet } from "../../../../wallets/native/create-wallet.js";
import type { AutoConnectProps } from "../../../core/hooks/connection/types.js";
import { useAutoConnectCore } from "../../../core/hooks/wallets/useAutoConnect.js";
import { getDefaultWallets } from "../../wallets/defaultWallets.js";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import type { AutoConnectProps } from "../../../core/hooks/connection/types.js";
import type { AutoConnectProps } from "../../../../wallets/connection/types.js";
import { useAutoConnect } from "../../hooks/wallets/useAutoConnect.js";

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { webLocalStorage } from "../../../../utils/storage/webStorage.js";
import type { AutoConnectProps } from "../../../../wallets/connection/types.js";
import { createWallet } from "../../../../wallets/create-wallet.js";
import { getDefaultWallets } from "../../../../wallets/defaultWallets.js";
import { getInstalledWalletProviders } from "../../../../wallets/injected/mipdStore.js";
import type { AutoConnectProps } from "../../../core/hooks/connection/types.js";
import { useAutoConnectCore } from "../../../core/hooks/wallets/useAutoConnect.js";
import { getDefaultWallets } from "../../wallets/defaultWallets.js";

/**
* Autoconnect the last previously connected wallet.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import type { AutoConnectProps } from "../../../core/hooks/connection/types.js";
import type { AutoConnectProps } from "../../../../wallets/connection/types.js";
import { useAutoConnect } from "../../hooks/wallets/useAutoConnect.js";

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import styled from "@emotion/styled";
import { useEffect, useMemo, useState } from "react";
import { getDefaultWallets } from "../../../../wallets/defaultWallets.js";
import { iconSize } from "../../../core/design-system/index.js";
import { useSiweAuth } from "../../../core/hooks/auth/useSiweAuth.js";
import type { ConnectButtonProps } from "../../../core/hooks/connection/ConnectButtonProps.js";
Expand All @@ -16,7 +17,6 @@ import {
} from "../../providers/wallet-ui-states-provider.js";
import { canFitWideModal } from "../../utils/canFitWideModal.js";
import { usePreloadWalletProviders } from "../../utils/usePreloadWalletProviders.js";
import { getDefaultWallets } from "../../wallets/defaultWallets.js";
import { AutoConnect } from "../AutoConnect/AutoConnect.js";
import { Modal } from "../components/Modal.js";
import { Spinner } from "../components/Spinner.js";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { useEffect, useMemo } from "react";
import type { Chain } from "../../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../../client/client.js";
import { getDefaultWallets } from "../../../../../wallets/defaultWallets.js";
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
import type { SmartWalletOptions } from "../../../../../wallets/smart/types.js";
import {
Expand All @@ -21,7 +22,6 @@ import { useConnectionManager } from "../../../../core/providers/connection-mana
import { WalletUIStatesProvider } from "../../../providers/wallet-ui-states-provider.js";
import { canFitWideModal } from "../../../utils/canFitWideModal.js";
import { usePreloadWalletProviders } from "../../../utils/usePreloadWalletProviders.js";
import { getDefaultWallets } from "../../../wallets/defaultWallets.js";
import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js";
import { AutoConnect } from "../../AutoConnect/AutoConnect.js";
import { DynamicHeight } from "../../components/DynamicHeight.js";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Chain } from "../../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../../client/client.js";
import { getDefaultWallets } from "../../../../../wallets/defaultWallets.js";
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
import type { SmartWalletOptions } from "../../../../../wallets/smart/types.js";
import type { AppMetadata } from "../../../../../wallets/types.js";
import type { WalletId } from "../../../../../wallets/wallet-types.js";
import { useConnectedWallets } from "../../../../core/hooks/wallets/useConnectedWallets.js";
import { getDefaultWallets } from "../../../wallets/defaultWallets.js";
import { ConnectModalContent } from "../Modal/ConnectModalContent.js";
import { useSetupScreen } from "../Modal/screen.js";
import type { ConnectLocale } from "../locale/types.js";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useCallback, useContext, useMemo, useState } from "react";
import type { Chain } from "../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../client/client.js";
import { getDefaultWallets } from "../../../../wallets/defaultWallets.js";
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
import type { SmartWalletOptions } from "../../../../wallets/smart/types.js";
import type { AppMetadata } from "../../../../wallets/types.js";
import type { Theme } from "../../../core/design-system/index.js";
import { SetRootElementContext } from "../../../core/providers/RootElementContext.js";
import { WalletUIStatesProvider } from "../../providers/wallet-ui-states-provider.js";
import { canFitWideModal } from "../../utils/canFitWideModal.js";
import { getDefaultWallets } from "../../wallets/defaultWallets.js";
import type { LocaleId } from "../types.js";
import ConnectModal from "./Modal/ConnectModal.js";
import { getConnectLocale } from "./locale/getConnectLocale.js";
Expand Down
Loading

0 comments on commit 7a3dff0

Please sign in to comment.