@thru/wallet 0.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/android/build.gradle +37 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/org/thru/walletnative/ThruWebViewBridgeModule.kt +77 -0
- package/app.plugin.cjs +101 -0
- package/dist/BrowserSDK-CpRFiJsW.d.ts +409 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +941 -0
- package/dist/index.js.map +1 -0
- package/dist/native/react.d.ts +109 -0
- package/dist/native/react.js +2381 -0
- package/dist/native/react.js.map +1 -0
- package/dist/native.d.ts +329 -0
- package/dist/native.js +1126 -0
- package/dist/native.js.map +1 -0
- package/dist/react-ui.d.ts +5 -0
- package/dist/react-ui.js +266 -0
- package/dist/react-ui.js.map +1 -0
- package/dist/react.d.ts +66 -0
- package/dist/react.js +1151 -0
- package/dist/react.js.map +1 -0
- package/expo-module.config.json +6 -0
- package/package.json +114 -0
- package/src/BrowserSDK.ts +315 -0
- package/src/index.ts +27 -0
- package/src/interfaces/IThruChain.ts +37 -0
- package/src/interfaces/accounts.ts +61 -0
- package/src/interfaces/index.ts +9 -0
- package/src/interfaces/types.ts +95 -0
- package/src/native/NativeSDK.test.ts +819 -0
- package/src/native/NativeSDK.ts +773 -0
- package/src/native/index.ts +39 -0
- package/src/native/provider/NativeProvider.ts +363 -0
- package/src/native/provider/WebViewBridge.test.ts +339 -0
- package/src/native/provider/WebViewBridge.ts +339 -0
- package/src/native/provider/chains/ThruChain.ts +85 -0
- package/src/native/provider/shell.html +88 -0
- package/src/native/provider/shell.test.ts +56 -0
- package/src/native/provider/shell.ts +111 -0
- package/src/native/provider/shims-html.d.ts +4 -0
- package/src/native/react/ThruContext.ts +37 -0
- package/src/native/react/ThruProvider.tsx +168 -0
- package/src/native/react/ThruWalletSheet.tsx +1162 -0
- package/src/native/react/android-webauthn.ts +37 -0
- package/src/native/react/hooks/useAccounts.ts +35 -0
- package/src/native/react/hooks/useThru.ts +11 -0
- package/src/native/react/hooks/useWallet.ts +71 -0
- package/src/native/react/hooks/useWalletAvailability.ts +31 -0
- package/src/native/react/hooks/waitForWallet.ts +21 -0
- package/src/native/react/index.ts +29 -0
- package/src/protocol/index.ts +2 -0
- package/src/protocol/postMessage.ts +283 -0
- package/src/protocol/walletState.ts +12 -0
- package/src/provider/EmbeddedProvider.ts +330 -0
- package/src/provider/IframeManager.ts +438 -0
- package/src/provider/chains/ThruChain.ts +86 -0
- package/src/provider/index.ts +17 -0
- package/src/provider/types/messages.ts +37 -0
- package/src/react/ThruContext.ts +31 -0
- package/src/react/ThruProvider.tsx +169 -0
- package/src/react/hooks/useAccounts.ts +38 -0
- package/src/react/hooks/useThru.ts +11 -0
- package/src/react/hooks/useWallet.ts +81 -0
- package/src/react/index.ts +30 -0
- package/src/react-ui/ThruAccountSwitcher.tsx +187 -0
- package/src/react-ui/custom.d.ts +8 -0
- package/src/react-ui/index.ts +1 -0
- package/src/static/logo.png +0 -0
- package/src/static/logomark_red.svg +11 -0
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
ErrorCode,
|
|
4
|
+
EMBEDDED_PROVIDER_EVENTS,
|
|
5
|
+
IFRAME_READY_EVENT,
|
|
6
|
+
POST_MESSAGE_EVENT_TYPE,
|
|
7
|
+
POST_MESSAGE_REQUEST_TYPES,
|
|
8
|
+
} from "../protocol";
|
|
9
|
+
import { NativeSDK } from "./NativeSDK";
|
|
10
|
+
import type { WebViewMessageEventLike } from "./provider/WebViewBridge";
|
|
11
|
+
|
|
12
|
+
class MockWebView {
|
|
13
|
+
injected: string[] = [];
|
|
14
|
+
injectJavaScript = (script: string): void => {
|
|
15
|
+
this.injected.push(script);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class MockStorage {
|
|
20
|
+
values = new Map<string, string>();
|
|
21
|
+
getItem = (key: string): string | null => this.values.get(key) ?? null;
|
|
22
|
+
setItem = (key: string, value: string): void => {
|
|
23
|
+
this.values.set(key, value);
|
|
24
|
+
};
|
|
25
|
+
removeItem = (key: string): void => {
|
|
26
|
+
this.values.delete(key);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function frameIdFor(sdk: NativeSDK): string {
|
|
31
|
+
const frameId = new URL(sdk.getIframeSrc()).searchParams.get("tn_frame_id");
|
|
32
|
+
if (!frameId) throw new Error("Missing frame id");
|
|
33
|
+
return frameId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readyMessage(frameId: string): WebViewMessageEventLike {
|
|
37
|
+
return {
|
|
38
|
+
nativeEvent: {
|
|
39
|
+
data: JSON.stringify({
|
|
40
|
+
type: IFRAME_READY_EVENT,
|
|
41
|
+
frameId,
|
|
42
|
+
data: { ready: true },
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function responseMessage(
|
|
49
|
+
frameId: string,
|
|
50
|
+
id: string,
|
|
51
|
+
result: unknown,
|
|
52
|
+
): WebViewMessageEventLike {
|
|
53
|
+
return {
|
|
54
|
+
nativeEvent: {
|
|
55
|
+
data: JSON.stringify({ id, frameId, success: true, result }),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function rejectedResponseMessage(
|
|
61
|
+
frameId: string,
|
|
62
|
+
id: string,
|
|
63
|
+
code: ErrorCode,
|
|
64
|
+
message: string,
|
|
65
|
+
): WebViewMessageEventLike {
|
|
66
|
+
return {
|
|
67
|
+
nativeEvent: {
|
|
68
|
+
data: JSON.stringify({
|
|
69
|
+
id,
|
|
70
|
+
frameId,
|
|
71
|
+
success: false,
|
|
72
|
+
error: { code, message },
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function eventMessage(
|
|
79
|
+
frameId: string,
|
|
80
|
+
event: string,
|
|
81
|
+
data?: unknown,
|
|
82
|
+
): WebViewMessageEventLike {
|
|
83
|
+
return {
|
|
84
|
+
nativeEvent: {
|
|
85
|
+
data: JSON.stringify({
|
|
86
|
+
type: POST_MESSAGE_EVENT_TYPE,
|
|
87
|
+
frameId,
|
|
88
|
+
event,
|
|
89
|
+
data,
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function flush(): Promise<void> {
|
|
96
|
+
for (let i = 0; i < 8; i++) await Promise.resolve();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseInjectedRequest(script: string): {
|
|
100
|
+
id: string;
|
|
101
|
+
type: string;
|
|
102
|
+
payload?: unknown;
|
|
103
|
+
origin: string;
|
|
104
|
+
} {
|
|
105
|
+
const match = script.match(/var msg = (.*?);\s*if \(window\.__pushIn\)/s);
|
|
106
|
+
if (!match) throw new Error("Injected request not found");
|
|
107
|
+
return JSON.parse(match[1]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe("NativeSDK", () => {
|
|
111
|
+
let sdk: NativeSDK;
|
|
112
|
+
let webView: MockWebView;
|
|
113
|
+
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
sdk = new NativeSDK({
|
|
116
|
+
walletUrl: "http://localhost:3000/embedded",
|
|
117
|
+
origin: "thru-mobile://token-dummy",
|
|
118
|
+
});
|
|
119
|
+
webView = new MockWebView();
|
|
120
|
+
sdk.attachWebView(webView);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
afterEach(() => {
|
|
124
|
+
sdk.destroy();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("defaults iOS WebView mode to shell iframe", () => {
|
|
128
|
+
expect(sdk.getIosWebViewMode()).toBe("shell-iframe");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("can opt into direct iOS WebView mode", () => {
|
|
132
|
+
const directSdk = new NativeSDK({
|
|
133
|
+
walletUrl: "http://localhost:3000/embedded",
|
|
134
|
+
origin: "thru-mobile://token-dummy",
|
|
135
|
+
iosWebViewMode: "direct",
|
|
136
|
+
});
|
|
137
|
+
expect(directSdk.getIosWebViewMode()).toBe("direct");
|
|
138
|
+
directSdk.destroy();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("maps signIn snake-case app metadata to wallet connect metadata", async () => {
|
|
142
|
+
const frameId = frameIdFor(sdk);
|
|
143
|
+
const promise = sdk.signIn({
|
|
144
|
+
app_id: "token_dummy_app",
|
|
145
|
+
app_display_name: "Token Dummy App",
|
|
146
|
+
app_url: "https://token-dummy.thru.org",
|
|
147
|
+
image_url: "https://token-dummy.thru.org/icon.png",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
sdk.onMessage(readyMessage(frameId));
|
|
151
|
+
await flush();
|
|
152
|
+
|
|
153
|
+
expect(webView.injected).toHaveLength(1);
|
|
154
|
+
const request = parseInjectedRequest(webView.injected[0]);
|
|
155
|
+
expect(request.type).toBe(POST_MESSAGE_REQUEST_TYPES.CONNECT);
|
|
156
|
+
expect(request.origin).toBe("thru-mobile://token-dummy");
|
|
157
|
+
expect(request.payload).toEqual({
|
|
158
|
+
metadata: {
|
|
159
|
+
appId: "token_dummy_app",
|
|
160
|
+
appName: "Token Dummy App",
|
|
161
|
+
appUrl: "https://token-dummy.thru.org",
|
|
162
|
+
imageUrl: "https://token-dummy.thru.org/icon.png",
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = {
|
|
167
|
+
accounts: [
|
|
168
|
+
{
|
|
169
|
+
accountType: "thru",
|
|
170
|
+
address: "thru_test_address",
|
|
171
|
+
label: "Account 1",
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
metadata: {
|
|
175
|
+
appId: "token_dummy_app",
|
|
176
|
+
appName: "Token Dummy App",
|
|
177
|
+
appUrl: "https://token-dummy.thru.org",
|
|
178
|
+
imageUrl: "https://token-dummy.thru.org/icon.png",
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
sdk.onMessage(responseMessage(frameId, request.id, result));
|
|
182
|
+
|
|
183
|
+
await expect(promise).resolves.toEqual({
|
|
184
|
+
...result,
|
|
185
|
+
selectedAccount: result.accounts[0],
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("opens a fresh connect request for account switching", async () => {
|
|
190
|
+
const storage = new MockStorage();
|
|
191
|
+
const selectedAccountStorageKey = "test-selected-account";
|
|
192
|
+
const initialAccount = {
|
|
193
|
+
accountType: "thru",
|
|
194
|
+
address: "thru_test_address_1",
|
|
195
|
+
label: "Account 1",
|
|
196
|
+
};
|
|
197
|
+
const switchedAccount = {
|
|
198
|
+
accountType: "thru",
|
|
199
|
+
address: "thru_test_address_2",
|
|
200
|
+
label: "Account 2",
|
|
201
|
+
};
|
|
202
|
+
storage.setItem(
|
|
203
|
+
selectedAccountStorageKey,
|
|
204
|
+
JSON.stringify({
|
|
205
|
+
version: 1,
|
|
206
|
+
origin: "thru-mobile://token-dummy",
|
|
207
|
+
walletOrigin: "http://localhost:3000",
|
|
208
|
+
savedAt: new Date().toISOString(),
|
|
209
|
+
selectedAccountAddress: initialAccount.address,
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
sdk.destroy();
|
|
214
|
+
sdk = new NativeSDK({
|
|
215
|
+
walletUrl: "http://localhost:3000/embedded",
|
|
216
|
+
origin: "thru-mobile://token-dummy",
|
|
217
|
+
storage,
|
|
218
|
+
selectedAccountStorageKey,
|
|
219
|
+
});
|
|
220
|
+
webView = new MockWebView();
|
|
221
|
+
sdk.attachWebView(webView);
|
|
222
|
+
|
|
223
|
+
const frameId = frameIdFor(sdk);
|
|
224
|
+
const connectPromise = sdk.signIn({
|
|
225
|
+
app_id: "token_dummy_app",
|
|
226
|
+
app_display_name: "Token Dummy App",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
sdk.onMessage(readyMessage(frameId));
|
|
230
|
+
await flush();
|
|
231
|
+
|
|
232
|
+
const connectRequest = parseInjectedRequest(webView.injected[0]);
|
|
233
|
+
sdk.onMessage(
|
|
234
|
+
responseMessage(frameId, connectRequest.id, {
|
|
235
|
+
accounts: [initialAccount],
|
|
236
|
+
selectedAccount: initialAccount,
|
|
237
|
+
status: "completed",
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
await connectPromise;
|
|
241
|
+
|
|
242
|
+
const switchPromise = sdk.signIn({
|
|
243
|
+
app_id: "token_dummy_app",
|
|
244
|
+
app_display_name: "Token Dummy App",
|
|
245
|
+
intent: "switch-account",
|
|
246
|
+
});
|
|
247
|
+
await flush();
|
|
248
|
+
|
|
249
|
+
expect(webView.injected).toHaveLength(2);
|
|
250
|
+
const switchRequest = parseInjectedRequest(webView.injected[1]);
|
|
251
|
+
expect(switchRequest.type).toBe(POST_MESSAGE_REQUEST_TYPES.CONNECT);
|
|
252
|
+
expect(switchRequest.payload).toEqual({
|
|
253
|
+
metadata: {
|
|
254
|
+
appId: "token_dummy_app",
|
|
255
|
+
appName: "Token Dummy App",
|
|
256
|
+
},
|
|
257
|
+
intent: "switch-account",
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const switchResult = {
|
|
261
|
+
accounts: [initialAccount, switchedAccount],
|
|
262
|
+
selectedAccount: switchedAccount,
|
|
263
|
+
status: "completed",
|
|
264
|
+
};
|
|
265
|
+
sdk.onMessage(responseMessage(frameId, switchRequest.id, switchResult));
|
|
266
|
+
|
|
267
|
+
await expect(switchPromise).resolves.toEqual({
|
|
268
|
+
...switchResult,
|
|
269
|
+
accounts: [switchedAccount],
|
|
270
|
+
});
|
|
271
|
+
expect(sdk.getAccounts()).toEqual([switchedAccount]);
|
|
272
|
+
expect(sdk.getSelectedAccount()).toEqual(switchedAccount);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("does not persist or restore native connection snapshots", async () => {
|
|
276
|
+
const storage = new MockStorage();
|
|
277
|
+
const storageKey = "test-connection";
|
|
278
|
+
const selectedAccountStorageKey = `${storageKey}.selected-account.v1`;
|
|
279
|
+
sdk.destroy();
|
|
280
|
+
sdk = new NativeSDK({
|
|
281
|
+
walletUrl: "http://localhost:3000/embedded",
|
|
282
|
+
origin: "thru-mobile://token-dummy",
|
|
283
|
+
storage,
|
|
284
|
+
storageKey,
|
|
285
|
+
});
|
|
286
|
+
webView = new MockWebView();
|
|
287
|
+
sdk.attachWebView(webView);
|
|
288
|
+
|
|
289
|
+
const frameId = frameIdFor(sdk);
|
|
290
|
+
const promise = sdk.signIn({
|
|
291
|
+
app_id: "token_dummy_app",
|
|
292
|
+
app_display_name: "Token Dummy App",
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
sdk.onMessage(readyMessage(frameId));
|
|
296
|
+
await flush();
|
|
297
|
+
|
|
298
|
+
const request = parseInjectedRequest(webView.injected[0]);
|
|
299
|
+
const initialAccount = {
|
|
300
|
+
accountType: "thru",
|
|
301
|
+
address: "thru_test_address_1",
|
|
302
|
+
label: "Account 1",
|
|
303
|
+
};
|
|
304
|
+
const selectedAccount = {
|
|
305
|
+
accountType: "thru",
|
|
306
|
+
address: "thru_test_address_2",
|
|
307
|
+
label: "Account 2",
|
|
308
|
+
};
|
|
309
|
+
const result = {
|
|
310
|
+
accounts: [initialAccount, selectedAccount],
|
|
311
|
+
selectedAccount,
|
|
312
|
+
metadata: {
|
|
313
|
+
appId: "token_dummy_app",
|
|
314
|
+
appName: "Token Dummy App",
|
|
315
|
+
appUrl: "thru-mobile://token-dummy",
|
|
316
|
+
},
|
|
317
|
+
status: "completed",
|
|
318
|
+
};
|
|
319
|
+
sdk.onMessage(responseMessage(frameId, request.id, result));
|
|
320
|
+
|
|
321
|
+
await expect(promise).resolves.toEqual({
|
|
322
|
+
...result,
|
|
323
|
+
accounts: [selectedAccount],
|
|
324
|
+
});
|
|
325
|
+
expect(storage.values.has(storageKey)).toBe(false);
|
|
326
|
+
const storedSelectedRaw =
|
|
327
|
+
storage.values.get(selectedAccountStorageKey) ?? "{}";
|
|
328
|
+
const storedSelected = JSON.parse(storedSelectedRaw);
|
|
329
|
+
expect(storedSelected.selectedAccountAddress).toBe(selectedAccount.address);
|
|
330
|
+
expect(storedSelected).not.toHaveProperty("result");
|
|
331
|
+
expect(storedSelected).not.toHaveProperty("accounts");
|
|
332
|
+
expect(storedSelectedRaw.toLowerCase()).not.toContain("passkey");
|
|
333
|
+
|
|
334
|
+
const restored = new NativeSDK({
|
|
335
|
+
walletUrl: "http://localhost:3000/embedded",
|
|
336
|
+
origin: "thru-mobile://token-dummy",
|
|
337
|
+
storage,
|
|
338
|
+
storageKey,
|
|
339
|
+
});
|
|
340
|
+
await expect(restored.restoreConnection()).resolves.toBeNull();
|
|
341
|
+
expect(restored.isConnected()).toBe(false);
|
|
342
|
+
expect(restored.getAccounts()).toEqual([]);
|
|
343
|
+
expect(storage.values.has(storageKey)).toBe(false);
|
|
344
|
+
restored.destroy();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("opens account settings through the wallet WebView", async () => {
|
|
348
|
+
const frameId = frameIdFor(sdk);
|
|
349
|
+
const connectPromise = sdk.signIn({
|
|
350
|
+
app_id: "token_dummy_app",
|
|
351
|
+
app_display_name: "Token Dummy App",
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
sdk.onMessage(readyMessage(frameId));
|
|
355
|
+
await flush();
|
|
356
|
+
|
|
357
|
+
const connectRequest = parseInjectedRequest(webView.injected[0]);
|
|
358
|
+
const initialAccount = {
|
|
359
|
+
accountType: "thru",
|
|
360
|
+
address: "thru_test_address_1",
|
|
361
|
+
label: "Account 1",
|
|
362
|
+
};
|
|
363
|
+
sdk.onMessage(
|
|
364
|
+
responseMessage(frameId, connectRequest.id, {
|
|
365
|
+
accounts: [initialAccount],
|
|
366
|
+
status: "completed",
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
369
|
+
await connectPromise;
|
|
370
|
+
|
|
371
|
+
const managedAccount = {
|
|
372
|
+
accountType: "thru",
|
|
373
|
+
address: "thru_test_address_2",
|
|
374
|
+
label: "Account 2",
|
|
375
|
+
};
|
|
376
|
+
const managePromise = sdk.manageAccounts();
|
|
377
|
+
await flush();
|
|
378
|
+
|
|
379
|
+
expect(webView.injected).toHaveLength(2);
|
|
380
|
+
const manageRequest = parseInjectedRequest(webView.injected[1]);
|
|
381
|
+
expect(manageRequest.type).toBe(POST_MESSAGE_REQUEST_TYPES.MANAGE_ACCOUNTS);
|
|
382
|
+
|
|
383
|
+
const manageResult = {
|
|
384
|
+
accounts: [initialAccount, managedAccount],
|
|
385
|
+
selectedAccount: managedAccount,
|
|
386
|
+
};
|
|
387
|
+
sdk.onMessage(responseMessage(frameId, manageRequest.id, manageResult));
|
|
388
|
+
|
|
389
|
+
await expect(managePromise).resolves.toEqual({
|
|
390
|
+
...manageResult,
|
|
391
|
+
accounts: [managedAccount],
|
|
392
|
+
});
|
|
393
|
+
expect(sdk.getAccounts()).toEqual([managedAccount]);
|
|
394
|
+
expect(sdk.getSelectedAccount()).toEqual(managedAccount);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("persists the selected account as an app-local preference", async () => {
|
|
398
|
+
const storage = new MockStorage();
|
|
399
|
+
const storageKey = "test-connection";
|
|
400
|
+
const selectedAccountStorageKey = "test-selected-account";
|
|
401
|
+
sdk.destroy();
|
|
402
|
+
sdk = new NativeSDK({
|
|
403
|
+
walletUrl: "http://localhost:3000/embedded",
|
|
404
|
+
origin: "thru-mobile://token-dummy",
|
|
405
|
+
storage,
|
|
406
|
+
storageKey,
|
|
407
|
+
selectedAccountStorageKey,
|
|
408
|
+
});
|
|
409
|
+
webView = new MockWebView();
|
|
410
|
+
sdk.attachWebView(webView);
|
|
411
|
+
|
|
412
|
+
const frameId = frameIdFor(sdk);
|
|
413
|
+
const connectPromise = sdk.signIn({
|
|
414
|
+
app_id: "token_dummy_app",
|
|
415
|
+
app_display_name: "Token Dummy App",
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
sdk.onMessage(readyMessage(frameId));
|
|
419
|
+
await flush();
|
|
420
|
+
|
|
421
|
+
const connectRequest = parseInjectedRequest(webView.injected[0]);
|
|
422
|
+
const initialAccount = {
|
|
423
|
+
accountType: "thru",
|
|
424
|
+
address: "thru_test_address_1",
|
|
425
|
+
label: "Account 1",
|
|
426
|
+
};
|
|
427
|
+
const selectedAccount = {
|
|
428
|
+
accountType: "thru",
|
|
429
|
+
address: "thru_test_address_2",
|
|
430
|
+
label: "Account 2",
|
|
431
|
+
};
|
|
432
|
+
sdk.onMessage(
|
|
433
|
+
responseMessage(frameId, connectRequest.id, {
|
|
434
|
+
accounts: [initialAccount, selectedAccount],
|
|
435
|
+
status: "completed",
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
await connectPromise;
|
|
439
|
+
|
|
440
|
+
const selectPromise = sdk.selectAccount(selectedAccount.address);
|
|
441
|
+
await flush();
|
|
442
|
+
|
|
443
|
+
const selectRequest = parseInjectedRequest(webView.injected[1]);
|
|
444
|
+
expect(selectRequest.type).toBe(POST_MESSAGE_REQUEST_TYPES.SELECT_ACCOUNT);
|
|
445
|
+
sdk.onMessage(
|
|
446
|
+
responseMessage(frameId, selectRequest.id, {
|
|
447
|
+
account: selectedAccount,
|
|
448
|
+
}),
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
await selectPromise;
|
|
452
|
+
const storedSelected = JSON.parse(
|
|
453
|
+
storage.values.get(selectedAccountStorageKey) ?? "{}",
|
|
454
|
+
);
|
|
455
|
+
expect(storedSelected.selectedAccountAddress).toBe(selectedAccount.address);
|
|
456
|
+
expect(storage.values.has(storageKey)).toBe(false);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("restores the app-local selected account on login", async () => {
|
|
460
|
+
const storage = new MockStorage();
|
|
461
|
+
const storageKey = "test-connection";
|
|
462
|
+
const selectedAccountStorageKey = "test-selected-account";
|
|
463
|
+
const initialAccount = {
|
|
464
|
+
accountType: "thru",
|
|
465
|
+
address: "thru_test_address_1",
|
|
466
|
+
label: "Account 1",
|
|
467
|
+
};
|
|
468
|
+
const selectedAccount = {
|
|
469
|
+
accountType: "thru",
|
|
470
|
+
address: "thru_test_address_2",
|
|
471
|
+
label: "Account 2",
|
|
472
|
+
};
|
|
473
|
+
storage.setItem(
|
|
474
|
+
selectedAccountStorageKey,
|
|
475
|
+
JSON.stringify({
|
|
476
|
+
version: 1,
|
|
477
|
+
origin: "thru-mobile://token-dummy",
|
|
478
|
+
walletOrigin: "http://localhost:3000",
|
|
479
|
+
savedAt: new Date().toISOString(),
|
|
480
|
+
selectedAccountAddress: selectedAccount.address,
|
|
481
|
+
}),
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
sdk.destroy();
|
|
485
|
+
sdk = new NativeSDK({
|
|
486
|
+
walletUrl: "http://localhost:3000/embedded",
|
|
487
|
+
origin: "thru-mobile://token-dummy",
|
|
488
|
+
storage,
|
|
489
|
+
storageKey,
|
|
490
|
+
selectedAccountStorageKey,
|
|
491
|
+
});
|
|
492
|
+
webView = new MockWebView();
|
|
493
|
+
sdk.attachWebView(webView);
|
|
494
|
+
|
|
495
|
+
const frameId = frameIdFor(sdk);
|
|
496
|
+
const connectPromise = sdk.signIn({
|
|
497
|
+
app_id: "token_dummy_app",
|
|
498
|
+
app_display_name: "Token Dummy App",
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
sdk.onMessage(readyMessage(frameId));
|
|
502
|
+
await flush();
|
|
503
|
+
|
|
504
|
+
const connectRequest = parseInjectedRequest(webView.injected[0]);
|
|
505
|
+
expect(connectRequest.payload).toMatchObject({
|
|
506
|
+
metadata: {
|
|
507
|
+
appId: "token_dummy_app",
|
|
508
|
+
appName: "Token Dummy App",
|
|
509
|
+
},
|
|
510
|
+
preferredAccountAddress: selectedAccount.address,
|
|
511
|
+
});
|
|
512
|
+
const result = {
|
|
513
|
+
accounts: [initialAccount, selectedAccount],
|
|
514
|
+
selectedAccount,
|
|
515
|
+
status: "completed",
|
|
516
|
+
};
|
|
517
|
+
sdk.onMessage(responseMessage(frameId, connectRequest.id, result));
|
|
518
|
+
|
|
519
|
+
await expect(connectPromise).resolves.toEqual({
|
|
520
|
+
...result,
|
|
521
|
+
accounts: [selectedAccount],
|
|
522
|
+
});
|
|
523
|
+
expect(sdk.getAccounts()).toEqual([selectedAccount]);
|
|
524
|
+
expect(sdk.getSelectedAccount()).toEqual(selectedAccount);
|
|
525
|
+
expect(sdk.getWalletAvailability()).toMatchObject({
|
|
526
|
+
accounts: [selectedAccount],
|
|
527
|
+
selectedAccount,
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("clears legacy cached metadata without restoring a connection", async () => {
|
|
532
|
+
const storage = new MockStorage();
|
|
533
|
+
const storageKey = "test-connection";
|
|
534
|
+
const result = {
|
|
535
|
+
accounts: [
|
|
536
|
+
{
|
|
537
|
+
accountType: "thru",
|
|
538
|
+
address: "thru_test_address",
|
|
539
|
+
label: "Account 1",
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
metadata: {
|
|
543
|
+
appId: "token_dummy_app",
|
|
544
|
+
appName: "Token Dummy App",
|
|
545
|
+
appUrl: "thru-mobile://token-dummy",
|
|
546
|
+
},
|
|
547
|
+
status: "completed",
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
storage.setItem(
|
|
551
|
+
storageKey,
|
|
552
|
+
JSON.stringify({
|
|
553
|
+
version: 1,
|
|
554
|
+
origin: "thru-mobile://token-dummy",
|
|
555
|
+
walletOrigin: "http://localhost:3000",
|
|
556
|
+
savedAt: new Date().toISOString(),
|
|
557
|
+
result,
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
sdk.destroy();
|
|
562
|
+
sdk = new NativeSDK({
|
|
563
|
+
walletUrl: "http://localhost:3000/embedded",
|
|
564
|
+
origin: "thru-mobile://token-dummy",
|
|
565
|
+
storage,
|
|
566
|
+
storageKey,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
await expect(sdk.restoreConnection({ hydrate: false })).resolves.toBeNull();
|
|
570
|
+
expect(sdk.isConnected()).toBe(false);
|
|
571
|
+
expect(sdk.getAccounts()).toEqual([]);
|
|
572
|
+
expect(storage.values.has(storageKey)).toBe(false);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("syncs restored state against the wallet without opening a connect flow", async () => {
|
|
576
|
+
const frameId = frameIdFor(sdk);
|
|
577
|
+
const promise = sdk.syncConnectionState({
|
|
578
|
+
metadata: {
|
|
579
|
+
appId: "token_dummy_app",
|
|
580
|
+
appName: "Token Dummy App",
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
sdk.onMessage(readyMessage(frameId));
|
|
585
|
+
await flush();
|
|
586
|
+
|
|
587
|
+
const request = parseInjectedRequest(webView.injected[0]);
|
|
588
|
+
expect(request.type).toBe(POST_MESSAGE_REQUEST_TYPES.GET_CONNECTION_STATE);
|
|
589
|
+
expect(request.origin).toBe("thru-mobile://token-dummy");
|
|
590
|
+
|
|
591
|
+
const state = {
|
|
592
|
+
isAuthorized: true,
|
|
593
|
+
isConnected: true,
|
|
594
|
+
isUnlocked: false,
|
|
595
|
+
hasPasskey: true,
|
|
596
|
+
hasWalletAccount: true,
|
|
597
|
+
accounts: [
|
|
598
|
+
{
|
|
599
|
+
accountType: "thru",
|
|
600
|
+
address: "thru_test_address",
|
|
601
|
+
label: "Account 1",
|
|
602
|
+
},
|
|
603
|
+
],
|
|
604
|
+
selectedAccount: {
|
|
605
|
+
accountType: "thru",
|
|
606
|
+
address: "thru_test_address",
|
|
607
|
+
label: "Account 1",
|
|
608
|
+
},
|
|
609
|
+
metadata: {
|
|
610
|
+
appId: "token_dummy_app",
|
|
611
|
+
appName: "Token Dummy App",
|
|
612
|
+
appUrl: "thru-mobile://token-dummy",
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
sdk.onMessage(responseMessage(frameId, request.id, state));
|
|
616
|
+
|
|
617
|
+
await expect(promise).resolves.toEqual(state);
|
|
618
|
+
expect(sdk.isConnected()).toBe(true);
|
|
619
|
+
expect(sdk.getAccounts()).toEqual(state.accounts);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("reports wallet availability without hydrating unauthorized accounts", async () => {
|
|
623
|
+
const availabilityEvents: unknown[] = [];
|
|
624
|
+
sdk.on("availabilityChanged", (availability) => {
|
|
625
|
+
availabilityEvents.push(availability);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const frameId = frameIdFor(sdk);
|
|
629
|
+
const promise = sdk.refreshWalletAvailability({
|
|
630
|
+
metadata: {
|
|
631
|
+
appId: "token_dummy_app",
|
|
632
|
+
appName: "Token Dummy App",
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
sdk.onMessage(readyMessage(frameId));
|
|
637
|
+
await flush();
|
|
638
|
+
|
|
639
|
+
const request = parseInjectedRequest(webView.injected[0]);
|
|
640
|
+
expect(request.type).toBe(POST_MESSAGE_REQUEST_TYPES.GET_CONNECTION_STATE);
|
|
641
|
+
|
|
642
|
+
const state = {
|
|
643
|
+
isAuthorized: false,
|
|
644
|
+
isConnected: false,
|
|
645
|
+
isUnlocked: false,
|
|
646
|
+
hasPasskey: true,
|
|
647
|
+
hasWalletAccount: true,
|
|
648
|
+
accounts: [],
|
|
649
|
+
selectedAccount: null,
|
|
650
|
+
metadata: null,
|
|
651
|
+
};
|
|
652
|
+
sdk.onMessage(responseMessage(frameId, request.id, state));
|
|
653
|
+
|
|
654
|
+
await expect(promise).resolves.toMatchObject({
|
|
655
|
+
status: "ready",
|
|
656
|
+
isAuthorized: false,
|
|
657
|
+
hasPasskey: true,
|
|
658
|
+
hasWalletAccount: true,
|
|
659
|
+
accounts: [],
|
|
660
|
+
selectedAccount: null,
|
|
661
|
+
metadata: null,
|
|
662
|
+
});
|
|
663
|
+
expect(availabilityEvents).toHaveLength(1);
|
|
664
|
+
expect(availabilityEvents[0]).toMatchObject({
|
|
665
|
+
status: "ready",
|
|
666
|
+
hasPasskey: true,
|
|
667
|
+
hasWalletAccount: true,
|
|
668
|
+
});
|
|
669
|
+
expect(sdk.isConnected()).toBe(false);
|
|
670
|
+
expect(sdk.getAccounts()).toEqual([]);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it("clears stale native state when the wallet has no active passkey session", async () => {
|
|
674
|
+
const storage = new MockStorage();
|
|
675
|
+
const storageKey = "test-connection";
|
|
676
|
+
const staleResult = {
|
|
677
|
+
accounts: [
|
|
678
|
+
{
|
|
679
|
+
accountType: "thru",
|
|
680
|
+
address: "thru_test_address",
|
|
681
|
+
label: "Account 1",
|
|
682
|
+
},
|
|
683
|
+
],
|
|
684
|
+
metadata: {
|
|
685
|
+
appId: "token_dummy_app",
|
|
686
|
+
appName: "Token Dummy App",
|
|
687
|
+
appUrl: "thru-mobile://token-dummy",
|
|
688
|
+
},
|
|
689
|
+
status: "completed",
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
storage.setItem(
|
|
693
|
+
storageKey,
|
|
694
|
+
JSON.stringify({
|
|
695
|
+
version: 1,
|
|
696
|
+
origin: "thru-mobile://token-dummy",
|
|
697
|
+
walletOrigin: "http://localhost:3000",
|
|
698
|
+
savedAt: new Date().toISOString(),
|
|
699
|
+
result: staleResult,
|
|
700
|
+
}),
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
sdk.destroy();
|
|
704
|
+
sdk = new NativeSDK({
|
|
705
|
+
walletUrl: "http://localhost:3000/embedded",
|
|
706
|
+
origin: "thru-mobile://token-dummy",
|
|
707
|
+
storage,
|
|
708
|
+
storageKey,
|
|
709
|
+
});
|
|
710
|
+
webView = new MockWebView();
|
|
711
|
+
sdk.attachWebView(webView);
|
|
712
|
+
|
|
713
|
+
const frameId = frameIdFor(sdk);
|
|
714
|
+
const promise = sdk.syncConnectionState({
|
|
715
|
+
metadata: {
|
|
716
|
+
appId: "token_dummy_app",
|
|
717
|
+
appName: "Token Dummy App",
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
sdk.onMessage(readyMessage(frameId));
|
|
722
|
+
await flush();
|
|
723
|
+
|
|
724
|
+
const request = parseInjectedRequest(webView.injected[0]);
|
|
725
|
+
expect(request.type).toBe(POST_MESSAGE_REQUEST_TYPES.GET_CONNECTION_STATE);
|
|
726
|
+
|
|
727
|
+
const state = {
|
|
728
|
+
isAuthorized: true,
|
|
729
|
+
isConnected: true,
|
|
730
|
+
isUnlocked: false,
|
|
731
|
+
hasPasskey: false,
|
|
732
|
+
hasWalletAccount: true,
|
|
733
|
+
accounts: staleResult.accounts,
|
|
734
|
+
selectedAccount: staleResult.accounts[0],
|
|
735
|
+
metadata: staleResult.metadata,
|
|
736
|
+
};
|
|
737
|
+
sdk.onMessage(responseMessage(frameId, request.id, state));
|
|
738
|
+
|
|
739
|
+
await expect(promise).resolves.toEqual({
|
|
740
|
+
...state,
|
|
741
|
+
accounts: [],
|
|
742
|
+
selectedAccount: null,
|
|
743
|
+
});
|
|
744
|
+
expect(sdk.isConnected()).toBe(false);
|
|
745
|
+
expect(sdk.getAccounts()).toEqual([]);
|
|
746
|
+
expect(storage.values.has(storageKey)).toBe(false);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it("keeps legacy native session state cleared when sign-in consent is denied", async () => {
|
|
750
|
+
const storage = new MockStorage();
|
|
751
|
+
const storageKey = "test-connection";
|
|
752
|
+
const restoredResult = {
|
|
753
|
+
accounts: [
|
|
754
|
+
{
|
|
755
|
+
accountType: "thru",
|
|
756
|
+
address: "thru_test_address",
|
|
757
|
+
label: "Account 1",
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
metadata: {
|
|
761
|
+
appId: "token_dummy_app",
|
|
762
|
+
appName: "Token Dummy App",
|
|
763
|
+
appUrl: "thru-mobile://token-dummy",
|
|
764
|
+
},
|
|
765
|
+
status: "completed",
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
storage.setItem(
|
|
769
|
+
storageKey,
|
|
770
|
+
JSON.stringify({
|
|
771
|
+
version: 1,
|
|
772
|
+
origin: "thru-mobile://token-dummy",
|
|
773
|
+
walletOrigin: "http://localhost:3000",
|
|
774
|
+
savedAt: new Date().toISOString(),
|
|
775
|
+
result: restoredResult,
|
|
776
|
+
}),
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
sdk.destroy();
|
|
780
|
+
sdk = new NativeSDK({
|
|
781
|
+
walletUrl: "http://localhost:3000/embedded",
|
|
782
|
+
origin: "thru-mobile://token-dummy",
|
|
783
|
+
storage,
|
|
784
|
+
storageKey,
|
|
785
|
+
});
|
|
786
|
+
webView = new MockWebView();
|
|
787
|
+
sdk.attachWebView(webView);
|
|
788
|
+
|
|
789
|
+
await expect(sdk.restoreConnection({ hydrate: false })).resolves.toBeNull();
|
|
790
|
+
expect(storage.values.has(storageKey)).toBe(false);
|
|
791
|
+
|
|
792
|
+
const frameId = frameIdFor(sdk);
|
|
793
|
+
const promise = sdk.signIn({
|
|
794
|
+
app_id: "token_dummy_app",
|
|
795
|
+
app_display_name: "Token Dummy App",
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
sdk.onMessage(readyMessage(frameId));
|
|
799
|
+
await flush();
|
|
800
|
+
|
|
801
|
+
const request = parseInjectedRequest(webView.injected[0]);
|
|
802
|
+
sdk.onMessage(
|
|
803
|
+
rejectedResponseMessage(
|
|
804
|
+
frameId,
|
|
805
|
+
request.id,
|
|
806
|
+
ErrorCode.USER_REJECTED,
|
|
807
|
+
"User rejected the request",
|
|
808
|
+
),
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
await expect(promise).rejects.toMatchObject({
|
|
812
|
+
code: ErrorCode.USER_REJECTED,
|
|
813
|
+
});
|
|
814
|
+
expect(sdk.isConnected()).toBe(false);
|
|
815
|
+
expect(sdk.getAccounts()).toEqual([]);
|
|
816
|
+
expect(storage.values.has(storageKey)).toBe(false);
|
|
817
|
+
await expect(sdk.restoreConnection({ hydrate: false })).resolves.toBeNull();
|
|
818
|
+
});
|
|
819
|
+
});
|