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/thin-rockets-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Default to in-memory storage when creating inapp wallets outside the browser
106 changes: 76 additions & 30 deletions packages/thirdweb/src/wallets/in-app/web/in-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,29 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
* const account = await wallet.connect({
* client,
* chain,
* strategy: "google",
* strategy: "google", // or "apple", "facebook","discord", "github", "twitch", "x", "telegram", "line", "coinbase", etc
* });
* ```
*
* [View all available social auth methods](https://portal.thirdweb.com/connect/wallet/sign-in-methods/configure)
*
* ### Enable smart accounts and sponsor gas for your users:
*
* With the `executionMode` option, you can enable smart accounts and sponsor gas for your users.
*
* **Using EIP-7702** (recommended):
*
* On chains with EIP-7702 enabled, you can upgrade the inapp wallet to a smart account, keeping the same address and performance as the regular EOA.
*
* ```ts
* import { inAppWallet } from "thirdweb/wallets";
* import { sepolia } from "thirdweb/chains";
*
* const wallet = inAppWallet({
* smartAccount: {
* chain: sepolia,
* executionMode: {
* mode: "EIP7702",
* sponsorGas: true,
* },
* },
* });
*
* // account will be a smart account with sponsored gas enabled
Expand All @@ -49,8 +55,28 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
* });
* ```
*
* **Using EIP-4337**:
*
* On chains without EIP-7702 enabled, you can still use smart accounts using EIP-4337, this will return a different address (the smart contract address) than the regular EOA.
*
* ```ts
* import { inAppWallet } from "thirdweb/wallets/in-app";
*
* const wallet = inAppWallet({
* executionMode: {
* mode: "EIP4337",
* smartAccount: {
* chain: sepolia, // chain required for EIP-4337
* sponsorGas: true,
* }
* },
* });
* ```
*
* ### Login with email
*
* To login with email, you can use the `preAuthenticate` function to first send a verification code to the user's email, then login with the verification code.
*
* ```ts
* import { inAppWallet, preAuthenticate } from "thirdweb/wallets/in-app";
*
Expand All @@ -73,22 +99,10 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
* });
* ```
*
* ### Login with SIWE
* ```ts
* import { inAppWallet, createWallet } from "thirdweb/wallets";
*
* const rabby = createWallet("io.rabby");
* const inAppWallet = inAppWallet();
* ### Login with phone number
*
* const account = await inAppWallet.connect({
* strategy: "wallet",
* chain: mainnet,
* wallet: rabby,
* client: MY_CLIENT
* });
* ```
* Similar to email, you can login with a phone number by first sending a verification code to the user's phone number, then login with the verification code.
*
* ### Login with phone number
* ```ts
* import { inAppWallet, preAuthenticate } from "thirdweb/wallets/in-app";
*
Expand All @@ -111,8 +125,28 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
* });
* ```
*
* ### Login with another wallet (SIWE)
*
* You can also login to the in-app wallet with another existing wallet by signing a standard Sign in with Ethereum (SIWE) message.
*
* ```ts
* import { inAppWallet, createWallet } from "thirdweb/wallets";
*
* const rabby = createWallet("io.rabby");
* const inAppWallet = inAppWallet();
*
* const account = await inAppWallet.connect({
* strategy: "wallet",
* chain: mainnet,
* wallet: rabby,
* client: MY_CLIENT
* });
* ```
*
* ### Login with passkey
*
* You can also login with a passkey. This mode requires specifying whether it should create a new passkey, or sign in with an existing passkey. We recommend checking if the user has a passkey stored in their browser to automatically login with it.
*
* ```ts
* import { inAppWallet, hasStoredPasskey } from "thirdweb/wallets/in-app";
*
Expand All @@ -128,6 +162,11 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
* ```
*
* ### Connect to a guest account
*
* You can also connect to a guest account, this will create a new account for the user instantly and store it in the browser's local storage.
*
* You can later "upgrade" this account by linking another auth method, like email or phone for example. This will preserve the account's address and history.
*
* ```ts
* import { inAppWallet } from "thirdweb/wallets";
*
Expand All @@ -141,19 +180,19 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
*
* ### Connect to a backend account
*
* for usage in backends, you might also need to provide a 'storage' to store auth tokens. In-memory usually works for most purposes.
* For usage in backends, you can create wallets with the `backend` strategy and a stable walletSecret.
*
* Make sure to keep that walletSecret safe as it is the key to access that wallet, never expose it to the client.
*
* ```ts
* import { inAppWallet } from "thirdweb/wallets";
*
* const wallet = inAppWallet({
* storage: inMemoryStorage, // for usage in backends/scripts
* });
* const wallet = inAppWallet();
*
* const account = await wallet.connect({
* client,
* strategy: "backend",
* walletSecret: "...", // Provided by your app
* walletSecret: "...", // Your own secret, keep it safe
* });
* ```
*
Expand Down Expand Up @@ -189,23 +228,30 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
* });
* ```
*
* ### Specify a logo for your login page (Connect UI)
* ### Specify a logo, icon and name for your login page (Connect UI)
*
* You can specify a logo, icon and name for your login page to customize how in-app wallets are displayed in the Connect UI components (ConnectButton and ConnectEmbed).
*
* ```ts
* import { inAppWallet } from "thirdweb/wallets";
* const wallet = inAppWallet({
* metadata: {
* image: {
* src: "https://example.com/logo.png",
* alt: "My logo",
* width: 100,
* height: 100,
* name: "My App",
* icon: "https://example.com/icon.png",
* image: {
* src: "https://example.com/logo.png",
* alt: "My logo",
* width: 100,
* height: 100,
* },
* },
* });
* ```
*
* ### Hide the ability to export the private key within the Connect Modal UI
*
* By default, the Connect Modal will show a button to export the private key of the wallet. You can hide this button by setting the `hidePrivateKeyExport` option to `true`.
*
* ```ts
* import { inAppWallet } from "thirdweb/wallets";
* const wallet = inAppWallet({
Expand All @@ -228,7 +274,7 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.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.
* By default, wallet state is stored in the browser's local storage if in the browser, or in-memory storage if not in the browser. You can override this behavior by providing a custom storage object, useful for server side and CLI integrations.
*
* ```ts
* import { inAppWallet } from "thirdweb/wallets";
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { describe, expect, it } from "vitest";
import { TEST_CLIENT } from "~test/test-clients.js";
import { sepolia } from "../../../../chains/chain-definitions/sepolia.js";
import { parseEventLogs } from "../../../../event/actions/parse-logs.js";
import { userOperationEventEvent } from "../../../../extensions/erc4337/__generated__/IEntryPoint/events/UserOperationEvent.js";
import { executedEvent } from "../../../../extensions/erc7702/__generated__/MinimalAccount/events/Executed.js";
import { sendAndConfirmTransaction } from "../../../../transaction/actions/send-and-confirm-transaction.js";
import { prepareTransaction } from "../../../../transaction/prepare-transaction.js";
import { inAppWallet } from "../in-app.js";
describe("InAppWallet Integration Tests", () => {
it("should sign a message with backend strategy", async () => {
const wallet = inAppWallet();
const account = await wallet.connect({
client: TEST_CLIENT,
strategy: "backend",
walletSecret: "test-secret",
});
expect(account.address).toBeDefined();
const message = await account.signMessage({
message: "Hello, world!",
});
expect(message).toBeDefined();
});

it("should sign a message with guest strategy", async () => {
const wallet = inAppWallet();
const account = await wallet.connect({
client: TEST_CLIENT,
strategy: "guest",
});
expect(account.address).toBeDefined();
const message = await account.signMessage({
message: "Hello, world!",
});
expect(message).toBeDefined();
});

it("should sponsor gas for a 7702 smart account", async () => {
const chain = sepolia;
const wallet = inAppWallet({
executionMode: {
mode: "EIP7702",
sponsorGas: true,
},
});
const account = await wallet.connect({
client: TEST_CLIENT,
strategy: "guest",
chain,
});
expect(account.address).toBeDefined();
const tx = await sendAndConfirmTransaction({
transaction: prepareTransaction({
chain,
client: TEST_CLIENT,
to: account.address,
value: 0n,
}),
account,
});
expect(tx.transactionHash).toBeDefined();
const logs = parseEventLogs({
logs: tx.logs,
events: [executedEvent()],
});
const executedLog = logs[0];
if (!executedLog) {
throw new Error("No executed log found");
}
expect(executedLog.args.to).toBe(account.address);
expect(executedLog.args.value).toBe(0n);
});

it("should sponsor gas for a 4337 smart account", async () => {
const chain = sepolia;
const wallet = inAppWallet({
executionMode: {
mode: "EIP4337",
smartAccount: {
chain,
sponsorGas: true,
},
},
});
const account = await wallet.connect({
client: TEST_CLIENT,
strategy: "guest",
chain,
});
expect(account.address).toBeDefined();
const tx = await sendAndConfirmTransaction({
transaction: prepareTransaction({
chain,
client: TEST_CLIENT,
to: account.address,
value: 0n,
}),
account,
});
expect(tx.transactionHash).toBeDefined();
const logs = parseEventLogs({
logs: tx.logs,
events: [userOperationEventEvent()],
});
const executedLog = logs[0];
if (!executedLog) {
throw new Error("No executed log found");
}
expect(executedLog.args.sender).toBe(account.address);
expect(executedLog.args.success).toBe(true);
});
});
12 changes: 11 additions & 1 deletion packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { ThirdwebClient } from "../../../../client/client.js";
import { getThirdwebBaseUrl } from "../../../../utils/domains.js";
import type { AsyncStorage } from "../../../../utils/storage/AsyncStorage.js";
import { inMemoryStorage } from "../../../../utils/storage/inMemoryStorage.js";
import { webLocalStorage } from "../../../../utils/storage/webStorage.js";
import type { SocialAuthOption } from "../../../../wallets/types.js";
import type { Account } from "../../../interfaces/wallet.js";
Expand Down Expand Up @@ -86,7 +88,7 @@
this.ecosystem = ecosystem;
this.passkeyDomain = passkeyDomain;
this.storage = new ClientScopedStorage({
storage: storage ?? webLocalStorage,
storage: storage ?? getDefaultStorage(),
clientId: client.clientId,
ecosystem: ecosystem,
});
Expand Down Expand Up @@ -489,3 +491,11 @@
function assertUnreachable(x: never, message?: string): never {
throw new Error(message ?? `Invalid param: ${x}`);
}

function getDefaultStorage(): AsyncStorage {
if (typeof window !== "undefined" && window.localStorage) {
return webLocalStorage;
}

Check warning on line 498 in packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts#L497-L498

Added lines #L497 - L498 were not covered by tests
// default to in-memory storage if we're not in the browser
return inMemoryStorage;
}
Loading