Skip to content

Commit 233001c

Browse files
Default to in-memory storage for inAppWallet outside browser
1 parent 1c31a1e commit 233001c

File tree

6 files changed

+207
-60
lines changed

6 files changed

+207
-60
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: 88 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import type { ThirdwebClient } from "../../../client/client.js";
2+
import type { AsyncStorage } from "../../../utils/storage/AsyncStorage.js";
3+
import { inMemoryStorage } from "../../../utils/storage/inMemoryStorage.js";
4+
import { webLocalStorage } from "../../../utils/storage/webStorage.js";
25
import type { Wallet } from "../../interfaces/wallet.js";
36
import { createInAppWallet } from "../core/wallet/in-app-core.js";
47
import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
@@ -23,23 +26,29 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
2326
* const account = await wallet.connect({
2427
* client,
2528
* chain,
26-
* strategy: "google",
29+
* strategy: "google", // or "apple", "facebook","discord", "github", "twitch", "x", "telegram", "line", "coinbase", etc
2730
* });
2831
* ```
2932
*
3033
* [View all available social auth methods](https://portal.thirdweb.com/connect/wallet/sign-in-methods/configure)
3134
*
3235
* ### Enable smart accounts and sponsor gas for your users:
3336
*
37+
* With the `executionMode` option, you can enable smart accounts and sponsor gas for your users.
38+
*
39+
* **Using EIP-7702** (recommended):
40+
*
41+
* 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.
42+
*
3443
* ```ts
3544
* import { inAppWallet } from "thirdweb/wallets";
3645
* import { sepolia } from "thirdweb/chains";
3746
*
3847
* const wallet = inAppWallet({
39-
* smartAccount: {
40-
* chain: sepolia,
48+
* executionMode: {
49+
* mode: "EIP7702",
4150
* sponsorGas: true,
42-
* },
51+
* },
4352
* });
4453
*
4554
* // account will be a smart account with sponsored gas enabled
@@ -49,8 +58,28 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
4958
* });
5059
* ```
5160
*
61+
* **Using EIP-4337**:
62+
*
63+
* 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.
64+
*
65+
* ```ts
66+
* import { inAppWallet } from "thirdweb/wallets/in-app";
67+
*
68+
* const wallet = inAppWallet({
69+
* executionMode: {
70+
* mode: "EIP4337",
71+
* smartAccount: {
72+
* chain: sepolia, // chain required for EIP-4337
73+
* sponsorGas: true,
74+
* }
75+
* },
76+
* });
77+
* ```
78+
*
5279
* ### Login with email
5380
*
81+
* 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.
82+
*
5483
* ```ts
5584
* import { inAppWallet, preAuthenticate } from "thirdweb/wallets/in-app";
5685
*
@@ -73,22 +102,10 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
73102
* });
74103
* ```
75104
*
76-
* ### Login with SIWE
77-
* ```ts
78-
* import { inAppWallet, createWallet } from "thirdweb/wallets";
79-
*
80-
* const rabby = createWallet("io.rabby");
81-
* const inAppWallet = inAppWallet();
105+
* ### Login with phone number
82106
*
83-
* const account = await inAppWallet.connect({
84-
* strategy: "wallet",
85-
* chain: mainnet,
86-
* wallet: rabby,
87-
* client: MY_CLIENT
88-
* });
89-
* ```
107+
* 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.
90108
*
91-
* ### Login with phone number
92109
* ```ts
93110
* import { inAppWallet, preAuthenticate } from "thirdweb/wallets/in-app";
94111
*
@@ -111,8 +128,28 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
111128
* });
112129
* ```
113130
*
131+
* ### Login with another wallet (SIWE)
132+
*
133+
* You can also login to the in-app wallet with another existing wallet by signing a standard Sign in with Ethereum (SIWE) message.
134+
*
135+
* ```ts
136+
* import { inAppWallet, createWallet } from "thirdweb/wallets";
137+
*
138+
* const rabby = createWallet("io.rabby");
139+
* const inAppWallet = inAppWallet();
140+
*
141+
* const account = await inAppWallet.connect({
142+
* strategy: "wallet",
143+
* chain: mainnet,
144+
* wallet: rabby,
145+
* client: MY_CLIENT
146+
* });
147+
* ```
148+
*
114149
* ### Login with passkey
115150
*
151+
* 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.
152+
*
116153
* ```ts
117154
* import { inAppWallet, hasStoredPasskey } from "thirdweb/wallets/in-app";
118155
*
@@ -128,6 +165,11 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
128165
* ```
129166
*
130167
* ### Connect to a guest account
168+
*
169+
* 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.
170+
*
171+
* 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.
172+
*
131173
* ```ts
132174
* import { inAppWallet } from "thirdweb/wallets";
133175
*
@@ -141,19 +183,19 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
141183
*
142184
* ### Connect to a backend account
143185
*
144-
* for usage in backends, you might also need to provide a 'storage' to store auth tokens. In-memory usually works for most purposes.
186+
* For usage in backends, you can create wallets with the `backend` strategy and a stable walletSecret.
187+
*
188+
* Make sure to keep that walletSecret safe as it is the key to access that wallet, never expose it to the client.
145189
*
146190
* ```ts
147191
* import { inAppWallet } from "thirdweb/wallets";
148192
*
149-
* const wallet = inAppWallet({
150-
* storage: inMemoryStorage, // for usage in backends/scripts
151-
* });
193+
* const wallet = inAppWallet();
152194
*
153195
* const account = await wallet.connect({
154196
* client,
155197
* strategy: "backend",
156-
* walletSecret: "...", // Provided by your app
198+
* walletSecret: "...", // Your own secret, keep it safe
157199
* });
158200
* ```
159201
*
@@ -189,23 +231,30 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
189231
* });
190232
* ```
191233
*
192-
* ### Specify a logo for your login page (Connect UI)
234+
* ### Specify a logo, icon and name for your login page (Connect UI)
235+
*
236+
* 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).
237+
*
193238
* ```ts
194239
* import { inAppWallet } from "thirdweb/wallets";
195240
* const wallet = inAppWallet({
196241
* metadata: {
197-
* image: {
198-
* src: "https://example.com/logo.png",
199-
* alt: "My logo",
200-
* width: 100,
201-
* height: 100,
242+
* name: "My App",
243+
* icon: "https://example.com/icon.png",
244+
* image: {
245+
* src: "https://example.com/logo.png",
246+
* alt: "My logo",
247+
* width: 100,
248+
* height: 100,
202249
* },
203250
* },
204251
* });
205252
* ```
206253
*
207254
* ### Hide the ability to export the private key within the Connect Modal UI
208255
*
256+
* 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`.
257+
*
209258
* ```ts
210259
* import { inAppWallet } from "thirdweb/wallets";
211260
* const wallet = inAppWallet({
@@ -228,7 +277,7 @@ import type { InAppWalletCreationOptions } from "../core/wallet/types.js";
228277
*
229278
* ### Override storage for the wallet state
230279
*
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.
280+
* 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.
232281
*
233282
* ```ts
234283
* import { inAppWallet } from "thirdweb/wallets";
@@ -264,8 +313,16 @@ export function inAppWallet(
264313
return new InAppWebConnector({
265314
client,
266315
passkeyDomain: createOptions?.auth?.passkeyDomain,
267-
storage: createOptions?.storage,
316+
storage: createOptions?.storage ?? getDefaultStorage(),
268317
});
269318
},
270319
}) as Wallet<"inApp">;
271320
}
321+
322+
function getDefaultStorage(): AsyncStorage {
323+
if (typeof window !== "undefined" && window.localStorage) {
324+
return webLocalStorage;
325+
}
326+
// default to in-memory storage if we're not in the browser
327+
return inMemoryStorage;
328+
}

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export class InAppWebConnector implements InAppConnector {
8686
this.ecosystem = ecosystem;
8787
this.passkeyDomain = passkeyDomain;
8888
this.storage = new ClientScopedStorage({
89-
storage: storage ?? webLocalStorage,
89+
storage,
9090
clientId: client.clientId,
9191
ecosystem: ecosystem,
9292
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export type InAppWalletConstructorType = ClientIdConstructorType & {
3333
/**
3434
* The storage to use for storing wallet state
3535
*/
36-
storage?: AsyncStorage;
36+
storage: AsyncStorage;
3737
};
3838

3939
export type ClientIdWithQuerierType = ClientIdConstructorType & {

0 commit comments

Comments
 (0)