@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.
Files changed (69) hide show
  1. package/README.md +67 -0
  2. package/android/build.gradle +37 -0
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/org/thru/walletnative/ThruWebViewBridgeModule.kt +77 -0
  5. package/app.plugin.cjs +101 -0
  6. package/dist/BrowserSDK-CpRFiJsW.d.ts +409 -0
  7. package/dist/index.d.ts +23 -0
  8. package/dist/index.js +941 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/native/react.d.ts +109 -0
  11. package/dist/native/react.js +2381 -0
  12. package/dist/native/react.js.map +1 -0
  13. package/dist/native.d.ts +329 -0
  14. package/dist/native.js +1126 -0
  15. package/dist/native.js.map +1 -0
  16. package/dist/react-ui.d.ts +5 -0
  17. package/dist/react-ui.js +266 -0
  18. package/dist/react-ui.js.map +1 -0
  19. package/dist/react.d.ts +66 -0
  20. package/dist/react.js +1151 -0
  21. package/dist/react.js.map +1 -0
  22. package/expo-module.config.json +6 -0
  23. package/package.json +114 -0
  24. package/src/BrowserSDK.ts +315 -0
  25. package/src/index.ts +27 -0
  26. package/src/interfaces/IThruChain.ts +37 -0
  27. package/src/interfaces/accounts.ts +61 -0
  28. package/src/interfaces/index.ts +9 -0
  29. package/src/interfaces/types.ts +95 -0
  30. package/src/native/NativeSDK.test.ts +819 -0
  31. package/src/native/NativeSDK.ts +773 -0
  32. package/src/native/index.ts +39 -0
  33. package/src/native/provider/NativeProvider.ts +363 -0
  34. package/src/native/provider/WebViewBridge.test.ts +339 -0
  35. package/src/native/provider/WebViewBridge.ts +339 -0
  36. package/src/native/provider/chains/ThruChain.ts +85 -0
  37. package/src/native/provider/shell.html +88 -0
  38. package/src/native/provider/shell.test.ts +56 -0
  39. package/src/native/provider/shell.ts +111 -0
  40. package/src/native/provider/shims-html.d.ts +4 -0
  41. package/src/native/react/ThruContext.ts +37 -0
  42. package/src/native/react/ThruProvider.tsx +168 -0
  43. package/src/native/react/ThruWalletSheet.tsx +1162 -0
  44. package/src/native/react/android-webauthn.ts +37 -0
  45. package/src/native/react/hooks/useAccounts.ts +35 -0
  46. package/src/native/react/hooks/useThru.ts +11 -0
  47. package/src/native/react/hooks/useWallet.ts +71 -0
  48. package/src/native/react/hooks/useWalletAvailability.ts +31 -0
  49. package/src/native/react/hooks/waitForWallet.ts +21 -0
  50. package/src/native/react/index.ts +29 -0
  51. package/src/protocol/index.ts +2 -0
  52. package/src/protocol/postMessage.ts +283 -0
  53. package/src/protocol/walletState.ts +12 -0
  54. package/src/provider/EmbeddedProvider.ts +330 -0
  55. package/src/provider/IframeManager.ts +438 -0
  56. package/src/provider/chains/ThruChain.ts +86 -0
  57. package/src/provider/index.ts +17 -0
  58. package/src/provider/types/messages.ts +37 -0
  59. package/src/react/ThruContext.ts +31 -0
  60. package/src/react/ThruProvider.tsx +169 -0
  61. package/src/react/hooks/useAccounts.ts +38 -0
  62. package/src/react/hooks/useThru.ts +11 -0
  63. package/src/react/hooks/useWallet.ts +81 -0
  64. package/src/react/index.ts +30 -0
  65. package/src/react-ui/ThruAccountSwitcher.tsx +187 -0
  66. package/src/react-ui/custom.d.ts +8 -0
  67. package/src/react-ui/index.ts +1 -0
  68. package/src/static/logo.png +0 -0
  69. 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