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