Skip to content

Commit da3fc99

Browse files
[SDK] Default to in-memory storage for inAppWallet outside browser (#7038)
1 parent fb4c190 commit da3fc99

File tree

5 files changed

+204
-58
lines changed

5 files changed

+204
-58
lines changed

.changeset/thin-rockets-walk.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+
Default to in-memory storage when creating inapp wallets outside the browser

packages/thirdweb/src/wallets/in-app/web/in-app.ts

Lines changed: 76 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,29 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
2323
* const account = await wallet.connect({
2424
* client,
2525
* chain,
26-
* strategy: "google",
26+
* strategy: "google", // or "apple", "facebook","discord", "github", "twitch", "x", "telegram", "line", "coinbase", etc
2727
* });
2828
* ```
2929
*
3030
* [View all available social auth methods](https://portal.thirdweb.com/connect/wallet/sign-in-methods/configure)
3131
*
3232
* ### Enable smart accounts and sponsor gas for your users:
3333
*
34+
* With the `executionMode` option, you can enable smart accounts and sponsor gas for your users.
35+
*
36+
* **Using EIP-7702** (recommended):
37+
*
38+
* 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.
39+
*
3440
* ```ts
3541
* import { inAppWallet } from "thirdweb/wallets";
3642
* import { sepolia } from "thirdweb/chains";
3743
*
3844
* const wallet = inAppWallet({
39-
* smartAccount: {
40-
* chain: sepolia,
45+
* executionMode: {
46+
* mode: "EIP7702",
4147
* sponsorGas: true,
42-
* },
48+
* },
4349
* });
4450
*
4551
* // account will be a smart account with sponsored gas enabled
@@ -49,8 +55,28 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
4955
* });
5056
* ```
5157
*
58+
* **Using EIP-4337**:
59+
*
60+
* 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.
61+
*
62+
* ```ts
63+
* import { inAppWallet } from "thirdweb/wallets/in-app";
64+
*
65+
* const wallet = inAppWallet({
66+
* executionMode: {
67+
* mode: "EIP4337",
68+
* smartAccount: {
69+
* chain: sepolia, // chain required for EIP-4337
70+
* sponsorGas: true,
71+
* }
72+
* },
73+
* });
74+
* ```
75+
*
5276
* ### Login with email
5377
*
78+
* 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.
79+
*
5480
* ```ts
5581
* import { inAppWallet, preAuthenticate } from "thirdweb/wallets/in-app";
5682
*
@@ -73,22 +99,10 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
7399
* });
74100
* ```
75101
*
76-
* ### Login with SIWE
77-
* ```ts
78-
* import { inAppWallet, createWallet } from "thirdweb/wallets";
79-
*
80-
* const rabby = createWallet("io.rabby");
81-
* const inAppWallet = inAppWallet();
102+
* ### Login with phone number
82103
*
83-
* const account = await inAppWallet.connect({
84-
* strategy: "wallet",
85-
* chain: mainnet,
86-
* wallet: rabby,
87-
* client: MY_CLIENT
88-
* });
89-
* ```
104+
* 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.
90105
*
91-
* ### Login with phone number
92106
* ```ts
93107
* import { inAppWallet, preAuthenticate } from "thirdweb/wallets/in-app";
94108
*
@@ -111,8 +125,28 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
111125
* });
112126
* ```
113127
*
128+
* ### Login with another wallet (SIWE)
129+
*
130+
* You can also login to the in-app wallet with another existing wallet by signing a standard Sign in with Ethereum (SIWE) message.
131+
*
132+
* ```ts
133+
* import { inAppWallet, createWallet } from "thirdweb/wallets";
134+
*
135+
* const rabby = createWallet("io.rabby");
136+
* const inAppWallet = inAppWallet();
137+
*
138+
* const account = await inAppWallet.connect({
139+
* strategy: "wallet",
140+
* chain: mainnet,
141+
* wallet: rabby,
142+
* client: MY_CLIENT
143+
* });
144+
* ```
145+
*
114146
* ### Login with passkey
115147
*
148+
* 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.
149+
*
116150
* ```ts
117151
* import { inAppWallet, hasStoredPasskey } from "thirdweb/wallets/in-app";
118152
*
@@ -128,6 +162,11 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
128162
* ```
129163
*
130164
* ### Connect to a guest account
165+
*
166+
* 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.
167+
*
168+
* 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.
169+
*
131170
* ```ts
132171
* import { inAppWallet } from "thirdweb/wallets";
133172
*
@@ -141,19 +180,19 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
141180
*
142181
* ### Connect to a backend account
143182
*
144-
* for usage in backends, you might also need to provide a 'storage' to store auth tokens. In-memory usually works for most purposes.
183+
* For usage in backends, you can create wallets with the `backend` strategy and a stable walletSecret.
184+
*
185+
* Make sure to keep that walletSecret safe as it is the key to access that wallet, never expose it to the client.
145186
*
146187
* ```ts
147188
* import { inAppWallet } from "thirdweb/wallets";
148189
*
149-
* const wallet = inAppWallet({
150-
* storage: inMemoryStorage, // for usage in backends/scripts
151-
* });
190+
* const wallet = inAppWallet();
152191
*
153192
* const account = await wallet.connect({
154193
* client,
155194
* strategy: "backend",
156-
* walletSecret: "...", // Provided by your app
195+
* walletSecret: "...", // Your own secret, keep it safe
157196
* });
158197
* ```
159198
*
@@ -189,23 +228,30 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
189228
* });
190229
* ```
191230
*
192-
* ### Specify a logo for your login page (Connect UI)
231+
* ### Specify a logo, icon and name for your login page (Connect UI)
232+
*
233+
* 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).
234+
*
193235
* ```ts
194236
* import { inAppWallet } from "thirdweb/wallets";
195237
* const wallet = inAppWallet({
196238
* metadata: {
197-
* image: {
198-
* src: "https://example.com/logo.png",
199-
* alt: "My logo",
200-
* width: 100,
201-
* height: 100,
239+
* name: "My App",
240+
* icon: "https://example.com/icon.png",
241+
* image: {
242+
* src: "https://example.com/logo.png",
243+
* alt: "My logo",
244+
* width: 100,
245+
* height: 100,
202246
* },
203247
* },
204248
* });
205249
* ```
206250
*
207251
* ### Hide the ability to export the private key within the Connect Modal UI
208252
*
253+
* 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`.
254+
*
209255
* ```ts
210256
* import { inAppWallet } from "thirdweb/wallets";
211257
* const wallet = inAppWallet({
@@ -228,7 +274,7 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
228274
*
229275
* ### Override storage for the wallet state
230276
*
231-
* 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.
277+
* 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.
232278
*
233279
* ```ts
234280
* import { inAppWallet } from "thirdweb/wallets";

packages/thirdweb/src/wallets/in-app/web/lib/in-app-backend.test.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, expect, it } from "vitest";
2+
import { TEST_CLIENT } from "~test/test-clients.js";
3+
import { sepolia } from "../../../../chains/chain-definitions/sepolia.js";
4+
import { parseEventLogs } from "../../../../event/actions/parse-logs.js";
5+
import { userOperationEventEvent } from "../../../../extensions/erc4337/__generated__/IEntryPoint/events/UserOperationEvent.js";
6+
import { executedEvent } from "../../../../extensions/erc7702/__generated__/MinimalAccount/events/Executed.js";
7+
import { sendAndConfirmTransaction } from "../../../../transaction/actions/send-and-confirm-transaction.js";
8+
import { prepareTransaction } from "../../../../transaction/prepare-transaction.js";
9+
import { inAppWallet } from "../in-app.js";
10+
describe("InAppWallet Integration Tests", () => {
11+
it("should sign a message with backend strategy", async () => {
12+
const wallet = inAppWallet();
13+
const account = await wallet.connect({
14+
client: TEST_CLIENT,
15+
strategy: "backend",
16+
walletSecret: "test-secret",
17+
});
18+
expect(account.address).toBeDefined();
19+
const message = await account.signMessage({
20+
message: "Hello, world!",
21+
});
22+
expect(message).toBeDefined();
23+
});
24+
25+
it("should sign a message with guest strategy", async () => {
26+
const wallet = inAppWallet();
27+
const account = await wallet.connect({
28+
client: TEST_CLIENT,
29+
strategy: "guest",
30+
});
31+
expect(account.address).toBeDefined();
32+
const message = await account.signMessage({
33+
message: "Hello, world!",
34+
});
35+
expect(message).toBeDefined();
36+
});
37+
38+
it("should sponsor gas for a 7702 smart account", async () => {
39+
const chain = sepolia;
40+
const wallet = inAppWallet({
41+
executionMode: {
42+
mode: "EIP7702",
43+
sponsorGas: true,
44+
},
45+
});
46+
const account = await wallet.connect({
47+
client: TEST_CLIENT,
48+
strategy: "guest",
49+
chain,
50+
});
51+
expect(account.address).toBeDefined();
52+
const tx = await sendAndConfirmTransaction({
53+
transaction: prepareTransaction({
54+
chain,
55+
client: TEST_CLIENT,
56+
to: account.address,
57+
value: 0n,
58+
}),
59+
account,
60+
});
61+
expect(tx.transactionHash).toBeDefined();
62+
const logs = parseEventLogs({
63+
logs: tx.logs,
64+
events: [executedEvent()],
65+
});
66+
const executedLog = logs[0];
67+
if (!executedLog) {
68+
throw new Error("No executed log found");
69+
}
70+
expect(executedLog.args.to).toBe(account.address);
71+
expect(executedLog.args.value).toBe(0n);
72+
});
73+
74+
it("should sponsor gas for a 4337 smart account", async () => {
75+
const chain = sepolia;
76+
const wallet = inAppWallet({
77+
executionMode: {
78+
mode: "EIP4337",
79+
smartAccount: {
80+
chain,
81+
sponsorGas: true,
82+
},
83+
},
84+
});
85+
const account = await wallet.connect({
86+
client: TEST_CLIENT,
87+
strategy: "guest",
88+
chain,
89+
});
90+
expect(account.address).toBeDefined();
91+
const tx = await sendAndConfirmTransaction({
92+
transaction: prepareTransaction({
93+
chain,
94+
client: TEST_CLIENT,
95+
to: account.address,
96+
value: 0n,
97+
}),
98+
account,
99+
});
100+
expect(tx.transactionHash).toBeDefined();
101+
const logs = parseEventLogs({
102+
logs: tx.logs,
103+
events: [userOperationEventEvent()],
104+
});
105+
const executedLog = logs[0];
106+
if (!executedLog) {
107+
throw new Error("No executed log found");
108+
}
109+
expect(executedLog.args.sender).toBe(account.address);
110+
expect(executedLog.args.success).toBe(true);
111+
});
112+
});

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { ThirdwebClient } from "../../../../client/client.js";
22
import { getThirdwebBaseUrl } from "../../../../utils/domains.js";
3+
import type { AsyncStorage } from "../../../../utils/storage/AsyncStorage.js";
4+
import { inMemoryStorage } from "../../../../utils/storage/inMemoryStorage.js";
35
import { webLocalStorage } from "../../../../utils/storage/webStorage.js";
46
import type { SocialAuthOption } from "../../../../wallets/types.js";
57
import type { Account } from "../../../interfaces/wallet.js";
@@ -86,7 +88,7 @@ export class InAppWebConnector implements InAppConnector {
8688
this.ecosystem = ecosystem;
8789
this.passkeyDomain = passkeyDomain;
8890
this.storage = new ClientScopedStorage({
89-
storage: storage ?? webLocalStorage,
91+
storage: storage ?? getDefaultStorage(),
9092
clientId: client.clientId,
9193
ecosystem: ecosystem,
9294
});
@@ -489,3 +491,11 @@ export class InAppWebConnector implements InAppConnector {
489491
function assertUnreachable(x: never, message?: string): never {
490492
throw new Error(message ?? `Invalid param: ${x}`);
491493
}
494+
495+
function getDefaultStorage(): AsyncStorage {
496+
if (typeof window !== "undefined" && window.localStorage) {
497+
return webLocalStorage;
498+
}
499+
// default to in-memory storage if we're not in the browser
500+
return inMemoryStorage;
501+
}

0 commit comments

Comments
 (0)