From 19172734360fcf245300e3a2bbb567b1f4bc17c8 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Fri, 7 Mar 2025 11:43:10 +1300 Subject: [PATCH] [SDK] feat: Allow custom storage for inApp and ecosystem wallets --- .changeset/gentle-books-begin.md | 5 ++ packages/thirdweb/src/exports/storage.ts | 1 + .../web/ui/ConnectWallet/ConnectButton.tsx | 1 - .../ui/ConnectWallet/Modal/ConnectEmbed.tsx | 1 - .../web/utils/usePreloadWalletProviders.ts | 50 +------------------ .../thirdweb/src/wallets/ecosystem/types.ts | 5 ++ .../src/wallets/in-app/core/wallet/types.ts | 5 ++ .../wallets/in-app/native/auth/passkeys.ts | 8 +-- .../src/wallets/in-app/native/ecosystem.ts | 1 + .../src/wallets/in-app/native/in-app.ts | 1 + .../wallets/in-app/native/native-connector.ts | 5 +- .../src/wallets/in-app/web/ecosystem.ts | 1 + .../thirdweb/src/wallets/in-app/web/in-app.ts | 26 ++++++++++ .../wallets/in-app/web/lib/auth/passkeys.ts | 8 +-- .../wallets/in-app/web/lib/web-connector.ts | 3 +- .../thirdweb/src/wallets/in-app/web/types.ts | 7 ++- 16 files changed, 67 insertions(+), 61 deletions(-) create mode 100644 .changeset/gentle-books-begin.md diff --git a/.changeset/gentle-books-begin.md b/.changeset/gentle-books-begin.md new file mode 100644 index 00000000000..1e36ea4b9b3 --- /dev/null +++ b/.changeset/gentle-books-begin.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Allow overriding storage for inApp and ecosystem wallets diff --git a/packages/thirdweb/src/exports/storage.ts b/packages/thirdweb/src/exports/storage.ts index 89a2c6f4949..d3ec1bb8a64 100644 --- a/packages/thirdweb/src/exports/storage.ts +++ b/packages/thirdweb/src/exports/storage.ts @@ -10,3 +10,4 @@ export { resolveArweaveScheme, type ResolveArweaveSchemeOptions, } from "../utils/arweave.js"; +export type { AsyncStorage } from "../utils/storage/AsyncStorage.js"; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectButton.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectButton.tsx index 23b787bc8e8..8adcb702e06 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectButton.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectButton.tsx @@ -300,7 +300,6 @@ export function ConnectButton(props: ConnectButtonProps) { usePreloadWalletProviders({ wallets, - client: props.client, }); // Add props.chain and props.chains to defined chains store diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/ConnectEmbed.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/ConnectEmbed.tsx index d4f2b8d37f5..62570702010 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/ConnectEmbed.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/ConnectEmbed.tsx @@ -215,7 +215,6 @@ export function ConnectEmbed(props: ConnectEmbedProps) { usePreloadWalletProviders({ wallets, - client: props.client, }); const modalSize = useMemo(() => { diff --git a/packages/thirdweb/src/react/web/utils/usePreloadWalletProviders.ts b/packages/thirdweb/src/react/web/utils/usePreloadWalletProviders.ts index e574c4b0425..f8be5d8f0d1 100644 --- a/packages/thirdweb/src/react/web/utils/usePreloadWalletProviders.ts +++ b/packages/thirdweb/src/react/web/utils/usePreloadWalletProviders.ts @@ -1,15 +1,10 @@ import { useQueries } from "@tanstack/react-query"; -import type { ThirdwebClient } from "../../../client/client.js"; import { COINBASE } from "../../../wallets/constants.js"; import { isEcosystemWallet } from "../../../wallets/ecosystem/is-ecosystem-wallet.js"; import type { Wallet } from "../../../wallets/interfaces/wallet.js"; import type { CreateWalletArgs } from "../../../wallets/wallet-types.js"; -import type { EcosystemWalletId } from "../../../wallets/wallet-types.js"; -export function usePreloadWalletProviders({ - client, - wallets, -}: { client: ThirdwebClient; wallets: Wallet[] }) { +export function usePreloadWalletProviders({ wallets }: { wallets: Wallet[] }) { useQueries({ queries: wallets .filter( @@ -29,49 +24,6 @@ export function usePreloadWalletProviders({ // return _something_ return true; } - case "inApp" === w.id: { - const [ - { InAppWebConnector }, - { getOrCreateInAppWalletConnector }, - ] = await Promise.all([ - import("../../../wallets/in-app/web/lib/web-connector.js"), - import("../../../wallets/in-app/core/wallet/in-app-core.js"), - ]); - await getOrCreateInAppWalletConnector(client, async (client) => { - return new InAppWebConnector({ - client, - }); - }); - // return _something_ - return true; - } - case isEcosystemWallet(w.id): { - const [ - { InAppWebConnector }, - { getOrCreateInAppWalletConnector }, - ] = await Promise.all([ - import("../../../wallets/in-app/web/lib/web-connector.js"), - import("../../../wallets/in-app/core/wallet/in-app-core.js"), - ]); - const ecosystemWallet = w as Wallet; // we know this is an ecosystem wallet - await getOrCreateInAppWalletConnector( - client, - async (client) => { - return new InAppWebConnector({ - client, - ecosystem: { - id: ecosystemWallet.id, - partnerId: ecosystemWallet.getConfig()?.partnerId, - }, - }); - }, - { - id: ecosystemWallet.id, - partnerId: ecosystemWallet.getConfig()?.partnerId, - }, - ); - return true; - } // potentially add more wallets here default: { return false; diff --git a/packages/thirdweb/src/wallets/ecosystem/types.ts b/packages/thirdweb/src/wallets/ecosystem/types.ts index 8072a4d6661..92d4a9d066b 100644 --- a/packages/thirdweb/src/wallets/ecosystem/types.ts +++ b/packages/thirdweb/src/wallets/ecosystem/types.ts @@ -1,4 +1,5 @@ import type { SupportedSmsCountry } from "../../react/web/wallets/in-app/supported-sms-countries.js"; +import type { AsyncStorage } from "../../utils/storage/AsyncStorage.js"; import type { InAppWalletAutoConnectOptions, InAppWalletConnectionOptions, @@ -23,6 +24,10 @@ export type EcosystemWalletCreationOptions = { * The partnerId of the ecosystem wallet to connect to */ partnerId?: string; + /** + * The storage to use for storing wallet state + */ + storage?: AsyncStorage; }; export type EcosystemWalletConnectionOptions = InAppWalletConnectionOptions; diff --git a/packages/thirdweb/src/wallets/in-app/core/wallet/types.ts b/packages/thirdweb/src/wallets/in-app/core/wallet/types.ts index 7d0a048ad85..f46e767d3c7 100644 --- a/packages/thirdweb/src/wallets/in-app/core/wallet/types.ts +++ b/packages/thirdweb/src/wallets/in-app/core/wallet/types.ts @@ -1,6 +1,7 @@ import type { Chain } from "../../../../chains/types.js"; import type { ThirdwebClient } from "../../../../client/client.js"; import type { SupportedSmsCountry } from "../../../../react/web/wallets/in-app/supported-sms-countries.js"; +import type { AsyncStorage } from "../../../../utils/storage/AsyncStorage.js"; import type { Prettify } from "../../../../utils/type-utils.js"; import type { SmartWalletOptions } from "../../../smart/types.js"; import type { @@ -87,5 +88,9 @@ export type InAppWalletCreationOptions = * Whether to hide the private key export button in the Connect Modal */ hidePrivateKeyExport?: boolean; + /** + * The storage to use for storing wallet state + */ + storage?: AsyncStorage; } | undefined; diff --git a/packages/thirdweb/src/wallets/in-app/native/auth/passkeys.ts b/packages/thirdweb/src/wallets/in-app/native/auth/passkeys.ts index 48e250a0d3f..d8c42cd3245 100644 --- a/packages/thirdweb/src/wallets/in-app/native/auth/passkeys.ts +++ b/packages/thirdweb/src/wallets/in-app/native/auth/passkeys.ts @@ -3,6 +3,7 @@ import { concat } from "viem"; import type { ThirdwebClient } from "../../../../client/client.js"; import { toBytes } from "../../../../utils/encoding/to-bytes.js"; import { keccak256 } from "../../../../utils/hashing/keccak256.js"; +import type { AsyncStorage } from "../../../../utils/storage/AsyncStorage.js"; import { nativeLocalStorage } from "../../../../utils/storage/nativeStorage.js"; import { base64ToString, @@ -121,13 +122,14 @@ export class PasskeyNativeClient implements PasskeyClient { export async function hasStoredPasskey( client: ThirdwebClient, ecosystemId?: EcosystemWalletId, + storage?: AsyncStorage, ) { - const storage = new ClientScopedStorage({ - storage: nativeLocalStorage, + const clientStorage = new ClientScopedStorage({ + storage: storage ?? nativeLocalStorage, clientId: client.clientId, ecosystem: ecosystemId ? { id: ecosystemId } : undefined, }); - const credId = await storage.getPasskeyCredentialId(); + const credId = await clientStorage.getPasskeyCredentialId(); return !!credId; } diff --git a/packages/thirdweb/src/wallets/in-app/native/ecosystem.ts b/packages/thirdweb/src/wallets/in-app/native/ecosystem.ts index ee7948619e0..c6dd26a9696 100644 --- a/packages/thirdweb/src/wallets/in-app/native/ecosystem.ts +++ b/packages/thirdweb/src/wallets/in-app/native/ecosystem.ts @@ -83,6 +83,7 @@ export function ecosystemWallet( return new InAppNativeConnector({ client, ecosystem, + storage: createOptions?.storage, // TODO (enclave): passkeyDomain for ecosystem wallets }); }, diff --git a/packages/thirdweb/src/wallets/in-app/native/in-app.ts b/packages/thirdweb/src/wallets/in-app/native/in-app.ts index aba02bb5e20..fbff363a6ba 100644 --- a/packages/thirdweb/src/wallets/in-app/native/in-app.ts +++ b/packages/thirdweb/src/wallets/in-app/native/in-app.ts @@ -65,6 +65,7 @@ export function inAppWallet( return new InAppNativeConnector({ client, passkeyDomain: createOptions?.auth?.passkeyDomain, + storage: createOptions?.storage, }); }, }) as Wallet<"inApp">; diff --git a/packages/thirdweb/src/wallets/in-app/native/native-connector.ts b/packages/thirdweb/src/wallets/in-app/native/native-connector.ts index cae76d45c66..45b7fdb42d6 100644 --- a/packages/thirdweb/src/wallets/in-app/native/native-connector.ts +++ b/packages/thirdweb/src/wallets/in-app/native/native-connector.ts @@ -1,5 +1,6 @@ import type { ThirdwebClient } from "../../../client/client.js"; import { stringify } from "../../../utils/json.js"; +import type { AsyncStorage } from "../../../utils/storage/AsyncStorage.js"; import { nativeLocalStorage } from "../../../utils/storage/nativeStorage.js"; import type { Account } from "../../interfaces/wallet.js"; import { getUserStatus } from "../core/actions/get-enclave-user-status.js"; @@ -37,11 +38,11 @@ import { sendOtp, verifyOtp } from "../web/lib/auth/otp.js"; import { deleteActiveAccount, socialAuth } from "./auth/native-auth.js"; import { logoutUser } from "./helpers/auth/logout.js"; import { ShardedWallet } from "./helpers/wallet/sharded-wallet.js"; - type NativeConnectorOptions = { client: ThirdwebClient; ecosystem?: Ecosystem; passkeyDomain?: string; + storage?: AsyncStorage; }; export class InAppNativeConnector implements InAppConnector { @@ -56,7 +57,7 @@ export class InAppNativeConnector implements InAppConnector { this.passkeyDomain = options.passkeyDomain; this.ecosystem = options.ecosystem; this.storage = new ClientScopedStorage({ - storage: nativeLocalStorage, + storage: options.storage ?? nativeLocalStorage, clientId: this.client.clientId, ecosystem: options.ecosystem, }); diff --git a/packages/thirdweb/src/wallets/in-app/web/ecosystem.ts b/packages/thirdweb/src/wallets/in-app/web/ecosystem.ts index 774626aef09..b2bdc3b2ea4 100644 --- a/packages/thirdweb/src/wallets/in-app/web/ecosystem.ts +++ b/packages/thirdweb/src/wallets/in-app/web/ecosystem.ts @@ -73,6 +73,7 @@ export function ecosystemWallet( return new InAppWebConnector({ client, ecosystem, + storage: createOptions?.storage, }); }, }) as Wallet; diff --git a/packages/thirdweb/src/wallets/in-app/web/in-app.ts b/packages/thirdweb/src/wallets/in-app/web/in-app.ts index 6e90dd3b7e2..69fd5ad18e2 100644 --- a/packages/thirdweb/src/wallets/in-app/web/in-app.ts +++ b/packages/thirdweb/src/wallets/in-app/web/in-app.ts @@ -222,6 +222,31 @@ import { createInAppWallet } from "../core/wallet/in-app-core.js"; * }); * ``` * + * ### Override storage for the wallet state + * + * By default, wallet state is stored in the browser's local storage. You can override this behavior by providing a custom storage object, useful for server side integrations. + * + * ```ts + * import { inAppWallet } from "thirdweb/wallets"; + * import { AsyncStorage } from "thirdweb/storage"; + * + * const myStorage: AsyncStorage = { + * getItem: async (key) => { + * return customGet(`CUSTOM_STORAGE_KEY${key}`); + * }, + * setItem: async (key, value) => { + * return customSet(`CUSTOM_STORAGE_KEY${key}`, value); + * }, + * removeItem: async (key) => { + * return customRemove(`CUSTOM_STORAGE_KEY${key}`); + * }, + * }; + * + * const wallet = inAppWallet({ + * storage: myStorage, + * }); + * ``` + * * @returns The created in-app wallet. * @wallet */ @@ -235,6 +260,7 @@ export function inAppWallet( return new InAppWebConnector({ client, passkeyDomain: createOptions?.auth?.passkeyDomain, + storage: createOptions?.storage, }); }, }) as Wallet<"inApp">; diff --git a/packages/thirdweb/src/wallets/in-app/web/lib/auth/passkeys.ts b/packages/thirdweb/src/wallets/in-app/web/lib/auth/passkeys.ts index 1b502e9e547..22221b769ad 100644 --- a/packages/thirdweb/src/wallets/in-app/web/lib/auth/passkeys.ts +++ b/packages/thirdweb/src/wallets/in-app/web/lib/auth/passkeys.ts @@ -1,5 +1,6 @@ import { client, parsers } from "@passwordless-id/webauthn"; import type { ThirdwebClient } from "../../../../../client/client.js"; +import type { AsyncStorage } from "../../../../../utils/storage/AsyncStorage.js"; import { webLocalStorage } from "../../../../../utils/storage/webStorage.js"; import { base64ToString, @@ -83,12 +84,13 @@ export class PasskeyWebClient implements PasskeyClient { export async function hasStoredPasskey( client: ThirdwebClient, ecosystemId?: EcosystemWalletId, + storage?: AsyncStorage, ) { - const storage = new ClientScopedStorage({ - storage: webLocalStorage, // TODO (passkey) react native variant of this fn + const clientStorage = new ClientScopedStorage({ + storage: storage ?? webLocalStorage, // TODO (passkey) react native variant of this fn clientId: client.clientId, ecosystem: ecosystemId ? { id: ecosystemId } : undefined, }); - const credId = await storage.getPasskeyCredentialId(); + const credId = await clientStorage.getPasskeyCredentialId(); return !!credId; } diff --git a/packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts b/packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts index 78626542908..2b8b1b07028 100644 --- a/packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts +++ b/packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts @@ -74,6 +74,7 @@ export class InAppWebConnector implements InAppConnector { onAuthSuccess, ecosystem, passkeyDomain, + storage, }: InAppWalletConstructorType) { if (this.isClientIdLegacyPaper(client.clientId)) { throw new Error( @@ -85,7 +86,7 @@ export class InAppWebConnector implements InAppConnector { this.ecosystem = ecosystem; this.passkeyDomain = passkeyDomain; this.storage = new ClientScopedStorage({ - storage: webLocalStorage, + storage: storage ?? webLocalStorage, clientId: client.clientId, ecosystem: ecosystem, }); diff --git a/packages/thirdweb/src/wallets/in-app/web/types.ts b/packages/thirdweb/src/wallets/in-app/web/types.ts index 8d7a369fab7..1400fe95cd9 100644 --- a/packages/thirdweb/src/wallets/in-app/web/types.ts +++ b/packages/thirdweb/src/wallets/in-app/web/types.ts @@ -2,11 +2,11 @@ // types for class constructors still a little messy right now. import type { ThirdwebClient } from "../../../client/client.js"; +import type { AsyncStorage } from "../../../utils/storage/AsyncStorage.js"; import type { AuthAndWalletRpcReturnType } from "../core/authentication/types.js"; import type { Ecosystem } from "../core/wallet/types.js"; import type { InAppWalletIframeCommunicator } from "./utils/iFrameCommunication/InAppWalletIframeCommunicator.js"; -// Open to PRs from whoever sees this and knows of a cleaner way to handle things type ClientIdConstructorType = { /** * the clientId of your API Key. You can create an API key by creating a project on thirdweb dashboard. @@ -29,6 +29,11 @@ export type InAppWalletConstructorType = ClientIdConstructorType & { * The domain of the passkey to use for authentication */ passkeyDomain?: string; + + /** + * The storage to use for storing wallet state + */ + storage?: AsyncStorage; }; export type ClientIdWithQuerierType = ClientIdConstructorType & {