@thru/wallet 0.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/android/build.gradle +37 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/org/thru/walletnative/ThruWebViewBridgeModule.kt +77 -0
- package/app.plugin.cjs +101 -0
- package/dist/BrowserSDK-CpRFiJsW.d.ts +409 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +941 -0
- package/dist/index.js.map +1 -0
- package/dist/native/react.d.ts +109 -0
- package/dist/native/react.js +2381 -0
- package/dist/native/react.js.map +1 -0
- package/dist/native.d.ts +329 -0
- package/dist/native.js +1126 -0
- package/dist/native.js.map +1 -0
- package/dist/react-ui.d.ts +5 -0
- package/dist/react-ui.js +266 -0
- package/dist/react-ui.js.map +1 -0
- package/dist/react.d.ts +66 -0
- package/dist/react.js +1151 -0
- package/dist/react.js.map +1 -0
- package/expo-module.config.json +6 -0
- package/package.json +114 -0
- package/src/BrowserSDK.ts +315 -0
- package/src/index.ts +27 -0
- package/src/interfaces/IThruChain.ts +37 -0
- package/src/interfaces/accounts.ts +61 -0
- package/src/interfaces/index.ts +9 -0
- package/src/interfaces/types.ts +95 -0
- package/src/native/NativeSDK.test.ts +819 -0
- package/src/native/NativeSDK.ts +773 -0
- package/src/native/index.ts +39 -0
- package/src/native/provider/NativeProvider.ts +363 -0
- package/src/native/provider/WebViewBridge.test.ts +339 -0
- package/src/native/provider/WebViewBridge.ts +339 -0
- package/src/native/provider/chains/ThruChain.ts +85 -0
- package/src/native/provider/shell.html +88 -0
- package/src/native/provider/shell.test.ts +56 -0
- package/src/native/provider/shell.ts +111 -0
- package/src/native/provider/shims-html.d.ts +4 -0
- package/src/native/react/ThruContext.ts +37 -0
- package/src/native/react/ThruProvider.tsx +168 -0
- package/src/native/react/ThruWalletSheet.tsx +1162 -0
- package/src/native/react/android-webauthn.ts +37 -0
- package/src/native/react/hooks/useAccounts.ts +35 -0
- package/src/native/react/hooks/useThru.ts +11 -0
- package/src/native/react/hooks/useWallet.ts +71 -0
- package/src/native/react/hooks/useWalletAvailability.ts +31 -0
- package/src/native/react/hooks/waitForWallet.ts +21 -0
- package/src/native/react/index.ts +29 -0
- package/src/protocol/index.ts +2 -0
- package/src/protocol/postMessage.ts +283 -0
- package/src/protocol/walletState.ts +12 -0
- package/src/provider/EmbeddedProvider.ts +330 -0
- package/src/provider/IframeManager.ts +438 -0
- package/src/provider/chains/ThruChain.ts +86 -0
- package/src/provider/index.ts +17 -0
- package/src/provider/types/messages.ts +37 -0
- package/src/react/ThruContext.ts +31 -0
- package/src/react/ThruProvider.tsx +169 -0
- package/src/react/hooks/useAccounts.ts +38 -0
- package/src/react/hooks/useThru.ts +11 -0
- package/src/react/hooks/useWallet.ts +81 -0
- package/src/react/index.ts +30 -0
- package/src/react-ui/ThruAccountSwitcher.tsx +187 -0
- package/src/react-ui/custom.d.ts +8 -0
- package/src/react-ui/index.ts +1 -0
- package/src/static/logo.png +0 -0
- package/src/static/logomark_red.svg +11 -0
|
@@ -0,0 +1,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
|
+
});
|