@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,1162 @@
1
+ /* Bottom-sheet host for the wallet WebView. Auto-opens on UI_SHOW (or
2
+ any provider lifecycle that calls requestShow), auto-closes on
3
+ request resolution / DISCONNECT / LOCK. Mirrors how the iframe's
4
+ IframeManager.show()/hide() couples to UI_SHOW today. */
5
+
6
+ import {
7
+ Component,
8
+ forwardRef,
9
+ useCallback,
10
+ useEffect,
11
+ useImperativeHandle,
12
+ useMemo,
13
+ useRef,
14
+ useState,
15
+ type ComponentType,
16
+ type ReactNode,
17
+ } from "react";
18
+ import {
19
+ Image,
20
+ Platform,
21
+ StyleSheet,
22
+ Text,
23
+ View,
24
+ type LayoutChangeEvent,
25
+ useWindowDimensions,
26
+ } from "react-native";
27
+ import BottomSheet, { BottomSheetBackdrop } from "@gorhom/bottom-sheet";
28
+ import type BottomSheetType from "@gorhom/bottom-sheet";
29
+ import type { BottomSheetBackdropProps } from "@gorhom/bottom-sheet";
30
+ import { useSharedValue } from "react-native-reanimated";
31
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
32
+ import {
33
+ WebView,
34
+ type WebViewMessageEvent,
35
+ type WebView as WebViewType,
36
+ } from "react-native-webview";
37
+ import { getShellHtml } from "../provider/shell";
38
+ import type { WebViewRefLike } from "../provider/WebViewBridge";
39
+ import QRCodeStyledImport from "react-native-qrcode-styled";
40
+ import { useThru } from "./hooks/useThru";
41
+ import { enableWebAuthnSupport } from "./android-webauthn";
42
+
43
+ const DEFAULT_SHEET_BACKGROUND_COLOR = "#f9fbfb";
44
+ const DEFAULT_SNAP_POINTS: (string | number)[] = ["50%", "85%"];
45
+ const DEFAULT_FIT_CONTENT_MAX_SHEET_RATIO = 0.75;
46
+ const DEFAULT_FIT_CONTENT_MIN_SHEET_RATIO = 0;
47
+ const SHEET_HANDLE_HEIGHT = 10;
48
+ const NATIVE_CONTENT_HEIGHT_MESSAGE = "wallet:content-height";
49
+ const NATIVE_SCREEN_BRIGHTNESS_MESSAGE = "wallet:screen-brightness";
50
+ const NATIVE_PAIR_DEVICE_QR_MESSAGE = "wallet:pair-device-qr";
51
+ const NATIVE_PAIR_DEVICE_QR_STATUS_MESSAGE = "wallet:pair-device-qr-status";
52
+ const NATIVE_BOTTOM_INSET_PARAM = "tn_native_bottom_inset";
53
+ const NATIVE_QR_IMPORT_UNAVAILABLE_REASON =
54
+ "react-native-qrcode-styled component unavailable";
55
+ const NATIVE_QR_DARK_COLOR = "#151b1e";
56
+ const NATIVE_QR_ACCENT_COLOR = "#239f97";
57
+ const NATIVE_QR_ACCENT_DARK_COLOR = "#0a766f";
58
+ const NATIVE_QR_GRADIENT = {
59
+ type: "linear" as const,
60
+ options: {
61
+ colors: [
62
+ NATIVE_QR_DARK_COLOR,
63
+ NATIVE_QR_ACCENT_DARK_COLOR,
64
+ NATIVE_QR_ACCENT_COLOR,
65
+ ],
66
+ start: [0, 0] as [number, number],
67
+ end: [1, 1] as [number, number],
68
+ locations: [0, 0.55, 1],
69
+ },
70
+ };
71
+ const NATIVE_QR_OUTER_EYE_OPTIONS = {
72
+ borderRadius: "34%",
73
+ color: NATIVE_QR_DARK_COLOR,
74
+ };
75
+ const NATIVE_QR_INNER_EYE_OPTIONS = {
76
+ borderRadius: "50%",
77
+ color: NATIVE_QR_ACCENT_COLOR,
78
+ scale: 0.86,
79
+ };
80
+ const NATIVE_QR_WARMUP_DATA = "thru:qr-warmup";
81
+
82
+ type BrightnessModule = typeof import("expo-brightness");
83
+ type PreviousScreenBrightnessState = {
84
+ brightness: number;
85
+ didSetSystemBrightness: boolean;
86
+ systemBrightness: number | null;
87
+ systemBrightnessMode: Awaited<
88
+ ReturnType<BrightnessModule["getSystemBrightnessModeAsync"]>
89
+ > | null;
90
+ wasUsingSystemBrightness: boolean | null;
91
+ };
92
+ type PairDeviceQrFrame = {
93
+ top: number;
94
+ left: number;
95
+ width: number;
96
+ height: number;
97
+ };
98
+ type PairDeviceQrState = {
99
+ approveUrl: string;
100
+ frame: PairDeviceQrFrame;
101
+ qrDataUrl?: string;
102
+ };
103
+ type PairDeviceQrRenderStatus = "rendering" | "ready" | "unavailable";
104
+ type QRCodeStyledComponent = ComponentType<{
105
+ data: string;
106
+ size: number;
107
+ padding?: number;
108
+ color?: string;
109
+ gradient?: object;
110
+ pieceScale?: number;
111
+ pieceCornerType?: "rounded" | "cut";
112
+ pieceBorderRadius?: number | `${number}%`;
113
+ pieceLiquidRadius?: number | `${number}%`;
114
+ isPiecesGlued?: boolean;
115
+ outerEyesOptions?: object;
116
+ innerEyesOptions?: object;
117
+ errorCorrectionLevel?: "L" | "M" | "Q" | "H";
118
+ style?: object;
119
+ }>;
120
+
121
+ let brightnessModulePromise: Promise<BrightnessModule | null> | null = null;
122
+
123
+ function getBrightnessModule(): Promise<BrightnessModule | null> {
124
+ brightnessModulePromise ??= import("expo-brightness").catch((error) => {
125
+ console.warn(
126
+ "[ThruWalletSheet] expo-brightness is unavailable in this native build:",
127
+ error,
128
+ );
129
+ return null;
130
+ });
131
+ return brightnessModulePromise;
132
+ }
133
+
134
+ async function getPreviousScreenBrightnessState(
135
+ brightness: BrightnessModule,
136
+ ): Promise<PreviousScreenBrightnessState> {
137
+ const previousState: PreviousScreenBrightnessState = {
138
+ brightness: await brightness.getBrightnessAsync(),
139
+ didSetSystemBrightness: false,
140
+ systemBrightness: null,
141
+ systemBrightnessMode: null,
142
+ wasUsingSystemBrightness: null,
143
+ };
144
+
145
+ if (Platform.OS !== "android") return previousState;
146
+
147
+ const [systemBrightness, systemBrightnessMode, wasUsingSystemBrightness] =
148
+ await Promise.all([
149
+ brightness.getSystemBrightnessAsync().catch(() => null),
150
+ brightness.getSystemBrightnessModeAsync().catch(() => null),
151
+ brightness.isUsingSystemBrightnessAsync().catch(() => null),
152
+ ]);
153
+ previousState.systemBrightness = systemBrightness;
154
+ previousState.systemBrightnessMode = systemBrightnessMode;
155
+ previousState.wasUsingSystemBrightness = wasUsingSystemBrightness;
156
+
157
+ return previousState;
158
+ }
159
+
160
+ function isReactComponentLike(input: unknown): input is ComponentType<unknown> {
161
+ if (typeof input === "function") return true;
162
+ if (typeof input !== "object" || input === null) return false;
163
+
164
+ const reactType = (input as { $$typeof?: unknown }).$$typeof;
165
+ return (
166
+ reactType === Symbol.for("react.forward_ref") ||
167
+ reactType === Symbol.for("react.memo") ||
168
+ reactType === Symbol.for("react.lazy")
169
+ );
170
+ }
171
+
172
+ function resolveQRCodeComponent(
173
+ module: unknown,
174
+ namedExport: string,
175
+ ): ComponentType<unknown> | null {
176
+ const seen = new Set<unknown>();
177
+ const candidates = [module];
178
+
179
+ for (let idx = 0; idx < candidates.length; idx++) {
180
+ const candidate = candidates[idx];
181
+ if (isReactComponentLike(candidate)) return candidate;
182
+ if (
183
+ typeof candidate !== "object" ||
184
+ candidate === null ||
185
+ seen.has(candidate)
186
+ ) {
187
+ continue;
188
+ }
189
+
190
+ seen.add(candidate);
191
+ const record = candidate as Record<string, unknown>;
192
+ candidates.push(record[namedExport], record.default);
193
+ }
194
+
195
+ return null;
196
+ }
197
+
198
+ const RESOLVED_QR_CODE_STYLED = resolveQRCodeComponent(
199
+ QRCodeStyledImport,
200
+ "QRCodeStyled",
201
+ ) as QRCodeStyledComponent | null;
202
+
203
+ function parsePairDeviceQrFrame(input: unknown): PairDeviceQrFrame | null {
204
+ if (!input || typeof input !== "object") return null;
205
+ const frame = input as Partial<PairDeviceQrFrame>;
206
+ const { top, left, width, height } = frame;
207
+ if (
208
+ typeof top !== "number" ||
209
+ typeof left !== "number" ||
210
+ typeof width !== "number" ||
211
+ typeof height !== "number" ||
212
+ !Number.isFinite(top) ||
213
+ !Number.isFinite(left) ||
214
+ !Number.isFinite(width) ||
215
+ !Number.isFinite(height) ||
216
+ width <= 0 ||
217
+ height <= 0
218
+ ) {
219
+ return null;
220
+ }
221
+ return { top, left, width, height };
222
+ }
223
+
224
+ function getPairDeviceQrStatusScript(
225
+ status: PairDeviceQrRenderStatus,
226
+ approveUrl: string,
227
+ reason?: string,
228
+ ): string {
229
+ const message = JSON.stringify({
230
+ type: NATIVE_PAIR_DEVICE_QR_STATUS_MESSAGE,
231
+ data: { status, approveUrl, reason },
232
+ });
233
+
234
+ return `
235
+ (function () {
236
+ var message = ${JSON.stringify(message)};
237
+ try {
238
+ var parsed = JSON.parse(message);
239
+ if (typeof window.__pushIn === 'function') {
240
+ window.__pushIn(parsed);
241
+ } else {
242
+ window.dispatchEvent(new MessageEvent('message', {
243
+ data: parsed,
244
+ origin: window.location.origin
245
+ }));
246
+ }
247
+ } catch (error) {}
248
+ })();
249
+ true;
250
+ `;
251
+ }
252
+
253
+ class OptionalNativeQrBoundary extends Component<
254
+ {
255
+ children: ReactNode;
256
+ onError: (error: unknown) => void;
257
+ },
258
+ { hasError: boolean }
259
+ > {
260
+ state = { hasError: false };
261
+
262
+ static getDerivedStateFromError() {
263
+ return { hasError: true };
264
+ }
265
+
266
+ componentDidCatch(error: unknown) {
267
+ this.props.onError(error);
268
+ }
269
+
270
+ render() {
271
+ if (this.state.hasError) return null;
272
+ return this.props.children;
273
+ }
274
+ }
275
+
276
+ function appendNativeBottomInset(
277
+ urlValue: string,
278
+ bottomInset: number,
279
+ ): string {
280
+ try {
281
+ const url = new URL(urlValue);
282
+ url.searchParams.set(
283
+ NATIVE_BOTTOM_INSET_PARAM,
284
+ String(Math.max(0, Math.ceil(bottomInset))),
285
+ );
286
+ return url.toString();
287
+ } catch {
288
+ return urlValue;
289
+ }
290
+ }
291
+
292
+ export interface ThruWalletSheetProps {
293
+ /** Detents in @gorhom format. Default: ['50%', '85%']. */
294
+ snapPoints?: (string | number)[];
295
+ /** Initial detent index when opening. Default: first detent. */
296
+ initialOpenIndex?: number;
297
+ /** Optional override for the bottom sheet background colour. */
298
+ backgroundColor?: string;
299
+ }
300
+
301
+ export interface ThruWalletSheetHandle {
302
+ /** Imperatively open to a specific snap index. */
303
+ expand: (index?: number) => void;
304
+ /** Imperatively close the sheet. */
305
+ close: () => void;
306
+ }
307
+
308
+ export const ThruWalletSheet = forwardRef<
309
+ ThruWalletSheetHandle,
310
+ ThruWalletSheetProps
311
+ >(function ThruWalletSheet(
312
+ {
313
+ snapPoints,
314
+ initialOpenIndex,
315
+ backgroundColor = DEFAULT_SHEET_BACKGROUND_COLOR,
316
+ },
317
+ ref,
318
+ ) {
319
+ const { wallet } = useThru();
320
+ const { height } = useWindowDimensions();
321
+ const insets = useSafeAreaInsets();
322
+ const sheetRef = useRef<BottomSheetType>(null);
323
+ const webViewRef = useRef<WebViewType | null>(null);
324
+ const webViewNativeTagRef = useRef<number | null>(null);
325
+ const brightnessQueueRef = useRef<Promise<void>>(Promise.resolve());
326
+ const previousScreenBrightnessRef =
327
+ useRef<PreviousScreenBrightnessState | null>(null);
328
+ const didRefreshWalletAvailabilityRef = useRef(false);
329
+ const isProviderClosingRef = useRef(false);
330
+ const isSheetOpenRef = useRef(false);
331
+ const containerLayoutState = useSharedValue({
332
+ height,
333
+ offset: { top: 0, bottom: 0, left: 0, right: 0 },
334
+ });
335
+
336
+ const [shellHtml, setShellHtml] = useState<string | null>(null);
337
+ const [directWalletUrl, setDirectWalletUrl] = useState<string | null>(null);
338
+ const [hasBridgeMessage, setHasBridgeMessage] = useState(false);
339
+ const [walletLoadStatus, setWalletLoadStatus] = useState("Loading wallet...");
340
+ const [webViewError, setWebViewError] = useState<string | null>(null);
341
+ const [webContentHeight, setWebContentHeight] = useState<number | null>(null);
342
+ const [webContentMaxSheetRatio, setWebContentMaxSheetRatio] = useState(
343
+ DEFAULT_FIT_CONTENT_MAX_SHEET_RATIO,
344
+ );
345
+ const [webContentMinSheetRatio, setWebContentMinSheetRatio] = useState(
346
+ DEFAULT_FIT_CONTENT_MIN_SHEET_RATIO,
347
+ );
348
+ const [pairDeviceQr, setPairDeviceQr] = useState<PairDeviceQrState | null>(
349
+ null,
350
+ );
351
+ const [QRCodeStyled, setQRCodeStyled] =
352
+ useState<QRCodeStyledComponent | null>(() => RESOLVED_QR_CODE_STYLED);
353
+ const [isNativeQrUnavailable, setIsNativeQrUnavailable] = useState(
354
+ () => RESOLVED_QR_CODE_STYLED === null,
355
+ );
356
+ const [nativeQrUnavailableReason, setNativeQrUnavailableReason] = useState<
357
+ string | null
358
+ >(() =>
359
+ RESOLVED_QR_CODE_STYLED ? null : NATIVE_QR_IMPORT_UNAVAILABLE_REASON,
360
+ );
361
+ const shouldFitContent = !snapPoints || snapPoints.length === 0;
362
+ const configuredSnapPoints = useMemo(
363
+ () =>
364
+ snapPoints && snapPoints.length > 0 ? snapPoints : DEFAULT_SNAP_POINTS,
365
+ [snapPoints],
366
+ );
367
+ const memoSnapPoints = useMemo(() => {
368
+ if (!shouldFitContent || webContentHeight == null)
369
+ return configuredSnapPoints;
370
+
371
+ const maxSheetHeight = Math.floor(height * webContentMaxSheetRatio);
372
+ const minSheetHeight = Math.floor(
373
+ height * Math.min(webContentMinSheetRatio, webContentMaxSheetRatio),
374
+ );
375
+ const fittedSheetHeight = Math.min(
376
+ Math.max(webContentHeight + SHEET_HANDLE_HEIGHT, minSheetHeight),
377
+ maxSheetHeight,
378
+ );
379
+
380
+ if (fittedSheetHeight >= maxSheetHeight - 1) {
381
+ return [maxSheetHeight];
382
+ }
383
+
384
+ return [fittedSheetHeight, maxSheetHeight];
385
+ }, [
386
+ configuredSnapPoints,
387
+ height,
388
+ shouldFitContent,
389
+ webContentHeight,
390
+ webContentMaxSheetRatio,
391
+ webContentMinSheetRatio,
392
+ ]);
393
+ const openIndex = Math.max(
394
+ 0,
395
+ Math.min(initialOpenIndex ?? 0, memoSnapPoints.length - 1),
396
+ );
397
+ const snapToSheetIndex = useCallback(
398
+ (index: number) => {
399
+ const maxIndex = Math.max(0, memoSnapPoints.length - 1);
400
+ sheetRef.current?.snapToIndex(Math.max(0, Math.min(index, maxIndex)));
401
+ },
402
+ [memoSnapPoints.length],
403
+ );
404
+
405
+ const enqueueBrightnessTask = useCallback((task: () => Promise<void>) => {
406
+ const queuedTask = brightnessQueueRef.current.then(task, task);
407
+ brightnessQueueRef.current = queuedTask.catch(() => {});
408
+ }, []);
409
+
410
+ const restoreScreenBrightness = useCallback(() => {
411
+ enqueueBrightnessTask(async () => {
412
+ const previousState = previousScreenBrightnessRef.current;
413
+ previousScreenBrightnessRef.current = null;
414
+ if (!previousState) return;
415
+
416
+ try {
417
+ const brightness = await getBrightnessModule();
418
+ if (!brightness) return;
419
+ if (Platform.OS === "android") {
420
+ if (
421
+ previousState.didSetSystemBrightness &&
422
+ previousState.systemBrightness !== null
423
+ ) {
424
+ try {
425
+ await brightness.setSystemBrightnessAsync(
426
+ previousState.systemBrightness,
427
+ );
428
+ if (previousState.systemBrightnessMode !== null) {
429
+ await brightness.setSystemBrightnessModeAsync(
430
+ previousState.systemBrightnessMode,
431
+ );
432
+ }
433
+ } catch {
434
+ /* Fall back to restoring the current activity brightness. */
435
+ }
436
+ }
437
+ if (previousState.wasUsingSystemBrightness) {
438
+ try {
439
+ await brightness.restoreSystemBrightnessAsync();
440
+ return;
441
+ } catch {
442
+ /* Fall back to restoring the saved activity brightness. */
443
+ }
444
+ }
445
+ }
446
+ await brightness.setBrightnessAsync(previousState.brightness);
447
+ } catch (error) {
448
+ console.warn(
449
+ "[ThruWalletSheet] Failed to restore screen brightness:",
450
+ error,
451
+ );
452
+ }
453
+ });
454
+ }, [enqueueBrightnessTask]);
455
+
456
+ const maximizeScreenBrightness = useCallback(() => {
457
+ enqueueBrightnessTask(async () => {
458
+ try {
459
+ const brightness = await getBrightnessModule();
460
+ if (!brightness) return;
461
+ if (previousScreenBrightnessRef.current == null) {
462
+ previousScreenBrightnessRef.current =
463
+ await getPreviousScreenBrightnessState(brightness);
464
+ }
465
+ await brightness.setBrightnessAsync(1);
466
+ if (Platform.OS === "android") {
467
+ try {
468
+ await brightness.setSystemBrightnessAsync(1);
469
+ if (previousScreenBrightnessRef.current) {
470
+ previousScreenBrightnessRef.current.didSetSystemBrightness = true;
471
+ }
472
+ } catch {
473
+ /* Activity brightness above still maximizes the visible app. */
474
+ }
475
+ }
476
+ } catch (error) {
477
+ console.warn(
478
+ "[ThruWalletSheet] Failed to maximize screen brightness:",
479
+ error,
480
+ );
481
+ }
482
+ });
483
+ }, [enqueueBrightnessTask]);
484
+
485
+ const sendPairDeviceQrStatus = useCallback(
486
+ (
487
+ status: PairDeviceQrRenderStatus,
488
+ approveUrl: string,
489
+ reason?: string | null,
490
+ ) => {
491
+ webViewRef.current?.injectJavaScript(
492
+ getPairDeviceQrStatusScript(status, approveUrl, reason ?? undefined),
493
+ );
494
+ },
495
+ [],
496
+ );
497
+
498
+ const handleNativeQrError = useCallback(
499
+ (error: unknown) => {
500
+ console.warn("[ThruWalletSheet] Failed to render native QR:", error);
501
+ const reason =
502
+ error instanceof Error
503
+ ? `react-native-qrcode-styled render failed: ${error.message}`
504
+ : "react-native-qrcode-styled render failed";
505
+ if (pairDeviceQr) {
506
+ sendPairDeviceQrStatus("unavailable", pairDeviceQr.approveUrl, reason);
507
+ }
508
+ setNativeQrUnavailableReason(reason);
509
+ setIsNativeQrUnavailable(true);
510
+ setPairDeviceQr(null);
511
+ },
512
+ [pairDeviceQr, sendPairDeviceQrStatus],
513
+ );
514
+
515
+ useEffect(() => {
516
+ containerLayoutState.value = {
517
+ height,
518
+ offset: { top: 0, bottom: 0, left: 0, right: 0 },
519
+ };
520
+ }, [containerLayoutState, height]);
521
+
522
+ useEffect(() => {
523
+ return () => {
524
+ restoreScreenBrightness();
525
+ };
526
+ }, [restoreScreenBrightness]);
527
+
528
+ /* Build the wallet source once the SDK is available. Android and iOS
529
+ default to the shell iframe so the native SDK reuses the same wallet
530
+ postMessage protocol as web iframe consumers. */
531
+ useEffect(() => {
532
+ if (!wallet) return;
533
+ const walletUrl = appendNativeBottomInset(
534
+ wallet.getIframeSrc(),
535
+ insets.bottom,
536
+ );
537
+ const useDirectWallet =
538
+ Platform.OS === "ios" && wallet.getIosWebViewMode() === "direct";
539
+ if (useDirectWallet) {
540
+ setDirectWalletUrl(walletUrl);
541
+ setShellHtml(null);
542
+ } else {
543
+ const html = getShellHtml({
544
+ walletUrl,
545
+ walletOrigin: wallet.getWalletOrigin(),
546
+ });
547
+ setShellHtml(html);
548
+ setDirectWalletUrl(null);
549
+ }
550
+ setHasBridgeMessage(false);
551
+ setWalletLoadStatus("Loading wallet...");
552
+ setWebViewError(null);
553
+ setWebContentHeight(null);
554
+ setWebContentMaxSheetRatio(DEFAULT_FIT_CONTENT_MAX_SHEET_RATIO);
555
+ setWebContentMinSheetRatio(DEFAULT_FIT_CONTENT_MIN_SHEET_RATIO);
556
+ setPairDeviceQr(null);
557
+ setQRCodeStyled(() => RESOLVED_QR_CODE_STYLED);
558
+ setNativeQrUnavailableReason(
559
+ RESOLVED_QR_CODE_STYLED ? null : NATIVE_QR_IMPORT_UNAVAILABLE_REASON,
560
+ );
561
+ setIsNativeQrUnavailable(RESOLVED_QR_CODE_STYLED === null);
562
+ didRefreshWalletAvailabilityRef.current = false;
563
+ }, [insets.bottom, wallet]);
564
+
565
+ useEffect(() => {
566
+ if (!pairDeviceQr) return;
567
+ if (pairDeviceQr.qrDataUrl || (QRCodeStyled && !isNativeQrUnavailable)) {
568
+ sendPairDeviceQrStatus("ready", pairDeviceQr.approveUrl);
569
+ } else if (isNativeQrUnavailable) {
570
+ sendPairDeviceQrStatus(
571
+ "unavailable",
572
+ pairDeviceQr.approveUrl,
573
+ nativeQrUnavailableReason,
574
+ );
575
+ }
576
+ }, [
577
+ QRCodeStyled,
578
+ isNativeQrUnavailable,
579
+ nativeQrUnavailableReason,
580
+ pairDeviceQr,
581
+ sendPairDeviceQrStatus,
582
+ ]);
583
+
584
+ const isDirectWalletSource = directWalletUrl !== null;
585
+
586
+ /* Hand the WebView ref to the SDK once both exist. We expose only
587
+ the shape NativeProvider needs (injectJavaScript). Also flip the
588
+ Android WebView's WebAuthn support on, since react-native-webview
589
+ doesn't do it for us. iOS is a no-op (WKWebView WebAuthn is
590
+ governed by WKAppBoundDomains, set by our Expo config plugin). */
591
+ const attachIfReady = useCallback(() => {
592
+ if (!wallet || !webViewRef.current) return;
593
+ const ref: WebViewRefLike = {
594
+ injectJavaScript: (script: string) => {
595
+ webViewRef.current?.injectJavaScript(script);
596
+ },
597
+ };
598
+ wallet.attachWebView(ref);
599
+ }, [wallet]);
600
+
601
+ const enableAndroidWebAuthnIfNeeded = useCallback(async () => {
602
+ if (Platform.OS !== "android") return false;
603
+ const enabled = await enableWebAuthnSupport(webViewNativeTagRef.current);
604
+ webViewRef.current?.injectJavaScript(
605
+ "window.dispatchEvent(new Event('thru:native-webauthn-ready')); true;",
606
+ );
607
+ return enabled;
608
+ }, []);
609
+
610
+ const handleWebViewLayout = useCallback(
611
+ (event: LayoutChangeEvent) => {
612
+ const target = (event.nativeEvent as { target?: unknown }).target;
613
+ webViewNativeTagRef.current =
614
+ typeof target === "number" ? target : webViewNativeTagRef.current;
615
+ void enableAndroidWebAuthnIfNeeded();
616
+ },
617
+ [enableAndroidWebAuthnIfNeeded],
618
+ );
619
+
620
+ const refreshWalletAvailabilityIfReady = useCallback(() => {
621
+ if (!wallet || didRefreshWalletAvailabilityRef.current) return;
622
+ didRefreshWalletAvailabilityRef.current = true;
623
+ void wallet.refreshWalletAvailability();
624
+ }, [wallet]);
625
+
626
+ const handleLoadEnd = useCallback(() => {
627
+ attachIfReady();
628
+ if (isDirectWalletSource) {
629
+ wallet?.markWebViewReady();
630
+ setHasBridgeMessage(true);
631
+ setWebViewError(null);
632
+ void enableAndroidWebAuthnIfNeeded().finally(
633
+ refreshWalletAvailabilityIfReady,
634
+ );
635
+ }
636
+ }, [
637
+ attachIfReady,
638
+ enableAndroidWebAuthnIfNeeded,
639
+ isDirectWalletSource,
640
+ refreshWalletAvailabilityIfReady,
641
+ wallet,
642
+ ]);
643
+
644
+ /* Wire show/hide callbacks through the SDK's provider so UI_SHOW from
645
+ the wallet expands the sheet and request resolution closes it. */
646
+ useEffect(() => {
647
+ if (!wallet) return;
648
+ wallet.setUiHandlers({
649
+ onShowRequested: () => {
650
+ isProviderClosingRef.current = false;
651
+ snapToSheetIndex(openIndex);
652
+ },
653
+ onHideRequested: () => {
654
+ isProviderClosingRef.current = true;
655
+ sheetRef.current?.close();
656
+ },
657
+ });
658
+ return () => {
659
+ wallet.clearUiHandlers();
660
+ };
661
+ }, [wallet, openIndex, snapToSheetIndex]);
662
+
663
+ useEffect(() => {
664
+ if (!isSheetOpenRef.current) return;
665
+ const animationFrame = requestAnimationFrame(() => {
666
+ snapToSheetIndex(openIndex);
667
+ });
668
+ return () => cancelAnimationFrame(animationFrame);
669
+ }, [height, memoSnapPoints, openIndex, snapToSheetIndex]);
670
+
671
+ const walletOrigin = wallet?.getWalletOrigin();
672
+ const webViewSource = useMemo(() => {
673
+ if (directWalletUrl) return { uri: directWalletUrl };
674
+ if (shellHtml) {
675
+ return { html: shellHtml, baseUrl: walletOrigin ?? "about:blank" };
676
+ }
677
+ return null;
678
+ }, [directWalletUrl, shellHtml, walletOrigin]);
679
+
680
+ useEffect(() => {
681
+ if (!webViewSource) return;
682
+ attachIfReady();
683
+ void enableAndroidWebAuthnIfNeeded();
684
+ }, [attachIfReady, enableAndroidWebAuthnIfNeeded, webViewSource]);
685
+
686
+ const limitsNavigationsToAppBoundDomains =
687
+ Platform.OS === "ios" && isDirectWalletSource;
688
+
689
+ const handleMessage = useCallback(
690
+ (event: WebViewMessageEvent) => {
691
+ let shouldRefreshAfterBridgeReady = false;
692
+ try {
693
+ const data = JSON.parse(event.nativeEvent.data) as {
694
+ type?: string;
695
+ data?: {
696
+ approveUrl?: string;
697
+ fitContent?: boolean;
698
+ frame?: unknown;
699
+ height?: number;
700
+ maxSheetRatio?: number;
701
+ minSheetRatio?: number;
702
+ mode?: string;
703
+ qrDataUrl?: string;
704
+ src?: string;
705
+ visible?: boolean;
706
+ };
707
+ };
708
+ if (data.type === NATIVE_CONTENT_HEIGHT_MESSAGE) {
709
+ const nextMaxSheetRatio = data.data?.maxSheetRatio;
710
+ const nextMinSheetRatio = data.data?.minSheetRatio;
711
+ setWebContentMaxSheetRatio(
712
+ typeof nextMaxSheetRatio === "number" &&
713
+ Number.isFinite(nextMaxSheetRatio) &&
714
+ nextMaxSheetRatio > 0 &&
715
+ nextMaxSheetRatio <= 1
716
+ ? nextMaxSheetRatio
717
+ : DEFAULT_FIT_CONTENT_MAX_SHEET_RATIO,
718
+ );
719
+ setWebContentMinSheetRatio(
720
+ typeof nextMinSheetRatio === "number" &&
721
+ Number.isFinite(nextMinSheetRatio) &&
722
+ nextMinSheetRatio >= 0 &&
723
+ nextMinSheetRatio <= 1
724
+ ? nextMinSheetRatio
725
+ : DEFAULT_FIT_CONTENT_MIN_SHEET_RATIO,
726
+ );
727
+ if (data.data?.fitContent === false) {
728
+ setWebContentHeight(null);
729
+ return;
730
+ }
731
+ const nextHeight = data.data?.height;
732
+ if (
733
+ typeof nextHeight === "number" &&
734
+ Number.isFinite(nextHeight) &&
735
+ nextHeight > 0
736
+ ) {
737
+ setWebContentHeight(Math.ceil(nextHeight));
738
+ }
739
+ return;
740
+ }
741
+ if (data.type === NATIVE_PAIR_DEVICE_QR_MESSAGE) {
742
+ if (data.data?.visible === false) {
743
+ setPairDeviceQr(null);
744
+ return;
745
+ }
746
+ const approveUrl = data.data?.approveUrl;
747
+ const frame = parsePairDeviceQrFrame(data.data?.frame);
748
+ if (typeof approveUrl === "string" && approveUrl && frame) {
749
+ const qrDataUrl =
750
+ typeof data.data?.qrDataUrl === "string" &&
751
+ data.data.qrDataUrl.startsWith("data:image/")
752
+ ? data.data.qrDataUrl
753
+ : undefined;
754
+ if (isNativeQrUnavailable) {
755
+ sendPairDeviceQrStatus(
756
+ "unavailable",
757
+ approveUrl,
758
+ nativeQrUnavailableReason,
759
+ );
760
+ return;
761
+ }
762
+ setPairDeviceQr({ approveUrl, frame, qrDataUrl });
763
+ sendPairDeviceQrStatus("rendering", approveUrl);
764
+ }
765
+ return;
766
+ }
767
+ if (data.type === NATIVE_SCREEN_BRIGHTNESS_MESSAGE) {
768
+ if (data.data?.mode === "max") {
769
+ maximizeScreenBrightness();
770
+ } else if (data.data?.mode === "restore") {
771
+ restoreScreenBrightness();
772
+ }
773
+ return;
774
+ }
775
+ if (data.type === "shell:loading") {
776
+ setWalletLoadStatus("Loading wallet iframe...");
777
+ return;
778
+ }
779
+ if (data.type === "shell:iframe-load") {
780
+ setWebViewError(null);
781
+ setWalletLoadStatus(
782
+ "Wallet iframe loaded. Waiting for wallet app...",
783
+ );
784
+ return;
785
+ }
786
+ if (data.type === "shell:iframe-error") {
787
+ if (hasBridgeMessage) {
788
+ console.warn(
789
+ "[ThruWalletSheet] Ignoring post-ready wallet iframe error:",
790
+ data.data,
791
+ );
792
+ return;
793
+ }
794
+ setWebViewError(
795
+ `Wallet iframe failed to load${data.data?.src ? `: ${data.data.src}` : ""}`,
796
+ );
797
+ return;
798
+ }
799
+ if (data.type === "iframe:ready") {
800
+ setHasBridgeMessage(true);
801
+ setWebViewError(null);
802
+ shouldRefreshAfterBridgeReady = true;
803
+ }
804
+ } catch {
805
+ /* Let the bridge ignore malformed messages. */
806
+ }
807
+ wallet?.onMessage({
808
+ nativeEvent: { data: event.nativeEvent.data },
809
+ });
810
+ if (shouldRefreshAfterBridgeReady) {
811
+ void enableAndroidWebAuthnIfNeeded().finally(
812
+ refreshWalletAvailabilityIfReady,
813
+ );
814
+ }
815
+ },
816
+ [
817
+ enableAndroidWebAuthnIfNeeded,
818
+ hasBridgeMessage,
819
+ isNativeQrUnavailable,
820
+ nativeQrUnavailableReason,
821
+ maximizeScreenBrightness,
822
+ refreshWalletAvailabilityIfReady,
823
+ restoreScreenBrightness,
824
+ sendPairDeviceQrStatus,
825
+ wallet,
826
+ ],
827
+ );
828
+
829
+ useImperativeHandle(
830
+ ref,
831
+ () => ({
832
+ expand: (index?: number) => snapToSheetIndex(index ?? openIndex),
833
+ close: () => sheetRef.current?.close(),
834
+ }),
835
+ [openIndex, snapToSheetIndex],
836
+ );
837
+
838
+ const handleSheetChange = useCallback(
839
+ (index: number) => {
840
+ isSheetOpenRef.current = index !== -1;
841
+ if (index !== -1) return;
842
+ restoreScreenBrightness();
843
+ setPairDeviceQr(null);
844
+ if (isProviderClosingRef.current) {
845
+ isProviderClosingRef.current = false;
846
+ return;
847
+ }
848
+ wallet?.rejectPendingRequests();
849
+ webViewRef.current?.injectJavaScript(
850
+ "window.dispatchEvent(new Event('thru:native-sheet-dismiss')); true;",
851
+ );
852
+ },
853
+ [restoreScreenBrightness, wallet],
854
+ );
855
+
856
+ const pairDeviceQrSize = pairDeviceQr
857
+ ? Math.max(
858
+ 96,
859
+ Math.floor(
860
+ Math.min(pairDeviceQr.frame.width, pairDeviceQr.frame.height) - 32,
861
+ ),
862
+ )
863
+ : 0;
864
+ const pairDeviceQrBadgeFontSize = Math.max(16, pairDeviceQrSize * 0.1);
865
+ const renderHandle = useCallback(
866
+ () => (
867
+ <View style={[styles.handleContainer, { backgroundColor }]}>
868
+ <View style={styles.handleIndicator} />
869
+ </View>
870
+ ),
871
+ [backgroundColor],
872
+ );
873
+
874
+ const renderBackdrop = useCallback(
875
+ (props: BottomSheetBackdropProps) => (
876
+ <BottomSheetBackdrop
877
+ {...props}
878
+ appearsOnIndex={0}
879
+ disappearsOnIndex={-1}
880
+ opacity={0.38}
881
+ pressBehavior="close"
882
+ />
883
+ ),
884
+ [],
885
+ );
886
+
887
+ return (
888
+ <BottomSheet
889
+ ref={sheetRef}
890
+ index={-1}
891
+ snapPoints={memoSnapPoints}
892
+ containerLayoutState={containerLayoutState}
893
+ enableDynamicSizing={false}
894
+ enableContentPanningGesture={false}
895
+ handleComponent={renderHandle}
896
+ backdropComponent={renderBackdrop}
897
+ enablePanDownToClose
898
+ onChange={handleSheetChange}
899
+ backgroundStyle={{ backgroundColor }}
900
+ >
901
+ <View style={[styles.body, { backgroundColor }]}>
902
+ {QRCodeStyled ? (
903
+ <View
904
+ collapsable={false}
905
+ pointerEvents="none"
906
+ style={styles.nativeQrWarmup}
907
+ >
908
+ <QRCodeStyled
909
+ data={NATIVE_QR_WARMUP_DATA}
910
+ size={64}
911
+ color={NATIVE_QR_DARK_COLOR}
912
+ errorCorrectionLevel="L"
913
+ padding={0}
914
+ pieceScale={1.025}
915
+ />
916
+ </View>
917
+ ) : null}
918
+ {webViewSource ? (
919
+ <WebView
920
+ ref={webViewRef}
921
+ source={webViewSource}
922
+ originWhitelist={["*"]}
923
+ javaScriptEnabled
924
+ domStorageEnabled
925
+ webviewDebuggingEnabled={__DEV__}
926
+ nestedScrollEnabled
927
+ sharedCookiesEnabled
928
+ allowsInlineMediaPlayback
929
+ mediaPlaybackRequiresUserAction={false}
930
+ /* iOS WKWebView: direct wallet pages need app-bound
931
+ navigation so WebAuthn and injected JS run in the
932
+ WKAppBoundDomains context configured by the host app. */
933
+ limitsNavigationsToAppBoundDomains={
934
+ limitsNavigationsToAppBoundDomains
935
+ }
936
+ onLoadStart={() => {
937
+ attachIfReady();
938
+ void enableAndroidWebAuthnIfNeeded();
939
+ }}
940
+ onLoadEnd={handleLoadEnd}
941
+ onLayout={handleWebViewLayout}
942
+ onError={(event) => {
943
+ const description =
944
+ event.nativeEvent.description ||
945
+ "Wallet WebView failed to load";
946
+ if (hasBridgeMessage) {
947
+ console.warn(
948
+ "[ThruWalletSheet] Ignoring post-ready WebView error:",
949
+ event.nativeEvent,
950
+ );
951
+ return;
952
+ }
953
+ setWebViewError(description);
954
+ console.warn("[ThruWalletSheet] WebView error:", description);
955
+ }}
956
+ onHttpError={(event) => {
957
+ const status = event.nativeEvent.statusCode;
958
+ const description = `Wallet returned HTTP ${status}`;
959
+ if (hasBridgeMessage) {
960
+ console.warn(
961
+ "[ThruWalletSheet] Ignoring post-ready WebView HTTP error:",
962
+ event.nativeEvent,
963
+ );
964
+ return;
965
+ }
966
+ setWebViewError(description);
967
+ console.warn(
968
+ "[ThruWalletSheet] WebView HTTP error:",
969
+ description,
970
+ );
971
+ }}
972
+ onMessage={handleMessage}
973
+ style={[styles.webview, { backgroundColor }]}
974
+ />
975
+ ) : null}
976
+ {webViewSource &&
977
+ ((!isDirectWalletSource && !hasBridgeMessage) || webViewError) ? (
978
+ <View
979
+ pointerEvents="none"
980
+ style={[styles.loadingOverlay, { backgroundColor }]}
981
+ >
982
+ <Text style={styles.loadingTitle}>
983
+ {webViewError ? "Wallet failed to load" : walletLoadStatus}
984
+ </Text>
985
+ {webViewError ? (
986
+ <Text style={styles.loadingDetail}>{webViewError}</Text>
987
+ ) : null}
988
+ </View>
989
+ ) : null}
990
+ {pairDeviceQr && (pairDeviceQr.qrDataUrl || QRCodeStyled) ? (
991
+ <View
992
+ pointerEvents="none"
993
+ style={[
994
+ styles.nativeQrOverlay,
995
+ {
996
+ height: pairDeviceQr.frame.height,
997
+ left: pairDeviceQr.frame.left,
998
+ top: pairDeviceQr.frame.top,
999
+ width: pairDeviceQr.frame.width,
1000
+ },
1001
+ ]}
1002
+ >
1003
+ <OptionalNativeQrBoundary
1004
+ key={pairDeviceQr.approveUrl}
1005
+ onError={handleNativeQrError}
1006
+ >
1007
+ <View style={styles.nativeQrCard}>
1008
+ {pairDeviceQr.qrDataUrl ? (
1009
+ <Image
1010
+ resizeMode="contain"
1011
+ source={{ uri: pairDeviceQr.qrDataUrl }}
1012
+ style={[
1013
+ styles.nativeQrImage,
1014
+ {
1015
+ height: pairDeviceQrSize,
1016
+ width: pairDeviceQrSize,
1017
+ },
1018
+ ]}
1019
+ />
1020
+ ) : QRCodeStyled ? (
1021
+ <View
1022
+ style={[
1023
+ styles.nativeQrStyledFallback,
1024
+ {
1025
+ height: pairDeviceQrSize,
1026
+ width: pairDeviceQrSize,
1027
+ },
1028
+ ]}
1029
+ >
1030
+ <QRCodeStyled
1031
+ data={pairDeviceQr.approveUrl}
1032
+ size={pairDeviceQrSize}
1033
+ color={NATIVE_QR_DARK_COLOR}
1034
+ gradient={NATIVE_QR_GRADIENT}
1035
+ errorCorrectionLevel="H"
1036
+ innerEyesOptions={NATIVE_QR_INNER_EYE_OPTIONS}
1037
+ isPiecesGlued
1038
+ outerEyesOptions={NATIVE_QR_OUTER_EYE_OPTIONS}
1039
+ padding={0}
1040
+ pieceBorderRadius="42%"
1041
+ pieceCornerType="rounded"
1042
+ pieceLiquidRadius="30%"
1043
+ pieceScale={1.02}
1044
+ style={styles.nativeQrSvg}
1045
+ />
1046
+ <View style={styles.nativeQrFallbackBadge}>
1047
+ <Text
1048
+ style={[
1049
+ styles.nativeQrFallbackBadgeText,
1050
+ {
1051
+ fontSize: pairDeviceQrBadgeFontSize,
1052
+ lineHeight: pairDeviceQrBadgeFontSize * 1.05,
1053
+ },
1054
+ ]}
1055
+ >
1056
+ J
1057
+ </Text>
1058
+ </View>
1059
+ </View>
1060
+ ) : null}
1061
+ </View>
1062
+ </OptionalNativeQrBoundary>
1063
+ </View>
1064
+ ) : null}
1065
+ </View>
1066
+ </BottomSheet>
1067
+ );
1068
+ });
1069
+
1070
+ const styles = StyleSheet.create({
1071
+ body: { flex: 1 },
1072
+ handleContainer: {
1073
+ alignItems: "center",
1074
+ height: SHEET_HANDLE_HEIGHT,
1075
+ justifyContent: "flex-start",
1076
+ paddingTop: 4,
1077
+ },
1078
+ handleIndicator: {
1079
+ backgroundColor: "#cdd5db",
1080
+ borderRadius: 999,
1081
+ height: 4,
1082
+ width: 42,
1083
+ },
1084
+ loadingDetail: {
1085
+ color: "#4b635f",
1086
+ fontSize: 13,
1087
+ lineHeight: 18,
1088
+ maxWidth: 280,
1089
+ textAlign: "center",
1090
+ },
1091
+ loadingOverlay: {
1092
+ alignItems: "center",
1093
+ bottom: 0,
1094
+ gap: 8,
1095
+ justifyContent: "center",
1096
+ left: 0,
1097
+ padding: 24,
1098
+ position: "absolute",
1099
+ right: 0,
1100
+ top: 0,
1101
+ },
1102
+ loadingTitle: {
1103
+ color: "#172b29",
1104
+ fontSize: 16,
1105
+ fontWeight: "600",
1106
+ textAlign: "center",
1107
+ },
1108
+ nativeQrCard: {
1109
+ alignItems: "center",
1110
+ backgroundColor: "#ffffff",
1111
+ borderColor: "#dbe4e8",
1112
+ borderRadius: 8,
1113
+ borderWidth: StyleSheet.hairlineWidth,
1114
+ flex: 1,
1115
+ justifyContent: "center",
1116
+ },
1117
+ nativeQrOverlay: {
1118
+ elevation: 16,
1119
+ position: "absolute",
1120
+ zIndex: 16,
1121
+ },
1122
+ nativeQrImage: {
1123
+ backgroundColor: "#ffffff",
1124
+ },
1125
+ nativeQrFallbackBadge: {
1126
+ alignItems: "center",
1127
+ aspectRatio: 1,
1128
+ backgroundColor: "#ffffff",
1129
+ borderColor: "#d1e1e1",
1130
+ borderRadius: 999,
1131
+ borderWidth: StyleSheet.hairlineWidth,
1132
+ justifyContent: "center",
1133
+ left: "41.5%",
1134
+ position: "absolute",
1135
+ top: "41.5%",
1136
+ width: "17%",
1137
+ },
1138
+ nativeQrFallbackBadgeText: {
1139
+ color: NATIVE_QR_ACCENT_DARK_COLOR,
1140
+ fontWeight: "700",
1141
+ includeFontPadding: false,
1142
+ textAlign: "center",
1143
+ },
1144
+ nativeQrStyledFallback: {
1145
+ alignItems: "center",
1146
+ backgroundColor: "#ffffff",
1147
+ justifyContent: "center",
1148
+ },
1149
+ nativeQrSvg: {
1150
+ backgroundColor: "#ffffff",
1151
+ },
1152
+ nativeQrWarmup: {
1153
+ height: 64,
1154
+ left: -128,
1155
+ opacity: 0,
1156
+ position: "absolute",
1157
+ top: -128,
1158
+ width: 64,
1159
+ zIndex: -1,
1160
+ },
1161
+ webview: { flex: 1, backgroundColor: "transparent" },
1162
+ });