@thru/wallet 0.2.25 → 0.2.28
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 +1 -0
- package/dist/{BrowserSDK-CpRFiJsW.d.ts → BrowserSDK-CRQTOT8S.d.ts} +178 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +376 -12
- package/dist/index.js.map +1 -1
- package/dist/native/react/transparent.d.ts +104 -0
- package/dist/native/react/transparent.js +2210 -0
- package/dist/native/react/transparent.js.map +1 -0
- package/dist/native/react.d.ts +5 -90
- package/dist/native/react.js +765 -32
- package/dist/native/react.js.map +1 -1
- package/dist/native.d.ts +105 -1
- package/dist/native.js +521 -31
- package/dist/native.js.map +1 -1
- package/dist/react-ui.js +5 -0
- package/dist/react-ui.js.map +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +376 -12
- package/dist/react.js.map +1 -1
- package/package.json +8 -2
- package/src/BrowserSDK.ts +32 -1
- package/src/encoding.ts +39 -0
- package/src/index.ts +5 -1
- package/src/interfaces/IThruChain.ts +50 -1
- package/src/interfaces/types.ts +52 -0
- package/src/native/NativeSDK.test.ts +200 -1
- package/src/native/NativeSDK.ts +124 -10
- package/src/native/index.ts +12 -0
- package/src/native/provider/NativeProvider.ts +106 -5
- package/src/native/provider/WebViewBridge.test.ts +22 -1
- package/src/native/provider/WebViewBridge.ts +17 -7
- package/src/native/provider/chains/ThruChain.ts +215 -5
- package/src/native/react/ThruContext.ts +3 -1
- package/src/native/react/ThruProvider.tsx +25 -0
- package/src/native/react/ThruTransparentWalletBridge.tsx +281 -0
- package/src/native/react/hooks/useWallet.ts +12 -1
- package/src/native/react/index.ts +11 -0
- package/src/native/react/transparent.ts +35 -0
- package/src/protocol/postMessage.ts +127 -2
- package/src/provider/EmbeddedProvider.ts +7 -1
- package/src/provider/IframeManager.test.ts +18 -0
- package/src/provider/IframeManager.ts +8 -1
- package/src/provider/chains/ThruChain.ts +210 -4
- package/src/provider/types/messages.ts +16 -0
- package/src/react/index.ts +6 -0
- package/src/signing-sessions.test.ts +182 -0
- package/src/signing-sessions.ts +204 -0
|
@@ -0,0 +1,2210 @@
|
|
|
1
|
+
import { createContext, useState, useEffect, useCallback, useContext, useRef, useMemo } from 'react';
|
|
2
|
+
import { createThruClient } from '@thru/sdk/client';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
import { StyleSheet, Platform, View } from 'react-native';
|
|
5
|
+
import { WebView } from 'react-native-webview';
|
|
6
|
+
import { requireOptionalNativeModule } from 'expo-modules-core';
|
|
7
|
+
|
|
8
|
+
// src/native/react/ThruProvider.tsx
|
|
9
|
+
|
|
10
|
+
// src/interfaces/accounts.ts
|
|
11
|
+
function resolveSelectedWalletAccount(accounts, selectedAccount) {
|
|
12
|
+
if (selectedAccount) {
|
|
13
|
+
return accounts.find((account) => account.address === selectedAccount.address) ?? selectedAccount;
|
|
14
|
+
}
|
|
15
|
+
return accounts[0] ?? null;
|
|
16
|
+
}
|
|
17
|
+
function resolveWalletAccountByAddress(accounts, address) {
|
|
18
|
+
if (!address) return null;
|
|
19
|
+
return accounts.find((account) => account.address === address) ?? null;
|
|
20
|
+
}
|
|
21
|
+
function normalizeActiveWalletAccounts(accounts, selectedAccount) {
|
|
22
|
+
const activeAccount = resolveSelectedWalletAccount(accounts, selectedAccount);
|
|
23
|
+
return {
|
|
24
|
+
accounts: activeAccount ? [activeAccount] : [],
|
|
25
|
+
selectedAccount: activeAccount
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function normalizeWalletAccountResult(result, selectedAccount) {
|
|
29
|
+
const active = normalizeActiveWalletAccounts(
|
|
30
|
+
result.accounts,
|
|
31
|
+
selectedAccount ?? result.selectedAccount ?? null
|
|
32
|
+
);
|
|
33
|
+
return {
|
|
34
|
+
...result,
|
|
35
|
+
accounts: active.accounts,
|
|
36
|
+
selectedAccount: active.selectedAccount
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/interfaces/types.ts
|
|
41
|
+
var AddressType = {
|
|
42
|
+
THRU: "thru"
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/protocol/postMessage.ts
|
|
46
|
+
var POST_MESSAGE_REQUEST_TYPES = {
|
|
47
|
+
CONNECT: "connect",
|
|
48
|
+
CREATE_ACCOUNT: "createAccount",
|
|
49
|
+
DISCONNECT: "disconnect",
|
|
50
|
+
SIGN_MESSAGE: "signMessage",
|
|
51
|
+
SIGN_TRANSACTION: "signTransaction",
|
|
52
|
+
SIGN_PASSKEY_CHALLENGE: "signPasskeyChallenge",
|
|
53
|
+
GET_ACCOUNTS: "getAccounts",
|
|
54
|
+
GET_CONNECTION_STATE: "getConnectionState",
|
|
55
|
+
GET_SIGNING_CONTEXT: "getSigningContext",
|
|
56
|
+
SELECT_ACCOUNT: "selectAccount",
|
|
57
|
+
MANAGE_ACCOUNTS: "manageAccounts",
|
|
58
|
+
CREATE_SIGNING_SESSION: "createSigningSession",
|
|
59
|
+
CREATE_SIGNING_SESSION_INSTRUCTION: "createSigningSessionInstruction",
|
|
60
|
+
CONFIRM_SIGNING_SESSION: "confirmSigningSession",
|
|
61
|
+
REVOKE_SIGNING_SESSION: "revokeSigningSession"
|
|
62
|
+
};
|
|
63
|
+
var EMBEDDED_PROVIDER_EVENTS = {
|
|
64
|
+
CONNECT_START: "connect_start",
|
|
65
|
+
CONNECT: "connect",
|
|
66
|
+
DISCONNECT: "disconnect",
|
|
67
|
+
CONNECT_ERROR: "connect_error",
|
|
68
|
+
ERROR: "error",
|
|
69
|
+
LOCK: "lock",
|
|
70
|
+
UI_SHOW: "ui_show",
|
|
71
|
+
ACCOUNT_CHANGED: "account_changed"
|
|
72
|
+
};
|
|
73
|
+
var POST_MESSAGE_EVENT_TYPE = "event";
|
|
74
|
+
var IFRAME_READY_EVENT = "iframe:ready";
|
|
75
|
+
var REQUEST_ID_PREFIX = "req";
|
|
76
|
+
var createRequestId = (prefix = REQUEST_ID_PREFIX) => {
|
|
77
|
+
const random = Math.random().toString(36).slice(2, 11);
|
|
78
|
+
return `${prefix}_${Date.now()}_${random}`;
|
|
79
|
+
};
|
|
80
|
+
var ErrorCode = {
|
|
81
|
+
USER_REJECTED: "USER_REJECTED"};
|
|
82
|
+
|
|
83
|
+
// src/protocol/walletState.ts
|
|
84
|
+
function normalizeConnectionStateResult(result) {
|
|
85
|
+
if (!result.isAuthorized || !result.hasPasskey) {
|
|
86
|
+
return { ...result, accounts: [], selectedAccount: null };
|
|
87
|
+
}
|
|
88
|
+
return normalizeWalletAccountResult(result);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/encoding.ts
|
|
92
|
+
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
93
|
+
var BASE64_LOOKUP = new Map(
|
|
94
|
+
[...BASE64_ALPHABET].map((char, index) => [char, index])
|
|
95
|
+
);
|
|
96
|
+
function base64ToBytes(value) {
|
|
97
|
+
const normalized = value.replace(/\s+/g, "");
|
|
98
|
+
if (normalized.length === 0) return new Uint8Array();
|
|
99
|
+
if (normalized.length % 4 === 1) {
|
|
100
|
+
throw new Error("Invalid base64 data");
|
|
101
|
+
}
|
|
102
|
+
const padded = normalized.padEnd(
|
|
103
|
+
normalized.length + (4 - normalized.length % 4) % 4,
|
|
104
|
+
"="
|
|
105
|
+
);
|
|
106
|
+
const padding = padded.endsWith("==") ? 2 : padded.endsWith("=") ? 1 : 0;
|
|
107
|
+
const output = new Uint8Array(padded.length / 4 * 3 - padding);
|
|
108
|
+
let outIdx = 0;
|
|
109
|
+
for (let i = 0; i < padded.length; i += 4) {
|
|
110
|
+
const chars = padded.slice(i, i + 4);
|
|
111
|
+
const a = BASE64_LOOKUP.get(chars[0]);
|
|
112
|
+
const b = BASE64_LOOKUP.get(chars[1]);
|
|
113
|
+
const c = chars[2] === "=" ? 0 : BASE64_LOOKUP.get(chars[2]);
|
|
114
|
+
const d = chars[3] === "=" ? 0 : BASE64_LOOKUP.get(chars[3]);
|
|
115
|
+
if (a === void 0 || b === void 0 || c === void 0 || d === void 0) {
|
|
116
|
+
throw new Error("Invalid base64 data");
|
|
117
|
+
}
|
|
118
|
+
const chunk = a << 18 | b << 12 | c << 6 | d;
|
|
119
|
+
if (outIdx < output.length) output[outIdx++] = chunk >> 16 & 255;
|
|
120
|
+
if (outIdx < output.length) output[outIdx++] = chunk >> 8 & 255;
|
|
121
|
+
if (outIdx < output.length) output[outIdx++] = chunk & 255;
|
|
122
|
+
}
|
|
123
|
+
return output;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/signing-sessions.ts
|
|
127
|
+
var STORAGE_VERSION = 1;
|
|
128
|
+
var KEY_PREFIX = "thru.wallet.signing-sessions.v1";
|
|
129
|
+
function encodeKeyPart(input) {
|
|
130
|
+
return encodeURIComponent(input).replace(
|
|
131
|
+
/[!'()*]/g,
|
|
132
|
+
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
function nowSeconds() {
|
|
136
|
+
return Math.floor(Date.now() / 1e3);
|
|
137
|
+
}
|
|
138
|
+
function resolveSigningSessionStorageKey(params) {
|
|
139
|
+
if (params.storageKey) return params.storageKey;
|
|
140
|
+
return `${KEY_PREFIX}:${encodeKeyPart(params.walletOrigin)}:${encodeKeyPart(params.appOrigin)}`;
|
|
141
|
+
}
|
|
142
|
+
function normalizeExpiresAt(value, label = "expiresAt") {
|
|
143
|
+
if (value instanceof Date) {
|
|
144
|
+
const millis = value.getTime();
|
|
145
|
+
if (!Number.isFinite(millis)) throw new Error(`${label} must be a valid Date`);
|
|
146
|
+
return Math.floor(millis / 1e3);
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === "bigint") {
|
|
149
|
+
if (value < 0n || value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
150
|
+
throw new Error(`${label} must fit in a JavaScript safe integer`);
|
|
151
|
+
}
|
|
152
|
+
return Number(value);
|
|
153
|
+
}
|
|
154
|
+
if (typeof value === "string") {
|
|
155
|
+
const trimmed = value.trim();
|
|
156
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
157
|
+
throw new Error(`${label} must be a Unix timestamp in seconds`);
|
|
158
|
+
}
|
|
159
|
+
return normalizeExpiresAt(BigInt(trimmed), label);
|
|
160
|
+
}
|
|
161
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
162
|
+
throw new Error(`${label} must be a finite positive Unix timestamp`);
|
|
163
|
+
}
|
|
164
|
+
return Math.floor(value);
|
|
165
|
+
}
|
|
166
|
+
function resolveSessionExpirySeconds(options) {
|
|
167
|
+
const hasDuration = options.durationSeconds !== void 0;
|
|
168
|
+
const hasExpiresAt = options.expiresAt !== void 0;
|
|
169
|
+
if (hasDuration === hasExpiresAt) {
|
|
170
|
+
throw new Error("Provide exactly one of durationSeconds or expiresAt");
|
|
171
|
+
}
|
|
172
|
+
if (hasDuration) {
|
|
173
|
+
const duration = options.durationSeconds;
|
|
174
|
+
if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0) {
|
|
175
|
+
throw new Error("durationSeconds must be a positive number");
|
|
176
|
+
}
|
|
177
|
+
return nowSeconds() + Math.floor(duration);
|
|
178
|
+
}
|
|
179
|
+
return normalizeExpiresAt(options.expiresAt, "expiresAt");
|
|
180
|
+
}
|
|
181
|
+
function assertSigningSessionWalletAccountIdx(walletAccountIdx) {
|
|
182
|
+
if (!Number.isInteger(walletAccountIdx) || walletAccountIdx < 2 || walletAccountIdx > 65535) {
|
|
183
|
+
throw new Error("walletAccountIdx must be an account index between 2 and 65535");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function normalizeDescriptor(descriptor) {
|
|
187
|
+
return {
|
|
188
|
+
id: descriptor.id,
|
|
189
|
+
walletAddress: descriptor.walletAddress,
|
|
190
|
+
publicKey: descriptor.publicKey,
|
|
191
|
+
authIdx: Number(descriptor.authIdx),
|
|
192
|
+
expiresAt: normalizeExpiresAt(descriptor.expiresAt, "descriptor.expiresAt"),
|
|
193
|
+
createdAt: normalizeExpiresAt(descriptor.createdAt, "descriptor.createdAt")
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function isActive(descriptor) {
|
|
197
|
+
return nowSeconds() < descriptor.expiresAt;
|
|
198
|
+
}
|
|
199
|
+
var SigningSessionDescriptorStore = class {
|
|
200
|
+
constructor(storage, key) {
|
|
201
|
+
this.storage = storage;
|
|
202
|
+
this.key = key;
|
|
203
|
+
}
|
|
204
|
+
async list() {
|
|
205
|
+
const sessions = await this.read();
|
|
206
|
+
const active = sessions.filter(isActive);
|
|
207
|
+
if (active.length !== sessions.length) {
|
|
208
|
+
await this.write(active);
|
|
209
|
+
}
|
|
210
|
+
return active;
|
|
211
|
+
}
|
|
212
|
+
async get(id) {
|
|
213
|
+
const sessions = await this.list();
|
|
214
|
+
return sessions.find((session) => session.id === id) ?? null;
|
|
215
|
+
}
|
|
216
|
+
async save(descriptor) {
|
|
217
|
+
const normalized = normalizeDescriptor(descriptor);
|
|
218
|
+
const sessions = (await this.list()).filter((session) => session.id !== normalized.id);
|
|
219
|
+
sessions.push(normalized);
|
|
220
|
+
await this.write(sessions);
|
|
221
|
+
}
|
|
222
|
+
async saveReplacingWalletSessions(descriptor) {
|
|
223
|
+
const normalized = normalizeDescriptor(descriptor);
|
|
224
|
+
const sessions = (await this.list()).filter(
|
|
225
|
+
(session) => session.id === normalized.id || session.walletAddress !== normalized.walletAddress
|
|
226
|
+
);
|
|
227
|
+
const withoutCurrent = sessions.filter((session) => session.id !== normalized.id);
|
|
228
|
+
withoutCurrent.push(normalized);
|
|
229
|
+
await this.write(withoutCurrent);
|
|
230
|
+
}
|
|
231
|
+
async remove(id) {
|
|
232
|
+
const sessions = (await this.list()).filter((session) => session.id !== id);
|
|
233
|
+
if (sessions.length === 0) {
|
|
234
|
+
await this.storage.removeItem(this.key);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
await this.write(sessions);
|
|
238
|
+
}
|
|
239
|
+
async read() {
|
|
240
|
+
const raw = await this.storage.getItem(this.key);
|
|
241
|
+
if (!raw) return [];
|
|
242
|
+
try {
|
|
243
|
+
const parsed = JSON.parse(raw);
|
|
244
|
+
if (parsed.version !== STORAGE_VERSION || !Array.isArray(parsed.sessions)) {
|
|
245
|
+
await this.storage.removeItem(this.key);
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
return parsed.sessions.map(normalizeDescriptor);
|
|
249
|
+
} catch {
|
|
250
|
+
await this.storage.removeItem(this.key);
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async write(sessions) {
|
|
255
|
+
const payload = {
|
|
256
|
+
version: STORAGE_VERSION,
|
|
257
|
+
sessions: sessions.map(normalizeDescriptor)
|
|
258
|
+
};
|
|
259
|
+
await this.storage.setItem(this.key, JSON.stringify(payload));
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/native/provider/chains/ThruChain.ts
|
|
264
|
+
function descriptorFromWire(session) {
|
|
265
|
+
return {
|
|
266
|
+
id: session.id,
|
|
267
|
+
walletAddress: session.walletAddress,
|
|
268
|
+
publicKey: session.publicKey,
|
|
269
|
+
authIdx: session.authIdx,
|
|
270
|
+
expiresAt: Number(BigInt(session.expiresAt)),
|
|
271
|
+
createdAt: Number(BigInt(session.createdAt))
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
var NativeThruChain = class {
|
|
275
|
+
constructor(bridge, provider, origin, signingSessions) {
|
|
276
|
+
this.bridge = bridge;
|
|
277
|
+
this.provider = provider;
|
|
278
|
+
this.origin = origin;
|
|
279
|
+
this.signingSessions = signingSessions;
|
|
280
|
+
}
|
|
281
|
+
get connected() {
|
|
282
|
+
return this.provider.isConnected();
|
|
283
|
+
}
|
|
284
|
+
async connect() {
|
|
285
|
+
const result = await this.provider.connect();
|
|
286
|
+
const selectedAccount = result.selectedAccount;
|
|
287
|
+
const thruAccount = selectedAccount?.accountType === AddressType.THRU ? selectedAccount : result.accounts.find((addr) => addr.accountType === AddressType.THRU);
|
|
288
|
+
if (!thruAccount) {
|
|
289
|
+
throw new Error("Thru address not found in connection result");
|
|
290
|
+
}
|
|
291
|
+
return { publicKey: thruAccount.address };
|
|
292
|
+
}
|
|
293
|
+
async disconnect() {
|
|
294
|
+
await this.provider.disconnect();
|
|
295
|
+
}
|
|
296
|
+
async getSigningContext() {
|
|
297
|
+
if (!this.provider.isConnected() && !this.provider.isTransparent()) {
|
|
298
|
+
throw new Error("Wallet not connected");
|
|
299
|
+
}
|
|
300
|
+
const response = await this.bridge.sendMessage({
|
|
301
|
+
id: createRequestId(),
|
|
302
|
+
type: POST_MESSAGE_REQUEST_TYPES.GET_SIGNING_CONTEXT,
|
|
303
|
+
origin: this.origin
|
|
304
|
+
});
|
|
305
|
+
return response.result.signingContext;
|
|
306
|
+
}
|
|
307
|
+
async signTransaction(transaction) {
|
|
308
|
+
const signingSessionId = transaction.signingSessionId;
|
|
309
|
+
if (!signingSessionId && !this.provider.isConnected() && !this.provider.isTransparent()) {
|
|
310
|
+
throw new Error("Wallet not connected");
|
|
311
|
+
}
|
|
312
|
+
const session = signingSessionId ? await this.requireSigningSession(signingSessionId) : null;
|
|
313
|
+
const shouldShowWallet = !signingSessionId;
|
|
314
|
+
if (shouldShowWallet) {
|
|
315
|
+
await this.provider.requestShow();
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const response = await this.bridge.sendMessage({
|
|
319
|
+
id: createRequestId(),
|
|
320
|
+
type: POST_MESSAGE_REQUEST_TYPES.SIGN_TRANSACTION,
|
|
321
|
+
payload: {
|
|
322
|
+
walletAddress: transaction.walletAddress ?? session?.walletAddress,
|
|
323
|
+
programAddress: transaction.programAddress,
|
|
324
|
+
instructionData: transaction.instructionData,
|
|
325
|
+
readWriteAddresses: transaction.readWriteAddresses,
|
|
326
|
+
readOnlyAddresses: transaction.readOnlyAddresses,
|
|
327
|
+
review: transaction.review,
|
|
328
|
+
signingSessionId
|
|
329
|
+
},
|
|
330
|
+
origin: this.origin
|
|
331
|
+
});
|
|
332
|
+
return response.result.signedTransaction;
|
|
333
|
+
} finally {
|
|
334
|
+
if (shouldShowWallet) {
|
|
335
|
+
this.provider.requestHide();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async signPasskeyChallenge(challenge) {
|
|
340
|
+
if (!this.provider.isConnected() && !this.provider.isTransparent()) {
|
|
341
|
+
throw new Error("Wallet not connected");
|
|
342
|
+
}
|
|
343
|
+
await this.provider.requestShow();
|
|
344
|
+
try {
|
|
345
|
+
const response = await this.bridge.sendMessage({
|
|
346
|
+
id: createRequestId(),
|
|
347
|
+
type: POST_MESSAGE_REQUEST_TYPES.SIGN_PASSKEY_CHALLENGE,
|
|
348
|
+
payload: {
|
|
349
|
+
challenge: challenge.challenge,
|
|
350
|
+
walletAddress: challenge.walletAddress
|
|
351
|
+
},
|
|
352
|
+
origin: this.origin
|
|
353
|
+
});
|
|
354
|
+
return response.result;
|
|
355
|
+
} finally {
|
|
356
|
+
this.provider.requestHide();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async createSigningSession(options) {
|
|
360
|
+
if (!this.provider.isConnected()) {
|
|
361
|
+
throw new Error("Wallet not connected");
|
|
362
|
+
}
|
|
363
|
+
if (!this.signingSessions) {
|
|
364
|
+
throw new Error("NativeSDKStorage is required for signing sessions");
|
|
365
|
+
}
|
|
366
|
+
const expiresAt = resolveSessionExpirySeconds(options);
|
|
367
|
+
await this.provider.requestShow();
|
|
368
|
+
try {
|
|
369
|
+
const response = await this.bridge.sendMessage({
|
|
370
|
+
id: createRequestId(),
|
|
371
|
+
type: POST_MESSAGE_REQUEST_TYPES.CREATE_SIGNING_SESSION,
|
|
372
|
+
payload: {
|
|
373
|
+
walletAddress: options.walletAddress,
|
|
374
|
+
expiresAt: String(expiresAt),
|
|
375
|
+
review: options.review
|
|
376
|
+
},
|
|
377
|
+
origin: this.origin
|
|
378
|
+
});
|
|
379
|
+
const descriptor = descriptorFromWire(response.result.session);
|
|
380
|
+
await this.signingSessions.saveReplacingWalletSessions(descriptor);
|
|
381
|
+
return this.toSigningSession(descriptor);
|
|
382
|
+
} finally {
|
|
383
|
+
this.provider.requestHide();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async createSigningSessionInstruction(options) {
|
|
387
|
+
if (!this.provider.isConnected()) {
|
|
388
|
+
throw new Error("Wallet not connected");
|
|
389
|
+
}
|
|
390
|
+
if (!this.signingSessions) {
|
|
391
|
+
throw new Error("NativeSDKStorage is required for signing sessions");
|
|
392
|
+
}
|
|
393
|
+
const expiresAt = resolveSessionExpirySeconds(options);
|
|
394
|
+
assertSigningSessionWalletAccountIdx(options.walletAccountIdx);
|
|
395
|
+
const response = await this.bridge.sendMessage({
|
|
396
|
+
id: createRequestId(),
|
|
397
|
+
type: POST_MESSAGE_REQUEST_TYPES.CREATE_SIGNING_SESSION_INSTRUCTION,
|
|
398
|
+
payload: {
|
|
399
|
+
walletAddress: options.walletAddress,
|
|
400
|
+
expiresAt: String(expiresAt),
|
|
401
|
+
walletAccountIdx: options.walletAccountIdx
|
|
402
|
+
},
|
|
403
|
+
origin: this.origin
|
|
404
|
+
});
|
|
405
|
+
const descriptor = descriptorFromWire(response.result.session);
|
|
406
|
+
return {
|
|
407
|
+
session: this.toSigningSession(descriptor),
|
|
408
|
+
programAddress: response.result.programAddress,
|
|
409
|
+
instructionData: base64ToBytes(response.result.instructionData)
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
async confirmSigningSession(id) {
|
|
413
|
+
if (!this.provider.isConnected()) {
|
|
414
|
+
throw new Error("Wallet not connected");
|
|
415
|
+
}
|
|
416
|
+
if (!this.signingSessions) {
|
|
417
|
+
throw new Error("NativeSDKStorage is required for signing sessions");
|
|
418
|
+
}
|
|
419
|
+
const response = await this.bridge.sendMessage({
|
|
420
|
+
id: createRequestId(),
|
|
421
|
+
type: POST_MESSAGE_REQUEST_TYPES.CONFIRM_SIGNING_SESSION,
|
|
422
|
+
payload: { sessionId: id },
|
|
423
|
+
origin: this.origin
|
|
424
|
+
});
|
|
425
|
+
const descriptor = descriptorFromWire(response.result.session);
|
|
426
|
+
await this.signingSessions.saveReplacingWalletSessions(descriptor);
|
|
427
|
+
return this.toSigningSession(descriptor);
|
|
428
|
+
}
|
|
429
|
+
async getSigningSession(id) {
|
|
430
|
+
if (!this.signingSessions) return null;
|
|
431
|
+
const descriptor = await this.signingSessions.get(id);
|
|
432
|
+
return descriptor ? this.toSigningSession(descriptor) : null;
|
|
433
|
+
}
|
|
434
|
+
async getSigningSessions() {
|
|
435
|
+
if (!this.signingSessions) return [];
|
|
436
|
+
return (await this.signingSessions.list()).map(
|
|
437
|
+
(descriptor) => this.toSigningSession(descriptor)
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
async revokeSigningSession(id) {
|
|
441
|
+
try {
|
|
442
|
+
await this.bridge.sendMessage({
|
|
443
|
+
id: createRequestId(),
|
|
444
|
+
type: POST_MESSAGE_REQUEST_TYPES.REVOKE_SIGNING_SESSION,
|
|
445
|
+
payload: { sessionId: id },
|
|
446
|
+
origin: this.origin
|
|
447
|
+
});
|
|
448
|
+
} finally {
|
|
449
|
+
await this.signingSessions?.remove(id);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async requireSigningSession(id) {
|
|
453
|
+
if (!this.signingSessions) {
|
|
454
|
+
throw new Error("NativeSDKStorage is required for signing sessions");
|
|
455
|
+
}
|
|
456
|
+
const session = await this.signingSessions.get(id);
|
|
457
|
+
if (!session) {
|
|
458
|
+
throw new Error("Signing session is not known to this app");
|
|
459
|
+
}
|
|
460
|
+
return session;
|
|
461
|
+
}
|
|
462
|
+
toSigningSession(descriptor) {
|
|
463
|
+
return {
|
|
464
|
+
...descriptor,
|
|
465
|
+
signTransaction: (transaction) => this.signTransaction({
|
|
466
|
+
...transaction,
|
|
467
|
+
walletAddress: transaction.walletAddress ?? descriptor.walletAddress,
|
|
468
|
+
signingSessionId: descriptor.id
|
|
469
|
+
}),
|
|
470
|
+
revoke: () => this.revokeSigningSession(descriptor.id),
|
|
471
|
+
toJSON: () => ({ ...descriptor })
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// src/native/provider/WebViewBridge.ts
|
|
477
|
+
var PRODUCTION_WALLET_ORIGINS = [
|
|
478
|
+
"https://wallet.thru.org",
|
|
479
|
+
"https://wallet.tid.sh"
|
|
480
|
+
];
|
|
481
|
+
function isDevelopmentBuild() {
|
|
482
|
+
const runtime = globalThis;
|
|
483
|
+
const devFlag = runtime.__DEV__;
|
|
484
|
+
if (typeof devFlag === "boolean") return devFlag;
|
|
485
|
+
return runtime.process?.env?.NODE_ENV !== void 0 && runtime.process.env.NODE_ENV !== "production";
|
|
486
|
+
}
|
|
487
|
+
function isPrivateIpv4Host(hostname) {
|
|
488
|
+
const parts = hostname.split(".").map((part) => Number(part));
|
|
489
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
const [a, b] = parts;
|
|
493
|
+
return a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127;
|
|
494
|
+
}
|
|
495
|
+
function isAllowedDevelopmentOrigin(url) {
|
|
496
|
+
if (!isDevelopmentBuild()) return false;
|
|
497
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
498
|
+
const hostname = url.hostname.toLowerCase();
|
|
499
|
+
return hostname === "localhost" || hostname === "::1" || !hostname.includes(".") || hostname.endsWith(".local") || hostname.endsWith(".ts.net") || isPrivateIpv4Host(hostname);
|
|
500
|
+
}
|
|
501
|
+
function validateWalletOrigin(walletUrl) {
|
|
502
|
+
let url;
|
|
503
|
+
try {
|
|
504
|
+
url = new URL(walletUrl);
|
|
505
|
+
} catch {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Invalid wallet URL: ${walletUrl}. URL must be a valid absolute URL.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const origin = url.origin;
|
|
511
|
+
const isAllowed = PRODUCTION_WALLET_ORIGINS.includes(origin) || isAllowedDevelopmentOrigin(url);
|
|
512
|
+
if (!isAllowed) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`Untrusted wallet origin: ${origin}. Only trusted origins are allowed: ${PRODUCTION_WALLET_ORIGINS.join(", ")}. Development builds also allow localhost, LAN, and Tailscale wallet origins.`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function isNativeEmbeddedWalletPath(pathname) {
|
|
519
|
+
const normalized = pathname.replace(/\/+$/, "") || "/";
|
|
520
|
+
return normalized === "/embedded/native" || normalized.startsWith("/embedded/native/");
|
|
521
|
+
}
|
|
522
|
+
var READY_TIMEOUT_MS = 1e4;
|
|
523
|
+
var SLOW_REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
524
|
+
var FAST_REQUEST_TIMEOUT_MS = 30 * 1e3;
|
|
525
|
+
var SLOW_REQUEST_TYPES = /* @__PURE__ */ new Set([
|
|
526
|
+
POST_MESSAGE_REQUEST_TYPES.CONNECT,
|
|
527
|
+
POST_MESSAGE_REQUEST_TYPES.CREATE_ACCOUNT,
|
|
528
|
+
POST_MESSAGE_REQUEST_TYPES.SIGN_MESSAGE,
|
|
529
|
+
POST_MESSAGE_REQUEST_TYPES.SIGN_TRANSACTION,
|
|
530
|
+
POST_MESSAGE_REQUEST_TYPES.SIGN_PASSKEY_CHALLENGE,
|
|
531
|
+
POST_MESSAGE_REQUEST_TYPES.MANAGE_ACCOUNTS,
|
|
532
|
+
POST_MESSAGE_REQUEST_TYPES.CREATE_SIGNING_SESSION,
|
|
533
|
+
POST_MESSAGE_REQUEST_TYPES.CREATE_SIGNING_SESSION_INSTRUCTION,
|
|
534
|
+
POST_MESSAGE_REQUEST_TYPES.CONFIRM_SIGNING_SESSION
|
|
535
|
+
]);
|
|
536
|
+
var WebViewBridge = class {
|
|
537
|
+
constructor(options) {
|
|
538
|
+
this.webView = null;
|
|
539
|
+
this.ready = false;
|
|
540
|
+
this.readyPromise = null;
|
|
541
|
+
this.resolveReady = null;
|
|
542
|
+
this.rejectReady = null;
|
|
543
|
+
this.readyTimer = null;
|
|
544
|
+
this.messageHandlers = /* @__PURE__ */ new Map();
|
|
545
|
+
validateWalletOrigin(options.walletUrl);
|
|
546
|
+
this.walletUrl = options.walletUrl;
|
|
547
|
+
this.walletOrigin = new URL(options.walletUrl).origin;
|
|
548
|
+
this.frameId = createRequestId("frame");
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Compose the URL to load inside the shell <iframe>. The host
|
|
552
|
+
* (ThruWalletSheet) calls this when building the shell HTML.
|
|
553
|
+
*/
|
|
554
|
+
getIframeSrc() {
|
|
555
|
+
const url = new URL(this.walletUrl);
|
|
556
|
+
if (!isNativeEmbeddedWalletPath(url.pathname)) {
|
|
557
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/native`;
|
|
558
|
+
}
|
|
559
|
+
url.searchParams.set("tn_frame_id", this.frameId);
|
|
560
|
+
return url.toString();
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Hand the bridge a WebView ref. Required before `awaitReady()` /
|
|
564
|
+
* `sendMessage()` will resolve.
|
|
565
|
+
*/
|
|
566
|
+
attachWebView(ref) {
|
|
567
|
+
this.webView = ref;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Mark the bridge ready when the native host loads the wallet as the
|
|
571
|
+
* top-level WebView document instead of through the shell iframe.
|
|
572
|
+
*/
|
|
573
|
+
markReady() {
|
|
574
|
+
if (this.ready) return;
|
|
575
|
+
this.ready = true;
|
|
576
|
+
if (this.readyTimer) clearTimeout(this.readyTimer);
|
|
577
|
+
this.readyTimer = null;
|
|
578
|
+
const r = this.resolveReady;
|
|
579
|
+
this.resolveReady = null;
|
|
580
|
+
this.rejectReady = null;
|
|
581
|
+
r?.();
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Returns a promise that resolves when the iframe sends
|
|
585
|
+
* IFRAME_READY_EVENT. Idempotent: returns the same promise on
|
|
586
|
+
* subsequent calls. Rejects after READY_TIMEOUT_MS.
|
|
587
|
+
*/
|
|
588
|
+
awaitReady() {
|
|
589
|
+
if (this.ready) return Promise.resolve();
|
|
590
|
+
if (this.readyPromise) return this.readyPromise;
|
|
591
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
592
|
+
this.resolveReady = resolve;
|
|
593
|
+
this.rejectReady = reject;
|
|
594
|
+
this.readyTimer = setTimeout(() => {
|
|
595
|
+
this.readyTimer = null;
|
|
596
|
+
if (this.rejectReady) {
|
|
597
|
+
const r = this.rejectReady;
|
|
598
|
+
this.rejectReady = null;
|
|
599
|
+
this.resolveReady = null;
|
|
600
|
+
r(new Error("WebView ready timeout - wallet failed to load"));
|
|
601
|
+
}
|
|
602
|
+
}, READY_TIMEOUT_MS);
|
|
603
|
+
});
|
|
604
|
+
return this.readyPromise;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Send a request to the iframe (via injectJavaScript -> shell ->
|
|
608
|
+
* iframe.postMessage) and resolve with the matching response.
|
|
609
|
+
*/
|
|
610
|
+
async sendMessage(request) {
|
|
611
|
+
await this.awaitReady();
|
|
612
|
+
if (!this.webView) {
|
|
613
|
+
throw new Error("WebView not attached - call attachWebView() first");
|
|
614
|
+
}
|
|
615
|
+
const timeoutMs = SLOW_REQUEST_TYPES.has(request.type) ? SLOW_REQUEST_TIMEOUT_MS : FAST_REQUEST_TIMEOUT_MS;
|
|
616
|
+
return new Promise((resolve, reject) => {
|
|
617
|
+
const timer = setTimeout(() => {
|
|
618
|
+
this.messageHandlers.delete(request.id);
|
|
619
|
+
reject(new Error("Request timeout - wallet did not respond"));
|
|
620
|
+
}, timeoutMs);
|
|
621
|
+
this.messageHandlers.set(request.id, (response) => {
|
|
622
|
+
clearTimeout(timer);
|
|
623
|
+
this.messageHandlers.delete(request.id);
|
|
624
|
+
if (response.success) {
|
|
625
|
+
resolve(
|
|
626
|
+
response
|
|
627
|
+
);
|
|
628
|
+
} else {
|
|
629
|
+
const err = new Error(response.error?.message || "Unknown error");
|
|
630
|
+
err.code = response.error?.code;
|
|
631
|
+
err.data = response.error?.data;
|
|
632
|
+
reject(err);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
const script = `try {
|
|
636
|
+
var msg = ${JSON.stringify({ ...request, frameId: this.frameId })};
|
|
637
|
+
if (window.__pushIn) {
|
|
638
|
+
window.__pushIn(msg);
|
|
639
|
+
} else {
|
|
640
|
+
window.postMessage(msg, window.location.origin);
|
|
641
|
+
}
|
|
642
|
+
} catch (e) {} ; true;`;
|
|
643
|
+
this.webView.injectJavaScript(script);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Reject all in-flight wallet requests when the native host dismisses the
|
|
648
|
+
* WebView without waiting for a wallet-side response.
|
|
649
|
+
*/
|
|
650
|
+
rejectPendingRequests(message = "User rejected the request") {
|
|
651
|
+
for (const [id, handler] of Array.from(this.messageHandlers.entries())) {
|
|
652
|
+
handler({
|
|
653
|
+
id,
|
|
654
|
+
success: false,
|
|
655
|
+
error: {
|
|
656
|
+
code: ErrorCode.USER_REJECTED,
|
|
657
|
+
message
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Hook this into <WebView onMessage>. The shell forwards iframe
|
|
664
|
+
* postMessage payloads to ReactNativeWebView; we route them here.
|
|
665
|
+
*/
|
|
666
|
+
onMessage(event) {
|
|
667
|
+
let data;
|
|
668
|
+
try {
|
|
669
|
+
data = JSON.parse(event.nativeEvent.data);
|
|
670
|
+
} catch {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (!data || typeof data !== "object") return;
|
|
674
|
+
const msg = data;
|
|
675
|
+
if (msg.frameId !== this.frameId) return;
|
|
676
|
+
if (msg.type === IFRAME_READY_EVENT) {
|
|
677
|
+
this.markReady();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (typeof msg.id === "string" && this.messageHandlers.has(msg.id)) {
|
|
681
|
+
const handler = this.messageHandlers.get(msg.id);
|
|
682
|
+
handler(msg);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (msg.type === POST_MESSAGE_EVENT_TYPE) {
|
|
686
|
+
const evt = msg;
|
|
687
|
+
this.onEvent?.(evt.event, evt.data);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Drop pending handlers and clear ready promise. Call when the host
|
|
692
|
+
* unmounts the WebView.
|
|
693
|
+
*/
|
|
694
|
+
destroy() {
|
|
695
|
+
if (this.readyTimer) {
|
|
696
|
+
clearTimeout(this.readyTimer);
|
|
697
|
+
this.readyTimer = null;
|
|
698
|
+
}
|
|
699
|
+
if (this.rejectReady && this.readyPromise) {
|
|
700
|
+
this.readyPromise.catch(() => {
|
|
701
|
+
});
|
|
702
|
+
this.rejectReady(new Error("Bridge destroyed"));
|
|
703
|
+
}
|
|
704
|
+
this.resolveReady = null;
|
|
705
|
+
this.rejectReady = null;
|
|
706
|
+
this.readyPromise = null;
|
|
707
|
+
this.ready = false;
|
|
708
|
+
this.messageHandlers.clear();
|
|
709
|
+
this.webView = null;
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
// src/native/provider/NativeProvider.ts
|
|
714
|
+
var DEFAULT_WALLET_URL = "https://wallet.thru.org/embedded/native";
|
|
715
|
+
var DEFAULT_ORIGIN = "thru-mobile://app";
|
|
716
|
+
var TRANSPARENT_FOCUS_SETTLE_MS = 500;
|
|
717
|
+
var NativeProvider = class {
|
|
718
|
+
constructor(config = {}) {
|
|
719
|
+
this.connected = false;
|
|
720
|
+
this.accounts = [];
|
|
721
|
+
this.selectedAccount = null;
|
|
722
|
+
this.isSurfaceShown = false;
|
|
723
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
724
|
+
/** Pass through the WebView's `onMessage` event handler. */
|
|
725
|
+
this.onMessage = (event) => {
|
|
726
|
+
this.bridge.onMessage(event);
|
|
727
|
+
};
|
|
728
|
+
const walletUrl = config.walletUrl ?? DEFAULT_WALLET_URL;
|
|
729
|
+
this.origin = config.origin ?? DEFAULT_ORIGIN;
|
|
730
|
+
this.transparent = config.walletExperience === "transparent";
|
|
731
|
+
this.bridge = new WebViewBridge({ walletUrl });
|
|
732
|
+
this.bridge.onEvent = (eventType, payload) => {
|
|
733
|
+
if (this.transparent && eventType === EMBEDDED_PROVIDER_EVENTS.UI_SHOW) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
this.emit(eventType, payload);
|
|
737
|
+
if (eventType === EMBEDDED_PROVIDER_EVENTS.UI_SHOW) {
|
|
738
|
+
this.requestShow();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (eventType === EMBEDDED_PROVIDER_EVENTS.DISCONNECT || eventType === EMBEDDED_PROVIDER_EVENTS.LOCK) {
|
|
742
|
+
this.clearConnection();
|
|
743
|
+
this.requestHide();
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (eventType === EMBEDDED_PROVIDER_EVENTS.ACCOUNT_CHANGED) {
|
|
747
|
+
const account = payload?.account ?? null;
|
|
748
|
+
this.refreshAccountCache(account);
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
const addressTypes = config.addressTypes ?? [AddressType.THRU];
|
|
752
|
+
if (addressTypes.includes(AddressType.THRU)) {
|
|
753
|
+
this._thruChain = new NativeThruChain(
|
|
754
|
+
this.bridge,
|
|
755
|
+
this,
|
|
756
|
+
this.origin,
|
|
757
|
+
config.signingSessions
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/** Hand the bridge a WebView ref. Required before connect/sign. */
|
|
762
|
+
attachWebView(ref) {
|
|
763
|
+
this.bridge.attachWebView(ref);
|
|
764
|
+
}
|
|
765
|
+
/** Mark a direct top-level WebView wallet document as ready. */
|
|
766
|
+
markWebViewReady() {
|
|
767
|
+
this.bridge.markReady();
|
|
768
|
+
}
|
|
769
|
+
/** Build the URL to load inside the shell <iframe>. The host shell
|
|
770
|
+
template should substitute this for WALLET_URL_PLACEHOLDER. */
|
|
771
|
+
getIframeSrc() {
|
|
772
|
+
return this.bridge.getIframeSrc();
|
|
773
|
+
}
|
|
774
|
+
/** Wallet origin (e.g. https://wallet.thru.org). The shell template
|
|
775
|
+
should substitute this for WALLET_ORIGIN_PLACEHOLDER. */
|
|
776
|
+
getWalletOrigin() {
|
|
777
|
+
return this.bridge.walletOrigin;
|
|
778
|
+
}
|
|
779
|
+
/** Wait for the iframe's IFRAME_READY_EVENT handshake. */
|
|
780
|
+
async initialize() {
|
|
781
|
+
await this.bridge.awaitReady();
|
|
782
|
+
}
|
|
783
|
+
/** Open or focus the wallet host surface. Transparent hosts use this
|
|
784
|
+
to give WKWebView a focused document for WebAuthn without showing
|
|
785
|
+
wallet UI. */
|
|
786
|
+
async requestShow() {
|
|
787
|
+
if (this.transparent) {
|
|
788
|
+
if (!this.isSurfaceShown) {
|
|
789
|
+
this.isSurfaceShown = true;
|
|
790
|
+
this.onShowRequested?.();
|
|
791
|
+
}
|
|
792
|
+
await new Promise(
|
|
793
|
+
(resolve) => setTimeout(resolve, TRANSPARENT_FOCUS_SETTLE_MS)
|
|
794
|
+
);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (this.isSurfaceShown) return;
|
|
798
|
+
this.isSurfaceShown = true;
|
|
799
|
+
this.onShowRequested?.();
|
|
800
|
+
}
|
|
801
|
+
/** Close the wallet UI (called internally; also exposed for host). */
|
|
802
|
+
requestHide() {
|
|
803
|
+
this.isSurfaceShown = false;
|
|
804
|
+
this.onHideRequested?.();
|
|
805
|
+
}
|
|
806
|
+
/** Reject pending requests after a user-driven native sheet dismiss. */
|
|
807
|
+
rejectPendingRequests(message) {
|
|
808
|
+
this.bridge.rejectPendingRequests(message);
|
|
809
|
+
}
|
|
810
|
+
async connect(options) {
|
|
811
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT_START, {});
|
|
812
|
+
try {
|
|
813
|
+
await this.requestShow();
|
|
814
|
+
const payload = {};
|
|
815
|
+
if (options?.metadata) payload.metadata = options.metadata;
|
|
816
|
+
if (options?.preferredAccountAddress) {
|
|
817
|
+
payload.preferredAccountAddress = options.preferredAccountAddress;
|
|
818
|
+
}
|
|
819
|
+
if (options?.intent) payload.intent = options.intent;
|
|
820
|
+
const response = await this.bridge.sendMessage({
|
|
821
|
+
id: createRequestId(),
|
|
822
|
+
type: POST_MESSAGE_REQUEST_TYPES.CONNECT,
|
|
823
|
+
payload,
|
|
824
|
+
origin: this.origin
|
|
825
|
+
});
|
|
826
|
+
const result = normalizeWalletAccountResult(response.result);
|
|
827
|
+
if (!result.selectedAccount) {
|
|
828
|
+
throw new Error("Wallet did not return an account");
|
|
829
|
+
}
|
|
830
|
+
this.connected = true;
|
|
831
|
+
this.accounts = result.accounts;
|
|
832
|
+
this.selectedAccount = result.selectedAccount;
|
|
833
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT, result);
|
|
834
|
+
this.requestHide();
|
|
835
|
+
return result;
|
|
836
|
+
} catch (error) {
|
|
837
|
+
this.requestHide();
|
|
838
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT_ERROR, { error });
|
|
839
|
+
throw error;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async createAccount(options) {
|
|
843
|
+
try {
|
|
844
|
+
await this.requestShow();
|
|
845
|
+
const payload = {};
|
|
846
|
+
if (options?.accountName) payload.accountName = options.accountName;
|
|
847
|
+
if (options?.metadata) payload.metadata = options.metadata;
|
|
848
|
+
const response = await this.bridge.sendMessage({
|
|
849
|
+
id: createRequestId(),
|
|
850
|
+
type: POST_MESSAGE_REQUEST_TYPES.CREATE_ACCOUNT,
|
|
851
|
+
payload,
|
|
852
|
+
origin: this.origin
|
|
853
|
+
});
|
|
854
|
+
const normalized = normalizeWalletAccountResult(
|
|
855
|
+
response.result,
|
|
856
|
+
response.result.selectedAccount ?? response.result.account
|
|
857
|
+
);
|
|
858
|
+
const selectedAccount = normalized.selectedAccount ?? response.result.account;
|
|
859
|
+
if (!selectedAccount) {
|
|
860
|
+
throw new Error("Wallet did not return a created account");
|
|
861
|
+
}
|
|
862
|
+
const result = {
|
|
863
|
+
...response.result,
|
|
864
|
+
accounts: normalized.accounts,
|
|
865
|
+
selectedAccount,
|
|
866
|
+
account: selectedAccount
|
|
867
|
+
};
|
|
868
|
+
this.connected = true;
|
|
869
|
+
this.accounts = result.accounts;
|
|
870
|
+
this.selectedAccount = result.selectedAccount;
|
|
871
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT, {
|
|
872
|
+
accounts: result.accounts,
|
|
873
|
+
selectedAccount: result.selectedAccount,
|
|
874
|
+
status: "completed",
|
|
875
|
+
metadata: options?.metadata
|
|
876
|
+
});
|
|
877
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.ACCOUNT_CHANGED, {
|
|
878
|
+
account: result.selectedAccount
|
|
879
|
+
});
|
|
880
|
+
this.requestHide();
|
|
881
|
+
return result;
|
|
882
|
+
} catch (error) {
|
|
883
|
+
this.requestHide();
|
|
884
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.ERROR, { error });
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
async getConnectionState(options) {
|
|
889
|
+
const payload = {};
|
|
890
|
+
if (options?.metadata) payload.metadata = options.metadata;
|
|
891
|
+
if (options?.preferredAccountAddress) {
|
|
892
|
+
payload.preferredAccountAddress = options.preferredAccountAddress;
|
|
893
|
+
}
|
|
894
|
+
const response = await this.bridge.sendMessage({
|
|
895
|
+
id: createRequestId(),
|
|
896
|
+
type: POST_MESSAGE_REQUEST_TYPES.GET_CONNECTION_STATE,
|
|
897
|
+
payload,
|
|
898
|
+
origin: this.origin
|
|
899
|
+
});
|
|
900
|
+
const result = normalizeConnectionStateResult(response.result);
|
|
901
|
+
if (result.isAuthorized && result.hasPasskey && result.accounts.length > 0) {
|
|
902
|
+
this.hydrateConnection(
|
|
903
|
+
{
|
|
904
|
+
accounts: result.accounts,
|
|
905
|
+
status: "completed",
|
|
906
|
+
metadata: result.metadata ?? void 0,
|
|
907
|
+
selectedAccount: result.selectedAccount
|
|
908
|
+
},
|
|
909
|
+
result.selectedAccount?.address ?? null
|
|
910
|
+
);
|
|
911
|
+
} else {
|
|
912
|
+
this.clearConnection();
|
|
913
|
+
}
|
|
914
|
+
return result;
|
|
915
|
+
}
|
|
916
|
+
async disconnect() {
|
|
917
|
+
try {
|
|
918
|
+
await this.bridge.sendMessage({
|
|
919
|
+
id: createRequestId(),
|
|
920
|
+
type: POST_MESSAGE_REQUEST_TYPES.DISCONNECT,
|
|
921
|
+
origin: this.origin
|
|
922
|
+
});
|
|
923
|
+
this.clearConnection();
|
|
924
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.DISCONNECT, {});
|
|
925
|
+
} catch (error) {
|
|
926
|
+
this.clearConnection();
|
|
927
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.ERROR, { error });
|
|
928
|
+
throw error;
|
|
929
|
+
} finally {
|
|
930
|
+
this.requestHide();
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
isConnected() {
|
|
934
|
+
return this.connected;
|
|
935
|
+
}
|
|
936
|
+
isTransparent() {
|
|
937
|
+
return this.transparent;
|
|
938
|
+
}
|
|
939
|
+
hydrateConnection(result, selectedAccountAddress) {
|
|
940
|
+
const selectedAccount = resolveWalletAccountByAddress(result.accounts, selectedAccountAddress) ?? result.selectedAccount ?? null;
|
|
941
|
+
const normalized = normalizeWalletAccountResult(result, selectedAccount);
|
|
942
|
+
this.connected = true;
|
|
943
|
+
this.accounts = normalized.accounts;
|
|
944
|
+
this.selectedAccount = normalized.selectedAccount;
|
|
945
|
+
}
|
|
946
|
+
clearConnection() {
|
|
947
|
+
this.connected = false;
|
|
948
|
+
this.accounts = [];
|
|
949
|
+
this.selectedAccount = null;
|
|
950
|
+
}
|
|
951
|
+
getAccounts() {
|
|
952
|
+
return this.accounts;
|
|
953
|
+
}
|
|
954
|
+
getSelectedAccount() {
|
|
955
|
+
return this.selectedAccount;
|
|
956
|
+
}
|
|
957
|
+
async selectAccount(publicKey) {
|
|
958
|
+
if (!this.connected) throw new Error("Wallet not connected");
|
|
959
|
+
const payload = { publicKey };
|
|
960
|
+
const response = await this.bridge.sendMessage({
|
|
961
|
+
id: createRequestId(),
|
|
962
|
+
type: POST_MESSAGE_REQUEST_TYPES.SELECT_ACCOUNT,
|
|
963
|
+
payload,
|
|
964
|
+
origin: this.origin
|
|
965
|
+
});
|
|
966
|
+
const account = response.result.account;
|
|
967
|
+
this.refreshAccountCache(account);
|
|
968
|
+
return account;
|
|
969
|
+
}
|
|
970
|
+
async manageAccounts() {
|
|
971
|
+
if (!this.connected) throw new Error("Wallet not connected");
|
|
972
|
+
try {
|
|
973
|
+
await this.requestShow();
|
|
974
|
+
const response = await this.bridge.sendMessage({
|
|
975
|
+
id: createRequestId(),
|
|
976
|
+
type: POST_MESSAGE_REQUEST_TYPES.MANAGE_ACCOUNTS,
|
|
977
|
+
origin: this.origin
|
|
978
|
+
});
|
|
979
|
+
const result = normalizeWalletAccountResult(response.result);
|
|
980
|
+
this.accounts = result.accounts;
|
|
981
|
+
this.selectedAccount = result.selectedAccount;
|
|
982
|
+
this.requestHide();
|
|
983
|
+
return result;
|
|
984
|
+
} catch (error) {
|
|
985
|
+
this.requestHide();
|
|
986
|
+
this.emit(EMBEDDED_PROVIDER_EVENTS.ERROR, { error });
|
|
987
|
+
throw error;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
get thru() {
|
|
991
|
+
if (!this._thruChain) {
|
|
992
|
+
throw new Error("Thru chain not enabled in provider config");
|
|
993
|
+
}
|
|
994
|
+
return this._thruChain;
|
|
995
|
+
}
|
|
996
|
+
on(event, cb) {
|
|
997
|
+
if (!this.eventListeners.has(event)) {
|
|
998
|
+
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
999
|
+
}
|
|
1000
|
+
this.eventListeners.get(event).add(cb);
|
|
1001
|
+
}
|
|
1002
|
+
off(event, cb) {
|
|
1003
|
+
this.eventListeners.get(event)?.delete(cb);
|
|
1004
|
+
}
|
|
1005
|
+
/** Internal: used by NativeThruChain. */
|
|
1006
|
+
getBridge() {
|
|
1007
|
+
return this.bridge;
|
|
1008
|
+
}
|
|
1009
|
+
destroy() {
|
|
1010
|
+
this.bridge.destroy();
|
|
1011
|
+
this.eventListeners.clear();
|
|
1012
|
+
this.clearConnection();
|
|
1013
|
+
}
|
|
1014
|
+
emit(event, data) {
|
|
1015
|
+
this.eventListeners.get(event)?.forEach((cb) => {
|
|
1016
|
+
try {
|
|
1017
|
+
cb(data);
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
console.error(`[NativeProvider] listener error for ${event}:`, err);
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
refreshAccountCache(account) {
|
|
1024
|
+
if (!account) {
|
|
1025
|
+
this.accounts = [];
|
|
1026
|
+
this.selectedAccount = null;
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
this.accounts = [account];
|
|
1030
|
+
this.selectedAccount = account;
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
var DEFAULT_STORAGE_KEY = "thru.native-sdk.connection.v1";
|
|
1034
|
+
var SELECTED_ACCOUNT_STORAGE_KEY_SUFFIX = ".selected-account.v1";
|
|
1035
|
+
var SIGNING_SESSION_STORAGE_KEY_SUFFIX = ".signing-sessions.v1";
|
|
1036
|
+
var DEFAULT_NATIVE_WALLET_URL = "https://wallet.thru.org/embedded/native";
|
|
1037
|
+
var DEFAULT_TRANSPARENT_WALLET_URL = "https://wallet.thru.org/embedded/native/transparent";
|
|
1038
|
+
var CHECKING_WALLET_AVAILABILITY = {
|
|
1039
|
+
status: "checking",
|
|
1040
|
+
isAuthorized: false,
|
|
1041
|
+
isConnected: false,
|
|
1042
|
+
isUnlocked: false,
|
|
1043
|
+
hasPasskey: false,
|
|
1044
|
+
hasWalletAccount: false,
|
|
1045
|
+
accounts: [],
|
|
1046
|
+
selectedAccount: null,
|
|
1047
|
+
metadata: null,
|
|
1048
|
+
error: null
|
|
1049
|
+
};
|
|
1050
|
+
function completeAppMetadata(metadata) {
|
|
1051
|
+
if (!metadata?.appId || !metadata.appName || !metadata.appUrl) {
|
|
1052
|
+
return void 0;
|
|
1053
|
+
}
|
|
1054
|
+
return {
|
|
1055
|
+
appId: metadata.appId,
|
|
1056
|
+
appName: metadata.appName,
|
|
1057
|
+
appUrl: metadata.appUrl,
|
|
1058
|
+
...metadata.imageUrl ? { imageUrl: metadata.imageUrl } : {}
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
var NativeSDK = class {
|
|
1062
|
+
constructor(config = {}) {
|
|
1063
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
1064
|
+
this.initialized = false;
|
|
1065
|
+
this.thruClient = null;
|
|
1066
|
+
this.connectInFlight = null;
|
|
1067
|
+
this.lastConnectResult = null;
|
|
1068
|
+
this.walletAvailability = CHECKING_WALLET_AVAILABILITY;
|
|
1069
|
+
/** Bind to the WebView's `onMessage` handler. */
|
|
1070
|
+
this.onMessage = (event) => {
|
|
1071
|
+
this.provider.onMessage(event);
|
|
1072
|
+
};
|
|
1073
|
+
this.origin = config.origin ?? "thru-mobile://app";
|
|
1074
|
+
this.rpcUrl = config.rpcUrl;
|
|
1075
|
+
this.storage = config.storage;
|
|
1076
|
+
this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
1077
|
+
this.selectedAccountStorageKey = config.selectedAccountStorageKey ?? `${this.storageKey}${SELECTED_ACCOUNT_STORAGE_KEY_SUFFIX}`;
|
|
1078
|
+
this.iosWebViewMode = config.iosWebViewMode ?? "shell-iframe";
|
|
1079
|
+
this.walletExperience = config.walletExperience ?? "standard";
|
|
1080
|
+
this.defaultMetadata = config.metadata;
|
|
1081
|
+
const walletUrl = config.walletUrl ?? (this.walletExperience === "transparent" ? DEFAULT_TRANSPARENT_WALLET_URL : DEFAULT_NATIVE_WALLET_URL);
|
|
1082
|
+
const walletOrigin = new URL(walletUrl).origin;
|
|
1083
|
+
const signingSessions = this.storage ? new SigningSessionDescriptorStore(
|
|
1084
|
+
this.storage,
|
|
1085
|
+
resolveSigningSessionStorageKey({
|
|
1086
|
+
walletOrigin,
|
|
1087
|
+
appOrigin: this.origin,
|
|
1088
|
+
storageKey: config.signingSessionStorageKey ?? `${this.storageKey}${SIGNING_SESSION_STORAGE_KEY_SUFFIX}`
|
|
1089
|
+
})
|
|
1090
|
+
) : void 0;
|
|
1091
|
+
this.provider = new NativeProvider({
|
|
1092
|
+
walletUrl,
|
|
1093
|
+
origin: this.origin,
|
|
1094
|
+
metadata: this.defaultMetadata ? this.resolveMetadata(this.defaultMetadata) : void 0,
|
|
1095
|
+
addressTypes: config.addressTypes ?? [AddressType.THRU],
|
|
1096
|
+
signingSessions,
|
|
1097
|
+
walletExperience: this.walletExperience
|
|
1098
|
+
});
|
|
1099
|
+
this.setupEventForwarding();
|
|
1100
|
+
}
|
|
1101
|
+
/** Hand the WebView ref to the underlying provider/bridge. */
|
|
1102
|
+
attachWebView(ref) {
|
|
1103
|
+
this.provider.attachWebView(ref);
|
|
1104
|
+
}
|
|
1105
|
+
/** Mark a direct top-level WebView wallet document as ready. */
|
|
1106
|
+
markWebViewReady() {
|
|
1107
|
+
this.provider.markWebViewReady();
|
|
1108
|
+
}
|
|
1109
|
+
/** Build the URL to load inside the shell <iframe>. */
|
|
1110
|
+
getIframeSrc() {
|
|
1111
|
+
return this.provider.getIframeSrc();
|
|
1112
|
+
}
|
|
1113
|
+
/** Wallet origin (e.g. https://wallet.thru.org). */
|
|
1114
|
+
getWalletOrigin() {
|
|
1115
|
+
return this.provider.getWalletOrigin();
|
|
1116
|
+
}
|
|
1117
|
+
/** Bind host UI lifecycle handlers used by custom WebView hosts. */
|
|
1118
|
+
setUiHandlers(handlers) {
|
|
1119
|
+
this.provider.onShowRequested = handlers.onShowRequested;
|
|
1120
|
+
this.provider.onHideRequested = handlers.onHideRequested;
|
|
1121
|
+
}
|
|
1122
|
+
clearUiHandlers() {
|
|
1123
|
+
this.provider.onShowRequested = void 0;
|
|
1124
|
+
this.provider.onHideRequested = void 0;
|
|
1125
|
+
}
|
|
1126
|
+
/** Reject in-flight wallet requests after a user-driven host dismiss. */
|
|
1127
|
+
rejectPendingRequests(message) {
|
|
1128
|
+
this.provider.rejectPendingRequests(message);
|
|
1129
|
+
}
|
|
1130
|
+
/** iOS WebView host mode. Non-iOS hosts should ignore this value. */
|
|
1131
|
+
getIosWebViewMode() {
|
|
1132
|
+
return this.iosWebViewMode;
|
|
1133
|
+
}
|
|
1134
|
+
async initialize() {
|
|
1135
|
+
if (this.initialized) return;
|
|
1136
|
+
await this.provider.initialize();
|
|
1137
|
+
this.initialized = true;
|
|
1138
|
+
}
|
|
1139
|
+
async connect(options) {
|
|
1140
|
+
const isAccountSwitch = options?.intent === "switch-account";
|
|
1141
|
+
if (this.connectInFlight) return this.connectInFlight;
|
|
1142
|
+
if (!isAccountSwitch && this.lastConnectResult && this.provider.isConnected()) {
|
|
1143
|
+
return this.lastConnectResult;
|
|
1144
|
+
}
|
|
1145
|
+
this.emit("connect", { status: "connecting" });
|
|
1146
|
+
const inFlight = (async () => {
|
|
1147
|
+
try {
|
|
1148
|
+
await this.provider.requestShow();
|
|
1149
|
+
if (!this.initialized) await this.initialize();
|
|
1150
|
+
const metadata = this.resolveMetadata(options?.metadata);
|
|
1151
|
+
const preferredAccountAddress = isAccountSwitch ? null : options?.preferredAccountAddress ?? await this.readSelectedAccountAddress();
|
|
1152
|
+
const providerOptions = metadata || preferredAccountAddress || options?.intent ? {
|
|
1153
|
+
...metadata ? { metadata } : {},
|
|
1154
|
+
...preferredAccountAddress ? { preferredAccountAddress } : {},
|
|
1155
|
+
...options?.intent ? { intent: options.intent } : {}
|
|
1156
|
+
} : void 0;
|
|
1157
|
+
const result = await this.provider.connect(providerOptions);
|
|
1158
|
+
if (!isAccountSwitch) {
|
|
1159
|
+
await this.applyPreferredSelectedAccount(result.accounts);
|
|
1160
|
+
}
|
|
1161
|
+
const selectedAccount = this.provider.getSelectedAccount() ?? result.selectedAccount ?? null;
|
|
1162
|
+
const activeResult = normalizeWalletAccountResult(
|
|
1163
|
+
{
|
|
1164
|
+
...result,
|
|
1165
|
+
accounts: this.provider.getAccounts(),
|
|
1166
|
+
selectedAccount
|
|
1167
|
+
},
|
|
1168
|
+
selectedAccount
|
|
1169
|
+
);
|
|
1170
|
+
this.lastConnectResult = activeResult;
|
|
1171
|
+
await this.persistSelectedAccountAddress(
|
|
1172
|
+
activeResult.selectedAccount?.address ?? null
|
|
1173
|
+
);
|
|
1174
|
+
await this.clearPersistedConnection();
|
|
1175
|
+
this.setWalletAvailability(
|
|
1176
|
+
walletAvailabilityFromConnectResult(activeResult)
|
|
1177
|
+
);
|
|
1178
|
+
this.emit("connect", activeResult);
|
|
1179
|
+
return activeResult;
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
this.provider.requestHide();
|
|
1182
|
+
if (isUserRejectedError(error) && !isAccountSwitch) {
|
|
1183
|
+
this.provider.clearConnection();
|
|
1184
|
+
this.lastConnectResult = null;
|
|
1185
|
+
await this.clearPersistedConnection();
|
|
1186
|
+
this.clearAuthorizedAvailability();
|
|
1187
|
+
this.emit("disconnect", { reason: "user_rejected" });
|
|
1188
|
+
}
|
|
1189
|
+
this.emit("error", error);
|
|
1190
|
+
throw error;
|
|
1191
|
+
} finally {
|
|
1192
|
+
this.connectInFlight = null;
|
|
1193
|
+
}
|
|
1194
|
+
})();
|
|
1195
|
+
this.connectInFlight = inFlight;
|
|
1196
|
+
return inFlight;
|
|
1197
|
+
}
|
|
1198
|
+
async signIn(options) {
|
|
1199
|
+
return this.connect({
|
|
1200
|
+
metadata: this.resolveSignInMetadata(options),
|
|
1201
|
+
...options.intent ? { intent: options.intent } : {}
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
async createAccount(options = {}) {
|
|
1205
|
+
this.emit("connect", { status: "connecting" });
|
|
1206
|
+
try {
|
|
1207
|
+
await this.provider.requestShow();
|
|
1208
|
+
if (!this.initialized) await this.initialize();
|
|
1209
|
+
const metadata = this.resolveMetadata(options.metadata);
|
|
1210
|
+
const result = await this.provider.createAccount({
|
|
1211
|
+
...options.accountName ? { accountName: options.accountName } : {},
|
|
1212
|
+
...metadata ? { metadata } : {}
|
|
1213
|
+
});
|
|
1214
|
+
const selectedAccount = result.selectedAccount ?? result.account;
|
|
1215
|
+
const activeResult = {
|
|
1216
|
+
...result,
|
|
1217
|
+
accounts: this.provider.getAccounts(),
|
|
1218
|
+
selectedAccount,
|
|
1219
|
+
account: selectedAccount
|
|
1220
|
+
};
|
|
1221
|
+
const completedResult = {
|
|
1222
|
+
accounts: activeResult.accounts,
|
|
1223
|
+
selectedAccount: activeResult.selectedAccount,
|
|
1224
|
+
status: "completed",
|
|
1225
|
+
metadata: completeAppMetadata(metadata)
|
|
1226
|
+
};
|
|
1227
|
+
this.lastConnectResult = completedResult;
|
|
1228
|
+
await this.persistSelectedAccountAddress(
|
|
1229
|
+
activeResult.selectedAccount.address
|
|
1230
|
+
);
|
|
1231
|
+
await this.clearPersistedConnection();
|
|
1232
|
+
this.setWalletAvailability(
|
|
1233
|
+
walletAvailabilityFromConnectResult(completedResult)
|
|
1234
|
+
);
|
|
1235
|
+
this.emit("connect", completedResult);
|
|
1236
|
+
this.emit("accountChanged", activeResult.selectedAccount);
|
|
1237
|
+
return activeResult;
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
this.provider.requestHide();
|
|
1240
|
+
this.emit("error", error);
|
|
1241
|
+
throw error;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
async disconnect() {
|
|
1245
|
+
try {
|
|
1246
|
+
await this.provider.disconnect();
|
|
1247
|
+
this.emit("disconnect", {});
|
|
1248
|
+
this.lastConnectResult = null;
|
|
1249
|
+
await this.persistSelectedAccountAddress(null);
|
|
1250
|
+
await this.clearPersistedConnection();
|
|
1251
|
+
this.clearAuthorizedAvailability();
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
this.emit("error", error);
|
|
1254
|
+
throw error;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
isConnected() {
|
|
1258
|
+
return this.provider.isConnected();
|
|
1259
|
+
}
|
|
1260
|
+
getWalletAvailability() {
|
|
1261
|
+
return this.walletAvailability;
|
|
1262
|
+
}
|
|
1263
|
+
async restoreConnection(options = {}) {
|
|
1264
|
+
await this.clearPersistedConnection();
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
async syncConnectionState(options) {
|
|
1268
|
+
try {
|
|
1269
|
+
const state = await this.requestConnectionState(options);
|
|
1270
|
+
this.setWalletAvailability(walletAvailabilityFromConnectionState(state));
|
|
1271
|
+
await this.applyConnectionState(state);
|
|
1272
|
+
return state;
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
this.setWalletAvailability(walletAvailabilityFromError(error));
|
|
1275
|
+
this.emit("error", error);
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
async refreshWalletAvailability(options) {
|
|
1280
|
+
try {
|
|
1281
|
+
const state = await this.requestConnectionState(options);
|
|
1282
|
+
const availability = walletAvailabilityFromConnectionState(state);
|
|
1283
|
+
this.setWalletAvailability(availability);
|
|
1284
|
+
await this.applyConnectionState(state);
|
|
1285
|
+
return availability;
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
const availability = walletAvailabilityFromError(error);
|
|
1288
|
+
this.setWalletAvailability(availability);
|
|
1289
|
+
this.emit("error", error);
|
|
1290
|
+
return availability;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
getAccounts() {
|
|
1294
|
+
const accounts = this.provider.getAccounts();
|
|
1295
|
+
const activeAccounts = this.refreshCachedAccounts(
|
|
1296
|
+
accounts,
|
|
1297
|
+
this.provider.getSelectedAccount()
|
|
1298
|
+
);
|
|
1299
|
+
return activeAccounts;
|
|
1300
|
+
}
|
|
1301
|
+
getSelectedAccount() {
|
|
1302
|
+
return this.provider.getSelectedAccount();
|
|
1303
|
+
}
|
|
1304
|
+
async selectAccount(publicKey) {
|
|
1305
|
+
const account = await this.provider.selectAccount(publicKey);
|
|
1306
|
+
this.refreshCachedAccounts(this.provider.getAccounts(), account);
|
|
1307
|
+
await this.persistSelectedAccountAddress(account.address);
|
|
1308
|
+
return account;
|
|
1309
|
+
}
|
|
1310
|
+
async manageAccounts() {
|
|
1311
|
+
if (!this.initialized) await this.initialize();
|
|
1312
|
+
const result = await this.provider.manageAccounts();
|
|
1313
|
+
const activeResult = normalizeWalletAccountResult(result);
|
|
1314
|
+
const selectedAccount = activeResult.selectedAccount ?? null;
|
|
1315
|
+
this.refreshCachedAccounts(activeResult.accounts, selectedAccount);
|
|
1316
|
+
await this.persistSelectedAccountAddress(selectedAccount?.address ?? null);
|
|
1317
|
+
if (this.lastConnectResult) {
|
|
1318
|
+
this.setWalletAvailability(
|
|
1319
|
+
walletAvailabilityFromConnectResult(this.lastConnectResult)
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
this.emit("accountChanged", selectedAccount);
|
|
1323
|
+
return activeResult;
|
|
1324
|
+
}
|
|
1325
|
+
get thru() {
|
|
1326
|
+
return this.provider.thru;
|
|
1327
|
+
}
|
|
1328
|
+
on(event, callback) {
|
|
1329
|
+
if (!this.eventListeners.has(event)) {
|
|
1330
|
+
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
1331
|
+
}
|
|
1332
|
+
this.eventListeners.get(event).add(callback);
|
|
1333
|
+
}
|
|
1334
|
+
off(event, callback) {
|
|
1335
|
+
this.eventListeners.get(event)?.delete(callback);
|
|
1336
|
+
}
|
|
1337
|
+
once(event, callback) {
|
|
1338
|
+
const wrapped = (...args) => {
|
|
1339
|
+
callback(...args);
|
|
1340
|
+
this.off(event, wrapped);
|
|
1341
|
+
};
|
|
1342
|
+
this.on(event, wrapped);
|
|
1343
|
+
}
|
|
1344
|
+
destroy() {
|
|
1345
|
+
this.provider.destroy();
|
|
1346
|
+
this.eventListeners.clear();
|
|
1347
|
+
this.initialized = false;
|
|
1348
|
+
this.connectInFlight = null;
|
|
1349
|
+
this.lastConnectResult = null;
|
|
1350
|
+
this.walletAvailability = CHECKING_WALLET_AVAILABILITY;
|
|
1351
|
+
}
|
|
1352
|
+
/** Lazily-instantiated Thru chain client. */
|
|
1353
|
+
getThru() {
|
|
1354
|
+
if (!this.thruClient) {
|
|
1355
|
+
this.thruClient = createThruClient({ baseUrl: this.rpcUrl });
|
|
1356
|
+
}
|
|
1357
|
+
return this.thruClient;
|
|
1358
|
+
}
|
|
1359
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1360
|
+
emit(event, data) {
|
|
1361
|
+
this.eventListeners.get(event)?.forEach((cb) => {
|
|
1362
|
+
try {
|
|
1363
|
+
cb(data);
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
console.error(`[NativeSDK] listener error for ${event}:`, err);
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
setupEventForwarding() {
|
|
1370
|
+
this.provider.on(EMBEDDED_PROVIDER_EVENTS.DISCONNECT, (data) => {
|
|
1371
|
+
this.lastConnectResult = null;
|
|
1372
|
+
this.clearAuthorizedAvailability();
|
|
1373
|
+
this.emit("disconnect", data);
|
|
1374
|
+
});
|
|
1375
|
+
this.provider.on(EMBEDDED_PROVIDER_EVENTS.ERROR, (data) => {
|
|
1376
|
+
this.emit("error", data);
|
|
1377
|
+
});
|
|
1378
|
+
this.provider.on(EMBEDDED_PROVIDER_EVENTS.LOCK, (data) => {
|
|
1379
|
+
this.lastConnectResult = null;
|
|
1380
|
+
this.clearAuthorizedAvailability();
|
|
1381
|
+
this.emit("lock", data);
|
|
1382
|
+
this.emit("disconnect", { reason: "locked" });
|
|
1383
|
+
});
|
|
1384
|
+
this.provider.on(EMBEDDED_PROVIDER_EVENTS.ACCOUNT_CHANGED, (data) => {
|
|
1385
|
+
const payload = data;
|
|
1386
|
+
const account = payload?.account ?? null;
|
|
1387
|
+
this.refreshCachedAccounts(this.provider.getAccounts(), account);
|
|
1388
|
+
if (account) void this.persistSelectedAccountAddress(account.address);
|
|
1389
|
+
this.emit("accountChanged", account);
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
async requestConnectionState(options) {
|
|
1393
|
+
if (!this.initialized) await this.initialize();
|
|
1394
|
+
const metadata = options?.metadata ?? this.lastConnectResult?.metadata ?? this.defaultMetadata ?? void 0;
|
|
1395
|
+
const providerOptions = metadata ? { metadata: this.resolveMetadata(metadata) } : void 0;
|
|
1396
|
+
const preferredAccountAddress = options?.preferredAccountAddress ?? await this.readSelectedAccountAddress();
|
|
1397
|
+
const nextProviderOptions = providerOptions || preferredAccountAddress ? {
|
|
1398
|
+
...providerOptions ?? {},
|
|
1399
|
+
...preferredAccountAddress ? { preferredAccountAddress } : {}
|
|
1400
|
+
} : void 0;
|
|
1401
|
+
const state = await this.provider.getConnectionState(nextProviderOptions);
|
|
1402
|
+
return normalizeConnectionStateResult(state);
|
|
1403
|
+
}
|
|
1404
|
+
async applyConnectionState(state) {
|
|
1405
|
+
if (state.isAuthorized && state.hasPasskey && state.accounts.length > 0) {
|
|
1406
|
+
const result = {
|
|
1407
|
+
accounts: state.accounts,
|
|
1408
|
+
selectedAccount: state.selectedAccount,
|
|
1409
|
+
status: "completed",
|
|
1410
|
+
metadata: state.metadata ?? void 0
|
|
1411
|
+
};
|
|
1412
|
+
const activeResult = normalizeWalletAccountResult(result);
|
|
1413
|
+
this.lastConnectResult = activeResult;
|
|
1414
|
+
await this.persistSelectedAccountAddress(
|
|
1415
|
+
this.provider.getSelectedAccount()?.address ?? activeResult.selectedAccount?.address ?? null
|
|
1416
|
+
);
|
|
1417
|
+
await this.clearPersistedConnection();
|
|
1418
|
+
this.emit("connect", activeResult);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const wasConnected = this.provider.isConnected() || !!this.lastConnectResult;
|
|
1422
|
+
this.provider.clearConnection();
|
|
1423
|
+
this.lastConnectResult = null;
|
|
1424
|
+
await this.clearPersistedConnection();
|
|
1425
|
+
if (wasConnected) {
|
|
1426
|
+
this.emit("disconnect", { reason: "state_unavailable" });
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
setWalletAvailability(availability) {
|
|
1430
|
+
this.walletAvailability = availability;
|
|
1431
|
+
this.emit("availabilityChanged", availability);
|
|
1432
|
+
}
|
|
1433
|
+
clearAuthorizedAvailability() {
|
|
1434
|
+
const previous = this.walletAvailability.status === "ready" ? this.walletAvailability : null;
|
|
1435
|
+
this.setWalletAvailability({
|
|
1436
|
+
status: "ready",
|
|
1437
|
+
isAuthorized: false,
|
|
1438
|
+
isConnected: false,
|
|
1439
|
+
isUnlocked: false,
|
|
1440
|
+
hasPasskey: previous?.hasPasskey ?? false,
|
|
1441
|
+
hasWalletAccount: previous?.hasWalletAccount ?? false,
|
|
1442
|
+
accounts: [],
|
|
1443
|
+
selectedAccount: null,
|
|
1444
|
+
metadata: null,
|
|
1445
|
+
error: null
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
resolveMetadata(input) {
|
|
1449
|
+
const effectiveInput = input ?? this.defaultMetadata;
|
|
1450
|
+
if (!effectiveInput) {
|
|
1451
|
+
return { appId: this.origin };
|
|
1452
|
+
}
|
|
1453
|
+
const metadata = {
|
|
1454
|
+
appId: effectiveInput.appId ?? this.origin
|
|
1455
|
+
};
|
|
1456
|
+
if (effectiveInput.appUrl) metadata.appUrl = effectiveInput.appUrl;
|
|
1457
|
+
if (effectiveInput.appName) metadata.appName = effectiveInput.appName;
|
|
1458
|
+
if (effectiveInput.imageUrl) metadata.imageUrl = effectiveInput.imageUrl;
|
|
1459
|
+
return metadata;
|
|
1460
|
+
}
|
|
1461
|
+
resolveSignInMetadata(options) {
|
|
1462
|
+
const metadata = {
|
|
1463
|
+
appId: options.app_id,
|
|
1464
|
+
appName: options.app_display_name
|
|
1465
|
+
};
|
|
1466
|
+
if (options.app_url) metadata.appUrl = options.app_url;
|
|
1467
|
+
if (options.image_url) metadata.imageUrl = options.image_url;
|
|
1468
|
+
return metadata;
|
|
1469
|
+
}
|
|
1470
|
+
refreshCachedAccounts(accounts, selectedAccount) {
|
|
1471
|
+
const active = normalizeActiveWalletAccounts(accounts, selectedAccount);
|
|
1472
|
+
const nextAccounts = active.accounts;
|
|
1473
|
+
const nextSelectedAccount = active.selectedAccount;
|
|
1474
|
+
if (this.lastConnectResult && this.provider.isConnected()) {
|
|
1475
|
+
this.lastConnectResult = {
|
|
1476
|
+
...this.lastConnectResult,
|
|
1477
|
+
accounts: nextAccounts,
|
|
1478
|
+
selectedAccount: nextSelectedAccount
|
|
1479
|
+
};
|
|
1480
|
+
if (nextSelectedAccount) {
|
|
1481
|
+
void this.persistSelectedAccountAddress(nextSelectedAccount.address);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
return nextAccounts;
|
|
1485
|
+
}
|
|
1486
|
+
async applyPreferredSelectedAccount(accounts) {
|
|
1487
|
+
const preferredAddress = await this.readSelectedAccountAddress();
|
|
1488
|
+
if (!preferredAddress) return;
|
|
1489
|
+
if (!accounts.some((account) => account.address === preferredAddress)) {
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (this.provider.getSelectedAccount()?.address === preferredAddress) {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
try {
|
|
1496
|
+
await this.provider.selectAccount(preferredAddress);
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
console.warn("[NativeSDK] Failed to restore selected account:", error);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
async persistSelectedAccountAddress(selectedAccountAddress) {
|
|
1502
|
+
if (!this.storage) return;
|
|
1503
|
+
try {
|
|
1504
|
+
if (!selectedAccountAddress) {
|
|
1505
|
+
await this.storage.removeItem(this.selectedAccountStorageKey);
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const snapshot = {
|
|
1509
|
+
version: 1,
|
|
1510
|
+
origin: this.origin,
|
|
1511
|
+
walletOrigin: this.provider.getWalletOrigin(),
|
|
1512
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1513
|
+
selectedAccountAddress
|
|
1514
|
+
};
|
|
1515
|
+
await this.storage.setItem(
|
|
1516
|
+
this.selectedAccountStorageKey,
|
|
1517
|
+
JSON.stringify(snapshot)
|
|
1518
|
+
);
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
console.warn("[NativeSDK] Failed to persist selected account:", error);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
async clearPersistedConnection() {
|
|
1524
|
+
if (!this.storage) return;
|
|
1525
|
+
try {
|
|
1526
|
+
await this.storage.removeItem(this.storageKey);
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
console.warn("[NativeSDK] Failed to clear connection state:", error);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
async readSelectedAccountAddress() {
|
|
1532
|
+
if (!this.storage) return null;
|
|
1533
|
+
try {
|
|
1534
|
+
const raw = await this.storage.getItem(this.selectedAccountStorageKey);
|
|
1535
|
+
if (!raw) return null;
|
|
1536
|
+
const parsed = JSON.parse(
|
|
1537
|
+
raw
|
|
1538
|
+
);
|
|
1539
|
+
if (parsed.version !== 1 || parsed.origin !== this.origin || parsed.walletOrigin !== this.provider.getWalletOrigin() || typeof parsed.selectedAccountAddress !== "string" || parsed.selectedAccountAddress.length === 0) {
|
|
1540
|
+
await this.storage.removeItem(this.selectedAccountStorageKey);
|
|
1541
|
+
return null;
|
|
1542
|
+
}
|
|
1543
|
+
return parsed.selectedAccountAddress;
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
console.warn("[NativeSDK] Failed to restore selected account:", error);
|
|
1546
|
+
try {
|
|
1547
|
+
await this.storage.removeItem(this.selectedAccountStorageKey);
|
|
1548
|
+
} catch {
|
|
1549
|
+
}
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
function walletAvailabilityFromConnectResult(result, selectedAccount) {
|
|
1555
|
+
const active = normalizeWalletAccountResult(result, null);
|
|
1556
|
+
const hasActiveAccount = active.accounts.length > 0;
|
|
1557
|
+
return {
|
|
1558
|
+
status: "ready",
|
|
1559
|
+
isAuthorized: hasActiveAccount,
|
|
1560
|
+
isConnected: hasActiveAccount,
|
|
1561
|
+
isUnlocked: true,
|
|
1562
|
+
hasPasskey: hasActiveAccount,
|
|
1563
|
+
hasWalletAccount: hasActiveAccount,
|
|
1564
|
+
accounts: active.accounts,
|
|
1565
|
+
selectedAccount: active.selectedAccount,
|
|
1566
|
+
metadata: result.metadata ?? null,
|
|
1567
|
+
error: null
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
function walletAvailabilityFromConnectionState(state) {
|
|
1571
|
+
const active = normalizeConnectionStateResult(state);
|
|
1572
|
+
const hasWalletAccount = state.hasWalletAccount ?? state.accounts.length > 0;
|
|
1573
|
+
return {
|
|
1574
|
+
status: "ready",
|
|
1575
|
+
isAuthorized: state.isAuthorized,
|
|
1576
|
+
isConnected: state.isAuthorized && state.isConnected,
|
|
1577
|
+
isUnlocked: state.isUnlocked,
|
|
1578
|
+
hasPasskey: state.hasPasskey,
|
|
1579
|
+
hasWalletAccount,
|
|
1580
|
+
accounts: active.accounts,
|
|
1581
|
+
selectedAccount: active.selectedAccount,
|
|
1582
|
+
metadata: state.isAuthorized ? state.metadata : null,
|
|
1583
|
+
error: null
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
function walletAvailabilityFromError(error) {
|
|
1587
|
+
return {
|
|
1588
|
+
status: "error",
|
|
1589
|
+
isAuthorized: false,
|
|
1590
|
+
isConnected: false,
|
|
1591
|
+
isUnlocked: false,
|
|
1592
|
+
hasPasskey: false,
|
|
1593
|
+
hasWalletAccount: false,
|
|
1594
|
+
accounts: [],
|
|
1595
|
+
selectedAccount: null,
|
|
1596
|
+
metadata: null,
|
|
1597
|
+
error: error instanceof Error ? error : new Error("Wallet availability check failed")
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function isUserRejectedError(error) {
|
|
1601
|
+
if (!error || typeof error !== "object") return false;
|
|
1602
|
+
return error.code === ErrorCode.USER_REJECTED;
|
|
1603
|
+
}
|
|
1604
|
+
var CHECKING_WALLET_AVAILABILITY2 = {
|
|
1605
|
+
status: "checking",
|
|
1606
|
+
isAuthorized: false,
|
|
1607
|
+
isConnected: false,
|
|
1608
|
+
isUnlocked: false,
|
|
1609
|
+
hasPasskey: false,
|
|
1610
|
+
hasWalletAccount: false,
|
|
1611
|
+
accounts: [],
|
|
1612
|
+
selectedAccount: null,
|
|
1613
|
+
metadata: null,
|
|
1614
|
+
error: null
|
|
1615
|
+
};
|
|
1616
|
+
var ThruContext = createContext(null);
|
|
1617
|
+
function ThruProvider({ children, config }) {
|
|
1618
|
+
const [sdk, setSdk] = useState(null);
|
|
1619
|
+
const [thru, setThru] = useState(null);
|
|
1620
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
1621
|
+
const [accounts, setAccounts] = useState([]);
|
|
1622
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
1623
|
+
const [error, setError] = useState(null);
|
|
1624
|
+
const [selectedAccount, setSelectedAccount] = useState(
|
|
1625
|
+
null
|
|
1626
|
+
);
|
|
1627
|
+
const [walletAvailability, setWalletAvailability] = useState(CHECKING_WALLET_AVAILABILITY2);
|
|
1628
|
+
useEffect(() => {
|
|
1629
|
+
const sdkInstance = new NativeSDK(config);
|
|
1630
|
+
setSdk(sdkInstance);
|
|
1631
|
+
setThru(sdkInstance.getThru());
|
|
1632
|
+
const updateAccountsFromSdk = () => setAccounts(sdkInstance.getAccounts());
|
|
1633
|
+
const updateSelectedAccount = (account) => {
|
|
1634
|
+
if (account) {
|
|
1635
|
+
setSelectedAccount(account);
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const fallback = sdkInstance.getSelectedAccount() ?? sdkInstance.getAccounts()[0] ?? null;
|
|
1639
|
+
setSelectedAccount(fallback);
|
|
1640
|
+
};
|
|
1641
|
+
const handleConnect = (result) => {
|
|
1642
|
+
if (result?.status === "connecting") {
|
|
1643
|
+
setIsConnecting(true);
|
|
1644
|
+
setError(null);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
setIsConnected(true);
|
|
1648
|
+
updateAccountsFromSdk();
|
|
1649
|
+
setIsConnecting(false);
|
|
1650
|
+
setError(null);
|
|
1651
|
+
setWalletAvailability(sdkInstance.getWalletAvailability());
|
|
1652
|
+
updateSelectedAccount();
|
|
1653
|
+
};
|
|
1654
|
+
const resetData = () => {
|
|
1655
|
+
setIsConnected(false);
|
|
1656
|
+
setAccounts([]);
|
|
1657
|
+
setIsConnecting(false);
|
|
1658
|
+
setSelectedAccount(null);
|
|
1659
|
+
};
|
|
1660
|
+
const handleDisconnect = () => resetData();
|
|
1661
|
+
const handleError = (err) => {
|
|
1662
|
+
setError(err?.error ?? err ?? new Error("Unknown error"));
|
|
1663
|
+
setIsConnecting(false);
|
|
1664
|
+
setWalletAvailability(sdkInstance.getWalletAvailability());
|
|
1665
|
+
};
|
|
1666
|
+
const handleLock = () => resetData();
|
|
1667
|
+
const handleAccountChanged = (account) => {
|
|
1668
|
+
updateAccountsFromSdk();
|
|
1669
|
+
updateSelectedAccount(account ?? void 0);
|
|
1670
|
+
};
|
|
1671
|
+
const handleAvailabilityChanged = (availability) => {
|
|
1672
|
+
setWalletAvailability(availability);
|
|
1673
|
+
};
|
|
1674
|
+
sdkInstance.on("connect", handleConnect);
|
|
1675
|
+
sdkInstance.on("disconnect", handleDisconnect);
|
|
1676
|
+
sdkInstance.on("error", handleError);
|
|
1677
|
+
sdkInstance.on("lock", handleLock);
|
|
1678
|
+
sdkInstance.on("accountChanged", handleAccountChanged);
|
|
1679
|
+
sdkInstance.on("availabilityChanged", handleAvailabilityChanged);
|
|
1680
|
+
void sdkInstance.restoreConnection({ hydrate: false }).catch(handleError);
|
|
1681
|
+
return () => {
|
|
1682
|
+
sdkInstance.off("connect", handleConnect);
|
|
1683
|
+
sdkInstance.off("disconnect", handleDisconnect);
|
|
1684
|
+
sdkInstance.off("error", handleError);
|
|
1685
|
+
sdkInstance.off("lock", handleLock);
|
|
1686
|
+
sdkInstance.off("accountChanged", handleAccountChanged);
|
|
1687
|
+
sdkInstance.off("availabilityChanged", handleAvailabilityChanged);
|
|
1688
|
+
sdkInstance.destroy();
|
|
1689
|
+
};
|
|
1690
|
+
}, []);
|
|
1691
|
+
const selectAccount = useCallback(
|
|
1692
|
+
async (account) => {
|
|
1693
|
+
if (!sdk) throw new Error("NativeSDK not initialized");
|
|
1694
|
+
try {
|
|
1695
|
+
const updated = await sdk.selectAccount(account.address);
|
|
1696
|
+
setSelectedAccount(updated);
|
|
1697
|
+
setAccounts([updated]);
|
|
1698
|
+
} catch (err) {
|
|
1699
|
+
setError(
|
|
1700
|
+
err instanceof Error ? err : new Error("selectAccount failed")
|
|
1701
|
+
);
|
|
1702
|
+
throw err;
|
|
1703
|
+
}
|
|
1704
|
+
},
|
|
1705
|
+
[sdk]
|
|
1706
|
+
);
|
|
1707
|
+
const manageAccounts = useCallback(async () => {
|
|
1708
|
+
if (!sdk) throw new Error("NativeSDK not initialized");
|
|
1709
|
+
try {
|
|
1710
|
+
const result = await sdk.manageAccounts();
|
|
1711
|
+
setSelectedAccount(result.selectedAccount);
|
|
1712
|
+
setAccounts(result.accounts);
|
|
1713
|
+
return result;
|
|
1714
|
+
} catch (err) {
|
|
1715
|
+
setError(err instanceof Error ? err : new Error("manageAccounts failed"));
|
|
1716
|
+
throw err;
|
|
1717
|
+
}
|
|
1718
|
+
}, [sdk]);
|
|
1719
|
+
const createAccount = useCallback(
|
|
1720
|
+
async (options) => {
|
|
1721
|
+
if (!sdk) throw new Error("NativeSDK not initialized");
|
|
1722
|
+
try {
|
|
1723
|
+
const result = await sdk.createAccount(options);
|
|
1724
|
+
setSelectedAccount(result.selectedAccount);
|
|
1725
|
+
setAccounts(result.accounts);
|
|
1726
|
+
setIsConnected(true);
|
|
1727
|
+
setIsConnecting(false);
|
|
1728
|
+
setWalletAvailability(sdk.getWalletAvailability());
|
|
1729
|
+
return result;
|
|
1730
|
+
} catch (err) {
|
|
1731
|
+
setError(
|
|
1732
|
+
err instanceof Error ? err : new Error("createAccount failed")
|
|
1733
|
+
);
|
|
1734
|
+
setIsConnecting(false);
|
|
1735
|
+
throw err;
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1738
|
+
[sdk]
|
|
1739
|
+
);
|
|
1740
|
+
return /* @__PURE__ */ jsx(
|
|
1741
|
+
ThruContext.Provider,
|
|
1742
|
+
{
|
|
1743
|
+
value: {
|
|
1744
|
+
thru,
|
|
1745
|
+
wallet: sdk,
|
|
1746
|
+
isConnected,
|
|
1747
|
+
accounts,
|
|
1748
|
+
isConnecting,
|
|
1749
|
+
error,
|
|
1750
|
+
selectedAccount,
|
|
1751
|
+
walletAvailability,
|
|
1752
|
+
selectAccount,
|
|
1753
|
+
createAccount,
|
|
1754
|
+
manageAccounts
|
|
1755
|
+
},
|
|
1756
|
+
children
|
|
1757
|
+
}
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// src/native/provider/shell.ts
|
|
1762
|
+
var SHELL_HTML_TEMPLATE = String.raw`<!doctype html>
|
|
1763
|
+
<html lang="en">
|
|
1764
|
+
<head>
|
|
1765
|
+
<meta charset="utf-8" />
|
|
1766
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
1767
|
+
<title>thru-shell</title>
|
|
1768
|
+
<style>
|
|
1769
|
+
html, body, iframe {
|
|
1770
|
+
margin: 0;
|
|
1771
|
+
padding: 0;
|
|
1772
|
+
width: 100%;
|
|
1773
|
+
height: 100%;
|
|
1774
|
+
border: 0;
|
|
1775
|
+
background: transparent;
|
|
1776
|
+
}
|
|
1777
|
+
</style>
|
|
1778
|
+
</head>
|
|
1779
|
+
<body>
|
|
1780
|
+
<iframe
|
|
1781
|
+
id="w"
|
|
1782
|
+
data-src="WALLET_URL_PLACEHOLDER"
|
|
1783
|
+
allow="publickey-credentials-get *; publickey-credentials-create *"
|
|
1784
|
+
></iframe>
|
|
1785
|
+
<script>
|
|
1786
|
+
(function () {
|
|
1787
|
+
var f = document.getElementById('w');
|
|
1788
|
+
var ORIGIN = 'WALLET_ORIGIN_PLACEHOLDER';
|
|
1789
|
+
function frameId() {
|
|
1790
|
+
try {
|
|
1791
|
+
return new URL(f.dataset.src).searchParams.get('tn_frame_id');
|
|
1792
|
+
} catch (err) {
|
|
1793
|
+
return null;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function postShell(type, data) {
|
|
1797
|
+
var rn = window.ReactNativeWebView;
|
|
1798
|
+
if (!rn || !rn.postMessage) return;
|
|
1799
|
+
try {
|
|
1800
|
+
rn.postMessage(JSON.stringify({
|
|
1801
|
+
type: type,
|
|
1802
|
+
frameId: frameId(),
|
|
1803
|
+
data: data || {}
|
|
1804
|
+
}));
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
/* drop unserializable messages */
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function postToWallet(msg) {
|
|
1810
|
+
if (!f.contentWindow) return;
|
|
1811
|
+
var outbound = msg;
|
|
1812
|
+
if (msg && typeof msg === 'object') {
|
|
1813
|
+
outbound = Object.assign({}, msg, { frameId: frameId() });
|
|
1814
|
+
}
|
|
1815
|
+
f.contentWindow.postMessage(outbound, ORIGIN);
|
|
1816
|
+
}
|
|
1817
|
+
window.addEventListener('message', function (e) {
|
|
1818
|
+
var fromFrame = e.source === f.contentWindow;
|
|
1819
|
+
var fromWalletOrigin = e.origin === ORIGIN;
|
|
1820
|
+
var hasFrameId = e.data && e.data.frameId === frameId();
|
|
1821
|
+
if (!fromWalletOrigin || (!fromFrame && !hasFrameId)) return;
|
|
1822
|
+
var rn = window.ReactNativeWebView;
|
|
1823
|
+
if (rn && rn.postMessage) {
|
|
1824
|
+
try {
|
|
1825
|
+
rn.postMessage(JSON.stringify(e.data));
|
|
1826
|
+
} catch (err) {
|
|
1827
|
+
/* drop unserializable messages */
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
window.__pushIn = postToWallet;
|
|
1832
|
+
window.addEventListener('thru:native-sheet-dismiss', function () {
|
|
1833
|
+
postToWallet({
|
|
1834
|
+
type: 'thru:native-sheet-dismiss',
|
|
1835
|
+
frameId: frameId()
|
|
1836
|
+
});
|
|
1837
|
+
});
|
|
1838
|
+
f.addEventListener('load', function () {
|
|
1839
|
+
postShell('shell:iframe-load', { src: f.src });
|
|
1840
|
+
});
|
|
1841
|
+
f.addEventListener('error', function () {
|
|
1842
|
+
postShell('shell:iframe-error', { src: f.src });
|
|
1843
|
+
});
|
|
1844
|
+
postShell('shell:loading', { src: f.dataset.src });
|
|
1845
|
+
f.src = f.dataset.src;
|
|
1846
|
+
})();
|
|
1847
|
+
</script>
|
|
1848
|
+
</body>
|
|
1849
|
+
</html>`;
|
|
1850
|
+
var SHELL_PLACEHOLDER_PATTERN = /WALLET_URL_PLACEHOLDER|WALLET_ORIGIN_PLACEHOLDER/g;
|
|
1851
|
+
function getShellHtml(opts) {
|
|
1852
|
+
return SHELL_HTML_TEMPLATE.replace(
|
|
1853
|
+
SHELL_PLACEHOLDER_PATTERN,
|
|
1854
|
+
(placeholder) => placeholder === "WALLET_URL_PLACEHOLDER" ? opts.walletUrl : opts.walletOrigin
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
var Native = requireOptionalNativeModule(
|
|
1858
|
+
"ThruWebViewBridge"
|
|
1859
|
+
);
|
|
1860
|
+
async function enableWebAuthnSupport(viewTag) {
|
|
1861
|
+
if (Platform.OS !== "android") return false;
|
|
1862
|
+
if (!Native) return false;
|
|
1863
|
+
try {
|
|
1864
|
+
return await Native.enableWebAuthnSupport(viewTag);
|
|
1865
|
+
} catch (err) {
|
|
1866
|
+
console.warn("[@thru/wallet/native/react] enableWebAuthnSupport failed:", err);
|
|
1867
|
+
return false;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
function ThruTransparentWalletBridge({
|
|
1871
|
+
wallet: walletProp,
|
|
1872
|
+
style,
|
|
1873
|
+
webViewProps
|
|
1874
|
+
}) {
|
|
1875
|
+
const thruContext = useContext(ThruContext);
|
|
1876
|
+
const wallet = walletProp ?? thruContext?.wallet ?? null;
|
|
1877
|
+
const webViewRef = useRef(null);
|
|
1878
|
+
const webViewNativeTagRef = useRef(null);
|
|
1879
|
+
const didRefreshWalletAvailabilityRef = useRef(false);
|
|
1880
|
+
const [isFocusSurfaceActive, setIsFocusSurfaceActive] = useState(false);
|
|
1881
|
+
const attachIfReady = useCallback(() => {
|
|
1882
|
+
if (!wallet || !webViewRef.current) return;
|
|
1883
|
+
const ref = {
|
|
1884
|
+
injectJavaScript: (script) => {
|
|
1885
|
+
webViewRef.current?.injectJavaScript(script);
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
wallet.attachWebView(ref);
|
|
1889
|
+
}, [wallet]);
|
|
1890
|
+
const enableAndroidWebAuthnIfNeeded = useCallback(async () => {
|
|
1891
|
+
if (Platform.OS !== "android") return false;
|
|
1892
|
+
const enabled = await enableWebAuthnSupport(webViewNativeTagRef.current);
|
|
1893
|
+
webViewRef.current?.injectJavaScript(
|
|
1894
|
+
"window.dispatchEvent(new Event('thru:native-webauthn-ready')); true;"
|
|
1895
|
+
);
|
|
1896
|
+
return enabled;
|
|
1897
|
+
}, []);
|
|
1898
|
+
const focusWebViewDocument = useCallback(() => {
|
|
1899
|
+
const webView = webViewRef.current;
|
|
1900
|
+
webView?.requestFocus?.();
|
|
1901
|
+
webViewRef.current?.injectJavaScript(
|
|
1902
|
+
"try { window.focus(); document.body && document.body.focus && document.body.focus(); } catch (_) {} true;"
|
|
1903
|
+
);
|
|
1904
|
+
}, []);
|
|
1905
|
+
const refreshWalletAvailabilityIfReady = useCallback(() => {
|
|
1906
|
+
if (!wallet || didRefreshWalletAvailabilityRef.current) return;
|
|
1907
|
+
didRefreshWalletAvailabilityRef.current = true;
|
|
1908
|
+
void wallet.refreshWalletAvailability();
|
|
1909
|
+
}, [wallet]);
|
|
1910
|
+
useEffect(() => {
|
|
1911
|
+
if (!wallet) return;
|
|
1912
|
+
wallet.setUiHandlers({
|
|
1913
|
+
onShowRequested: () => {
|
|
1914
|
+
setIsFocusSurfaceActive(true);
|
|
1915
|
+
},
|
|
1916
|
+
onHideRequested: () => {
|
|
1917
|
+
setIsFocusSurfaceActive(false);
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
return () => {
|
|
1921
|
+
wallet.clearUiHandlers();
|
|
1922
|
+
};
|
|
1923
|
+
}, [focusWebViewDocument, wallet]);
|
|
1924
|
+
useEffect(() => {
|
|
1925
|
+
if (!isFocusSurfaceActive) return;
|
|
1926
|
+
const timers = [0, 50, 120, 250, 500].map(
|
|
1927
|
+
(delay) => setTimeout(focusWebViewDocument, delay)
|
|
1928
|
+
);
|
|
1929
|
+
return () => {
|
|
1930
|
+
timers.forEach(clearTimeout);
|
|
1931
|
+
};
|
|
1932
|
+
}, [focusWebViewDocument, isFocusSurfaceActive]);
|
|
1933
|
+
const webViewSource = useMemo(() => {
|
|
1934
|
+
if (!wallet) return null;
|
|
1935
|
+
if (Platform.OS === "ios" && wallet.getIosWebViewMode() === "direct") {
|
|
1936
|
+
return { uri: wallet.getIframeSrc() };
|
|
1937
|
+
}
|
|
1938
|
+
return {
|
|
1939
|
+
html: getShellHtml({
|
|
1940
|
+
walletUrl: wallet.getIframeSrc(),
|
|
1941
|
+
walletOrigin: wallet.getWalletOrigin()
|
|
1942
|
+
}),
|
|
1943
|
+
baseUrl: wallet.getWalletOrigin()
|
|
1944
|
+
};
|
|
1945
|
+
}, [wallet]);
|
|
1946
|
+
const isDirectWalletSource = Boolean(
|
|
1947
|
+
wallet && Platform.OS === "ios" && wallet.getIosWebViewMode() === "direct"
|
|
1948
|
+
);
|
|
1949
|
+
useEffect(() => {
|
|
1950
|
+
didRefreshWalletAvailabilityRef.current = false;
|
|
1951
|
+
}, [webViewSource]);
|
|
1952
|
+
const handleWebViewLayout = useCallback(
|
|
1953
|
+
(event) => {
|
|
1954
|
+
const target = event.nativeEvent.target;
|
|
1955
|
+
webViewNativeTagRef.current = typeof target === "number" ? target : webViewNativeTagRef.current;
|
|
1956
|
+
void enableAndroidWebAuthnIfNeeded();
|
|
1957
|
+
webViewProps?.onLayout?.(event);
|
|
1958
|
+
},
|
|
1959
|
+
[enableAndroidWebAuthnIfNeeded, webViewProps]
|
|
1960
|
+
);
|
|
1961
|
+
const handleLoadEnd = useCallback(
|
|
1962
|
+
(event) => {
|
|
1963
|
+
attachIfReady();
|
|
1964
|
+
if (isDirectWalletSource) {
|
|
1965
|
+
void enableAndroidWebAuthnIfNeeded().finally(
|
|
1966
|
+
refreshWalletAvailabilityIfReady
|
|
1967
|
+
);
|
|
1968
|
+
} else {
|
|
1969
|
+
void enableAndroidWebAuthnIfNeeded();
|
|
1970
|
+
}
|
|
1971
|
+
webViewProps?.onLoadEnd?.(event);
|
|
1972
|
+
},
|
|
1973
|
+
[
|
|
1974
|
+
attachIfReady,
|
|
1975
|
+
enableAndroidWebAuthnIfNeeded,
|
|
1976
|
+
isDirectWalletSource,
|
|
1977
|
+
refreshWalletAvailabilityIfReady,
|
|
1978
|
+
webViewProps
|
|
1979
|
+
]
|
|
1980
|
+
);
|
|
1981
|
+
const handleMessage = useCallback(
|
|
1982
|
+
(event) => {
|
|
1983
|
+
let shouldRefreshAfterBridgeReady = false;
|
|
1984
|
+
let shouldCollapseFocusSurface = false;
|
|
1985
|
+
try {
|
|
1986
|
+
const data = JSON.parse(event.nativeEvent.data);
|
|
1987
|
+
shouldRefreshAfterBridgeReady = data.type === "iframe:ready";
|
|
1988
|
+
shouldCollapseFocusSurface = typeof data.id === "string" && typeof data.success === "boolean";
|
|
1989
|
+
} catch {
|
|
1990
|
+
}
|
|
1991
|
+
if (shouldCollapseFocusSurface) {
|
|
1992
|
+
setIsFocusSurfaceActive(false);
|
|
1993
|
+
}
|
|
1994
|
+
wallet?.onMessage({
|
|
1995
|
+
nativeEvent: { data: event.nativeEvent.data }
|
|
1996
|
+
});
|
|
1997
|
+
webViewProps?.onMessage?.(event);
|
|
1998
|
+
if (shouldRefreshAfterBridgeReady) {
|
|
1999
|
+
void enableAndroidWebAuthnIfNeeded().finally(
|
|
2000
|
+
refreshWalletAvailabilityIfReady
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
},
|
|
2004
|
+
[
|
|
2005
|
+
enableAndroidWebAuthnIfNeeded,
|
|
2006
|
+
refreshWalletAvailabilityIfReady,
|
|
2007
|
+
wallet,
|
|
2008
|
+
webViewProps
|
|
2009
|
+
]
|
|
2010
|
+
);
|
|
2011
|
+
if (!webViewSource) return null;
|
|
2012
|
+
return /* @__PURE__ */ jsx(
|
|
2013
|
+
View,
|
|
2014
|
+
{
|
|
2015
|
+
collapsable: false,
|
|
2016
|
+
pointerEvents: isFocusSurfaceActive ? "auto" : "none",
|
|
2017
|
+
style: [
|
|
2018
|
+
styles.container,
|
|
2019
|
+
isFocusSurfaceActive ? styles.activeContainer : null,
|
|
2020
|
+
style
|
|
2021
|
+
],
|
|
2022
|
+
children: /* @__PURE__ */ jsx(
|
|
2023
|
+
WebView,
|
|
2024
|
+
{
|
|
2025
|
+
...webViewProps,
|
|
2026
|
+
ref: webViewRef,
|
|
2027
|
+
source: webViewSource,
|
|
2028
|
+
originWhitelist: ["*"],
|
|
2029
|
+
javaScriptEnabled: true,
|
|
2030
|
+
domStorageEnabled: true,
|
|
2031
|
+
webviewDebuggingEnabled: __DEV__,
|
|
2032
|
+
sharedCookiesEnabled: true,
|
|
2033
|
+
allowsInlineMediaPlayback: true,
|
|
2034
|
+
mediaPlaybackRequiresUserAction: false,
|
|
2035
|
+
limitsNavigationsToAppBoundDomains: isDirectWalletSource,
|
|
2036
|
+
onLoadStart: (event) => {
|
|
2037
|
+
attachIfReady();
|
|
2038
|
+
void enableAndroidWebAuthnIfNeeded();
|
|
2039
|
+
webViewProps?.onLoadStart?.(event);
|
|
2040
|
+
},
|
|
2041
|
+
onLoadEnd: handleLoadEnd,
|
|
2042
|
+
onLayout: handleWebViewLayout,
|
|
2043
|
+
onMessage: handleMessage,
|
|
2044
|
+
style: [
|
|
2045
|
+
styles.webview,
|
|
2046
|
+
isFocusSurfaceActive ? styles.activeWebview : null,
|
|
2047
|
+
webViewProps?.style
|
|
2048
|
+
]
|
|
2049
|
+
}
|
|
2050
|
+
)
|
|
2051
|
+
}
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
var styles = StyleSheet.create({
|
|
2055
|
+
container: {
|
|
2056
|
+
height: 1,
|
|
2057
|
+
left: 0,
|
|
2058
|
+
opacity: 0,
|
|
2059
|
+
overflow: "hidden",
|
|
2060
|
+
position: "absolute",
|
|
2061
|
+
top: 0,
|
|
2062
|
+
width: 1
|
|
2063
|
+
},
|
|
2064
|
+
activeContainer: {
|
|
2065
|
+
bottom: 0,
|
|
2066
|
+
height: "100%",
|
|
2067
|
+
opacity: 1,
|
|
2068
|
+
right: 0,
|
|
2069
|
+
width: "100%",
|
|
2070
|
+
zIndex: 2147483647
|
|
2071
|
+
},
|
|
2072
|
+
webview: {
|
|
2073
|
+
backgroundColor: "transparent",
|
|
2074
|
+
height: 1,
|
|
2075
|
+
width: 1
|
|
2076
|
+
},
|
|
2077
|
+
activeWebview: {
|
|
2078
|
+
flex: 1,
|
|
2079
|
+
height: "100%",
|
|
2080
|
+
width: "100%"
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
function useThru() {
|
|
2084
|
+
const ctx = useContext(ThruContext);
|
|
2085
|
+
if (!ctx) {
|
|
2086
|
+
throw new Error("useThru must be used inside <ThruProvider>");
|
|
2087
|
+
}
|
|
2088
|
+
return ctx;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// src/native/react/hooks/waitForWallet.ts
|
|
2092
|
+
function waitForWallet(getWallet, timeout = 5e3, interval = 100) {
|
|
2093
|
+
return new Promise((resolve, reject) => {
|
|
2094
|
+
const start = Date.now();
|
|
2095
|
+
const check = () => {
|
|
2096
|
+
const sdk = getWallet();
|
|
2097
|
+
if (sdk) return resolve(sdk);
|
|
2098
|
+
if (Date.now() - start > timeout) {
|
|
2099
|
+
return reject(new Error("NativeSDK not initialized in time"));
|
|
2100
|
+
}
|
|
2101
|
+
setTimeout(check, interval);
|
|
2102
|
+
};
|
|
2103
|
+
check();
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// src/native/react/hooks/useWallet.ts
|
|
2108
|
+
function useWallet() {
|
|
2109
|
+
const {
|
|
2110
|
+
wallet,
|
|
2111
|
+
isConnected,
|
|
2112
|
+
isConnecting,
|
|
2113
|
+
accounts,
|
|
2114
|
+
selectedAccount,
|
|
2115
|
+
selectAccount,
|
|
2116
|
+
manageAccounts,
|
|
2117
|
+
walletAvailability
|
|
2118
|
+
} = useThru();
|
|
2119
|
+
const walletRef = useRef(wallet);
|
|
2120
|
+
useEffect(() => {
|
|
2121
|
+
walletRef.current = wallet;
|
|
2122
|
+
}, [wallet]);
|
|
2123
|
+
const connect = useCallback(async (options) => {
|
|
2124
|
+
const ready = walletRef.current ?? await waitForWallet(() => walletRef.current);
|
|
2125
|
+
return ready.connect(options);
|
|
2126
|
+
}, []);
|
|
2127
|
+
const signIn = useCallback(async (options) => {
|
|
2128
|
+
const ready = walletRef.current ?? await waitForWallet(() => walletRef.current);
|
|
2129
|
+
return ready.signIn(options);
|
|
2130
|
+
}, []);
|
|
2131
|
+
const createTransparentAccount = useCallback(
|
|
2132
|
+
async (options) => {
|
|
2133
|
+
const ready = walletRef.current ?? await waitForWallet(() => walletRef.current);
|
|
2134
|
+
return ready.createAccount(options);
|
|
2135
|
+
},
|
|
2136
|
+
[]
|
|
2137
|
+
);
|
|
2138
|
+
const disconnect = useCallback(async () => {
|
|
2139
|
+
const ready = walletRef.current ?? await waitForWallet(() => walletRef.current);
|
|
2140
|
+
await ready.disconnect();
|
|
2141
|
+
}, []);
|
|
2142
|
+
const refreshWalletAvailability = useCallback(async (options) => {
|
|
2143
|
+
const ready = walletRef.current ?? await waitForWallet(() => walletRef.current);
|
|
2144
|
+
return ready.refreshWalletAvailability(options);
|
|
2145
|
+
}, []);
|
|
2146
|
+
return {
|
|
2147
|
+
/** Chain interface (`provider.thru`); undefined until connected. */
|
|
2148
|
+
wallet: wallet?.thru,
|
|
2149
|
+
accounts,
|
|
2150
|
+
connect,
|
|
2151
|
+
signIn,
|
|
2152
|
+
createAccount: createTransparentAccount,
|
|
2153
|
+
disconnect,
|
|
2154
|
+
isConnected: isConnected && !!wallet,
|
|
2155
|
+
isConnecting,
|
|
2156
|
+
selectedAccount,
|
|
2157
|
+
selectAccount,
|
|
2158
|
+
manageAccounts,
|
|
2159
|
+
walletAvailability,
|
|
2160
|
+
hasPasskey: walletAvailability.hasPasskey,
|
|
2161
|
+
hasWalletAccount: walletAvailability.hasWalletAccount,
|
|
2162
|
+
isWalletAvailabilityLoading: walletAvailability.status === "checking",
|
|
2163
|
+
refreshWalletAvailability
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
function useWalletAvailability() {
|
|
2167
|
+
const { wallet, walletAvailability } = useThru();
|
|
2168
|
+
const walletRef = useRef(wallet);
|
|
2169
|
+
useEffect(() => {
|
|
2170
|
+
walletRef.current = wallet;
|
|
2171
|
+
}, [wallet]);
|
|
2172
|
+
const refreshWalletAvailability = async (options) => {
|
|
2173
|
+
const ready = walletRef.current ?? await waitForWallet(() => walletRef.current);
|
|
2174
|
+
return ready.refreshWalletAvailability(options);
|
|
2175
|
+
};
|
|
2176
|
+
return {
|
|
2177
|
+
walletAvailability,
|
|
2178
|
+
refreshWalletAvailability,
|
|
2179
|
+
hasPasskey: walletAvailability.hasPasskey,
|
|
2180
|
+
hasWalletAccount: walletAvailability.hasWalletAccount,
|
|
2181
|
+
isAuthorized: walletAvailability.isAuthorized,
|
|
2182
|
+
isWalletAvailabilityLoading: walletAvailability.status === "checking",
|
|
2183
|
+
accounts: walletAvailability.accounts,
|
|
2184
|
+
selectedAccount: walletAvailability.selectedAccount,
|
|
2185
|
+
error: walletAvailability.error
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
function useAccounts({ onAccountSelect } = {}) {
|
|
2189
|
+
const { accounts, selectedAccount, isConnected, isConnecting } = useThru();
|
|
2190
|
+
const lastSeen = useRef(null);
|
|
2191
|
+
useEffect(() => {
|
|
2192
|
+
if (!selectedAccount) {
|
|
2193
|
+
lastSeen.current = null;
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
if (lastSeen.current === selectedAccount.address) return;
|
|
2197
|
+
lastSeen.current = selectedAccount.address;
|
|
2198
|
+
onAccountSelect?.(selectedAccount);
|
|
2199
|
+
}, [selectedAccount, onAccountSelect]);
|
|
2200
|
+
return {
|
|
2201
|
+
accounts,
|
|
2202
|
+
selectedAccount,
|
|
2203
|
+
isConnected,
|
|
2204
|
+
isConnecting
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
export { ThruContext, ThruProvider, ThruTransparentWalletBridge, enableWebAuthnSupport, useAccounts, useThru, useWallet, useWalletAvailability };
|
|
2209
|
+
//# sourceMappingURL=transparent.js.map
|
|
2210
|
+
//# sourceMappingURL=transparent.js.map
|