@dynamic-labs/react-native-extension 4.92.3 → 4.93.0
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/android/EmbeddedWebViewController.kt +19 -2
- package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
- package/android/embeddedwebview/EmbeddedWebViewController.kt +19 -2
- package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
- package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
- package/index.cjs +252 -796
- package/index.js +251 -797
- package/ios/EmbeddedWebViewController.swift +7 -6
- package/package.json +6 -6
- package/src/ReactNativeExtension/ReactNativeExtension.d.ts +11 -2
- package/src/components/WebView/EmbeddedWebView/EmbeddedWebView.d.ts +6 -19
- package/src/components/WebView/WebView.d.ts +17 -6
- package/src/errors/WebViewFailedToLoadError.d.ts +1 -86
- package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/embeddedWebViewPhaseTimers.d.ts +0 -66
- package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/index.d.ts +0 -1
- package/src/components/WebView/constants.d.ts +0 -3
- package/src/components/WebView/successLog/emitSuccessLog.d.ts +0 -35
- package/src/components/WebView/successLog/index.d.ts +0 -3
- package/src/components/WebView/useWebViewLoadingTimeout/index.d.ts +0 -1
- package/src/components/WebView/useWebViewLoadingTimeout/useWebViewLoadingTimeout.d.ts +0 -4
- package/src/components/WebView/useWebViewPhaseTimers/index.d.ts +0 -1
- package/src/components/WebView/useWebViewPhaseTimers/useWebViewPhaseTimers.d.ts +0 -46
- package/src/components/WebView/useWebViewRecoveryTimeout/index.d.ts +0 -1
- package/src/components/WebView/useWebViewRecoveryTimeout/useWebViewRecoveryTimeout.d.ts +0 -12
- package/src/components/WebView/useWebViewSuccessLog/index.d.ts +0 -1
- package/src/components/WebView/useWebViewSuccessLog/useWebViewSuccessLog.d.ts +0 -28
- package/src/components/WebView/utils/assignEnvironmentIdToUrl/assignEnvironmentIdToUrl.d.ts +0 -1
- package/src/components/WebView/utils/assignEnvironmentIdToUrl/index.d.ts +0 -1
- package/src/components/WebView/utils/assignStartTimeToUrl/assignStartTimeToUrl.d.ts +0 -1
- package/src/components/WebView/utils/assignStartTimeToUrl/index.d.ts +0 -1
- package/src/components/WebView/utils/hasClearStateInUrl/hasClearStateInUrl.d.ts +0 -1
- package/src/components/WebView/utils/hasClearStateInUrl/index.d.ts +0 -1
- package/src/components/WebView/utils/increaseRetryToUrl/increaseRetryToUrl.d.ts +0 -1
- package/src/components/WebView/utils/increaseRetryToUrl/index.d.ts +0 -1
- package/src/components/WebView/utils/setClearStateToUrl/index.d.ts +0 -1
- package/src/components/WebView/utils/setClearStateToUrl/setClearStateToUrl.d.ts +0 -1
- package/src/components/WebView/webViewPhaseTimerCore/index.d.ts +0 -2
- package/src/components/WebView/webViewPhaseTimerCore/webViewPhaseTimerCore.d.ts +0 -123
package/index.cjs
CHANGED
|
@@ -6,9 +6,9 @@ var assertPackageVersion = require('@dynamic-labs/assert-package-version');
|
|
|
6
6
|
var reactNative = require('react-native');
|
|
7
7
|
var react = require('react');
|
|
8
8
|
var reactNativeWebview = require('react-native-webview');
|
|
9
|
+
var client = require('@dynamic-labs/client');
|
|
9
10
|
var logger$1 = require('@dynamic-labs/logger');
|
|
10
11
|
var messageTransport = require('@dynamic-labs/message-transport');
|
|
11
|
-
var webviewMessages = require('@dynamic-labs/webview-messages');
|
|
12
12
|
var expoSecureStore = require('expo-secure-store');
|
|
13
13
|
var jsxRuntime = require('react/jsx-runtime');
|
|
14
14
|
var expoModulesCore = require('expo-modules-core');
|
|
@@ -35,7 +35,7 @@ function _interopNamespace(e) {
|
|
|
35
35
|
return Object.freeze(n);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
var version = "4.
|
|
38
|
+
var version = "4.93.0";
|
|
39
39
|
|
|
40
40
|
function _extends() {
|
|
41
41
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -47,12 +47,35 @@ function _extends() {
|
|
|
47
47
|
}, _extends.apply(null, arguments);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
/******************************************************************************
|
|
51
|
+
Copyright (c) Microsoft Corporation.
|
|
52
|
+
|
|
53
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
54
|
+
purpose with or without fee is hereby granted.
|
|
55
|
+
|
|
56
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
57
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
58
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
59
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
60
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
61
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
62
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
63
|
+
***************************************************************************** */
|
|
64
|
+
|
|
65
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
66
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
67
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
68
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
69
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
70
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
71
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
76
|
+
var e = new Error(message);
|
|
77
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
78
|
+
};
|
|
56
79
|
|
|
57
80
|
const logger = new logger$1.Logger('react-native-extension');
|
|
58
81
|
|
|
@@ -123,284 +146,40 @@ const useMessageTransportWebViewBridge = (core, webViewRef) => {
|
|
|
123
146
|
};
|
|
124
147
|
};
|
|
125
148
|
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
const hasClearStateInUrl = webViewUrl => Boolean(webViewUrl.searchParams.get(CLEAR_STATE_PARAM));
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Wire-format `type` of a webview-side `manifest` request.
|
|
134
|
-
*
|
|
135
|
-
* `requestChannel.request('manifest')` sends a `MessageTransportData` whose
|
|
136
|
-
* `type` is the literal request name. The host replies with `manifest__ack`,
|
|
137
|
-
* which we ignore: receiving the request is sufficient evidence that the
|
|
138
|
-
* webview JS bundle is alive and the message bridge works.
|
|
139
|
-
*/
|
|
140
|
-
const MANIFEST_MESSAGE_TYPE = 'manifest';
|
|
141
|
-
const emptyTimings = () => ({
|
|
142
|
-
htmlLoadStartedAt: null,
|
|
143
|
-
htmlLoadedAt: null,
|
|
144
|
-
manifestReceivedAt: null,
|
|
145
|
-
onLoadEndAt: null,
|
|
146
|
-
sdkReadyAt: null
|
|
147
|
-
});
|
|
148
|
-
const diffMs = (later, earlier) => later !== null && earlier !== null ? later - earlier : null;
|
|
149
|
-
/**
|
|
150
|
-
* Derive cross-phase durations from raw timestamps. The four durations
|
|
151
|
-
* are what end up in the failure / success log meta:
|
|
152
|
-
*
|
|
153
|
-
* - `htmlLoadMs` — `onLoadStart` to `onLoad`; native HTML fetch
|
|
154
|
-
* - `onLoadToOnLoadEndMs` — `onLoad` to `onLoadEnd`; native render/parse
|
|
155
|
-
* - `manifestReceivedMs` — `onLoadEnd` to first `manifest` request from
|
|
156
|
-
* the webview JS; "JS bundle is alive" signal
|
|
157
|
-
* - `sdkReadyMs` — `onLoadEnd` to first `sdkHasLoaded` event from the
|
|
158
|
-
* webview JS; "SDK fully bootstrapped" signal
|
|
159
|
-
*/
|
|
160
|
-
const computePhaseDurations = timings => ({
|
|
161
|
-
htmlLoadMs: diffMs(timings.htmlLoadedAt, timings.htmlLoadStartedAt),
|
|
162
|
-
manifestReceivedMs: diffMs(timings.manifestReceivedAt, timings.onLoadEndAt),
|
|
163
|
-
onLoadToOnLoadEndMs: diffMs(timings.onLoadEndAt, timings.htmlLoadedAt),
|
|
164
|
-
sdkReadyMs: diffMs(timings.sdkReadyAt, timings.onLoadEndAt)
|
|
165
|
-
});
|
|
166
|
-
/**
|
|
167
|
-
* Build the success-shape meta from a {@link WebViewPhaseTimerCore}
|
|
168
|
-
* snapshot plus the path-specific fields. Both call sites call this
|
|
169
|
-
* with their own context (RN reads `hadClearState` / `retryCount` from
|
|
170
|
-
* the URL; embedded passes constants) so the success-log payload stays
|
|
171
|
-
* identical across the two paths.
|
|
172
|
-
*/
|
|
173
|
-
const buildSuccessMeta = ({
|
|
174
|
-
core,
|
|
175
|
-
hadClearState,
|
|
176
|
-
loadingTimeoutMs,
|
|
177
|
-
recoveryTimeoutMs,
|
|
178
|
-
retryCount,
|
|
179
|
-
webviewUrl
|
|
180
|
-
}) => Object.assign(Object.assign({
|
|
181
|
-
hadClearState,
|
|
182
|
-
loadingTimeoutMs,
|
|
183
|
-
recoveryTimeoutMs,
|
|
184
|
-
retryCount,
|
|
185
|
-
webviewUrl
|
|
186
|
-
}, computePhaseDurations(core.getTimings())), core.getCounters());
|
|
187
|
-
/**
|
|
188
|
-
* Shared state machine driving WebView load-phase instrumentation.
|
|
189
|
-
*
|
|
190
|
-
* Both the React `<WebView>` (RN bridge) and the native embedded WebView
|
|
191
|
-
* (native bridge) feed lifecycle events into this core and produce
|
|
192
|
-
* matching meta from the same timings + counters. Each path layers its
|
|
193
|
-
* own path-specific fields (`hadClearState`, `retryCount`, etc.) on top
|
|
194
|
-
* of the raw state this core exposes via `getTimings()` / `getCounters()`.
|
|
195
|
-
*
|
|
196
|
-
* The core owns the `core.messageTransport` subscription that captures
|
|
197
|
-
* the two webview-originated signals bracketing SDK bootstrap
|
|
198
|
-
* (`manifest` once the JS bundle is alive; `sdkHasLoadedEventName`
|
|
199
|
-
* once the SDK is fully ready). Native bridge lifecycle events come in
|
|
200
|
-
* via `recordEvent` instead.
|
|
201
|
-
*/
|
|
202
|
-
const createWebViewPhaseTimerCore = ({
|
|
203
|
-
core
|
|
204
|
-
}) => {
|
|
205
|
-
let timings = emptyTimings();
|
|
206
|
-
let nativeErrorCount = 0;
|
|
207
|
-
let osKillCount = 0;
|
|
208
|
-
/**
|
|
209
|
-
* We only capture the first occurrence per attempt so that a healthy
|
|
210
|
-
* reload doesn't overwrite the timing from the attempt that ultimately
|
|
211
|
-
* failed.
|
|
212
|
-
*/
|
|
213
|
-
const handleMessage = message => {
|
|
214
|
-
if (message.origin !== 'webview') return;
|
|
215
|
-
if (message.type === MANIFEST_MESSAGE_TYPE && timings.manifestReceivedAt === null) {
|
|
216
|
-
timings.manifestReceivedAt = Date.now();
|
|
217
|
-
}
|
|
218
|
-
if (message.type === webviewMessages.sdkHasLoadedEventName && timings.sdkReadyAt === null) {
|
|
219
|
-
timings.sdkReadyAt = Date.now();
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
core.messageTransport.on(handleMessage);
|
|
223
|
-
return {
|
|
224
|
-
dispose: () => {
|
|
225
|
-
core.messageTransport.off(handleMessage);
|
|
226
|
-
},
|
|
227
|
-
getCounters: () => ({
|
|
228
|
-
nativeErrorCount,
|
|
229
|
-
osKillCount
|
|
230
|
-
}),
|
|
231
|
-
getTimings: () => timings,
|
|
232
|
-
recordEvent: event => {
|
|
233
|
-
const now = Date.now();
|
|
234
|
-
switch (event) {
|
|
235
|
-
case 'load_start':
|
|
236
|
-
timings = emptyTimings();
|
|
237
|
-
timings.htmlLoadStartedAt = now;
|
|
238
|
-
break;
|
|
239
|
-
case 'load':
|
|
240
|
-
timings.htmlLoadedAt = now;
|
|
241
|
-
break;
|
|
242
|
-
case 'load_end':
|
|
243
|
-
timings.onLoadEndAt = now;
|
|
244
|
-
break;
|
|
245
|
-
case 'native_error':
|
|
246
|
-
nativeErrorCount += 1;
|
|
247
|
-
break;
|
|
248
|
-
case 'os_kill':
|
|
249
|
-
osKillCount += 1;
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const RETRY_QUERY_PARAM = 'retry';
|
|
257
|
-
const readRetryCount = webViewUrl => {
|
|
258
|
-
const raw = webViewUrl.searchParams.get(RETRY_QUERY_PARAM);
|
|
259
|
-
if (raw === null) return 0;
|
|
260
|
-
const parsed = Number(raw);
|
|
261
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
262
|
-
};
|
|
263
|
-
/**
|
|
264
|
-
* Snapshot returned by the phase-timer core when it hasn't been
|
|
265
|
-
* instantiated yet (briefly true between hook construction and the
|
|
266
|
-
* `useEffect` running). Lets `getSuccessMeta` / `getMeta` produce a
|
|
267
|
-
* well-formed empty meta in that window without special-casing each
|
|
268
|
-
* field.
|
|
269
|
-
*/
|
|
270
|
-
const EMPTY_CORE_SNAPSHOT = {
|
|
271
|
-
getCounters: () => ({
|
|
272
|
-
nativeErrorCount: 0,
|
|
273
|
-
osKillCount: 0
|
|
274
|
-
}),
|
|
275
|
-
getTimings: () => ({
|
|
276
|
-
htmlLoadStartedAt: null,
|
|
277
|
-
htmlLoadedAt: null,
|
|
278
|
-
manifestReceivedAt: null,
|
|
279
|
-
onLoadEndAt: null,
|
|
280
|
-
sdkReadyAt: null
|
|
281
|
-
})
|
|
282
|
-
};
|
|
283
|
-
/**
|
|
284
|
-
* Tracks how long each step of the WebView load took, so when we raise
|
|
285
|
-
* `WebViewFailedToLoadError` we can attach the timings to the error log.
|
|
286
|
-
*
|
|
287
|
-
* The timings cover the host-side phases of the load — the webview-side
|
|
288
|
-
* timings (`webview.time_to_load_manifest`, `webview.time_to_sdk_ready`)
|
|
289
|
-
* still come from the webview itself once it boots, but those don't help
|
|
290
|
-
* when the webview never boots in the first place.
|
|
291
|
-
*
|
|
292
|
-
* The actual state machine lives in {@link createWebViewPhaseTimerCore},
|
|
293
|
-
* which is also reused by the embedded native WebView path. This hook
|
|
294
|
-
* only layers the React-specific bits (ref-based core lifetime tied to
|
|
295
|
-
* `useEffect`, URL-derived `hadClearState` / `retryCount`).
|
|
296
|
-
*/
|
|
297
|
-
const useWebViewPhaseTimers = ({
|
|
298
|
-
core,
|
|
299
|
-
loadingTimeout,
|
|
300
|
-
recoveryTimeout,
|
|
301
|
-
webViewUrl
|
|
302
|
-
}) => {
|
|
303
|
-
const phaseCoreRef = react.useRef(null);
|
|
304
|
-
const webViewUrlRef = react.useRef(webViewUrl);
|
|
305
|
-
webViewUrlRef.current = webViewUrl;
|
|
306
|
-
/**
|
|
307
|
-
* Owning the message-transport subscription in `useEffect` matches the
|
|
308
|
-
* lifetime semantics any other React subscription would have, and lets
|
|
309
|
-
* us cleanly re-subscribe if `core` is ever swapped at runtime.
|
|
310
|
-
*/
|
|
311
|
-
react.useEffect(() => {
|
|
312
|
-
const phaseCore = createWebViewPhaseTimerCore({
|
|
313
|
-
core
|
|
314
|
-
});
|
|
315
|
-
phaseCoreRef.current = phaseCore;
|
|
316
|
-
return () => {
|
|
317
|
-
phaseCore.dispose();
|
|
318
|
-
if (phaseCoreRef.current === phaseCore) {
|
|
319
|
-
phaseCoreRef.current = null;
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
}, [core]);
|
|
323
|
-
const recordEvent = react.useCallback(event => {
|
|
324
|
-
var _a;
|
|
325
|
-
(_a = phaseCoreRef.current) === null || _a === void 0 ? void 0 : _a.recordEvent(event);
|
|
326
|
-
}, []);
|
|
327
|
-
const getSuccessMeta = react.useCallback(() => {
|
|
328
|
-
var _a;
|
|
329
|
-
const url = webViewUrlRef.current;
|
|
330
|
-
return buildSuccessMeta({
|
|
331
|
-
core: (_a = phaseCoreRef.current) !== null && _a !== void 0 ? _a : EMPTY_CORE_SNAPSHOT,
|
|
332
|
-
hadClearState: hasClearStateInUrl(url),
|
|
333
|
-
loadingTimeoutMs: loadingTimeout,
|
|
334
|
-
recoveryTimeoutMs: recoveryTimeout,
|
|
335
|
-
retryCount: readRetryCount(url),
|
|
336
|
-
webviewUrl: url.toString()
|
|
337
|
-
});
|
|
338
|
-
}, [loadingTimeout, recoveryTimeout]);
|
|
339
|
-
const getMeta = react.useCallback(({
|
|
340
|
-
phase
|
|
341
|
-
}) => Object.assign(Object.assign({}, getSuccessMeta()), {
|
|
342
|
-
phase
|
|
343
|
-
}), [getSuccessMeta]);
|
|
149
|
+
const useWebViewVisibility = core => {
|
|
150
|
+
const webViewVisibilityRequestChannelRef = react.useRef(messageTransport.createRequestChannel(core.messageTransport));
|
|
151
|
+
const [visible, setVisible] = react.useState(false);
|
|
152
|
+
react.useEffect(() => webViewVisibilityRequestChannelRef.current.handle('setVisibility', setVisible), [setVisible]);
|
|
344
153
|
return {
|
|
345
|
-
|
|
346
|
-
getSuccessMeta,
|
|
347
|
-
recordEvent
|
|
154
|
+
visible
|
|
348
155
|
};
|
|
349
156
|
};
|
|
350
157
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
377
|
-
var e = new Error(message);
|
|
378
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
const SUCCESS_LOG_KEY = 'webview.load_succeeded';
|
|
382
|
-
/**
|
|
383
|
-
* Shared emission pattern for the `webview.load_succeeded`
|
|
384
|
-
* instrumentation log. Both the RN `<WebView>` and the embedded
|
|
385
|
-
* WebView fire the same event with the same payload shape after
|
|
386
|
-
* checking the same cooldown gate — this helper centralises that
|
|
387
|
-
* sequence so the two call sites stay aligned and any future field we
|
|
388
|
-
* add to the success log lands in both places automatically.
|
|
389
|
-
*/
|
|
390
|
-
const emitSuccessLog = _a => __awaiter(void 0, [_a], void 0, function* ({
|
|
391
|
-
cooldown,
|
|
392
|
-
core,
|
|
393
|
-
getMeta,
|
|
394
|
-
name
|
|
395
|
-
}) {
|
|
396
|
-
if (!(yield cooldown.shouldEmit())) return;
|
|
397
|
-
logger.instrument(`${name} loaded successfully`, Object.assign({
|
|
398
|
-
environmentId: core.environmentId,
|
|
399
|
-
hostSdkSessionId: core.hostSdkSessionId,
|
|
400
|
-
key: SUCCESS_LOG_KEY,
|
|
401
|
-
time: 0
|
|
402
|
-
}, getMeta()));
|
|
403
|
-
yield cooldown.recordEmitted();
|
|
158
|
+
const styles = reactNative.StyleSheet.create({
|
|
159
|
+
container: {
|
|
160
|
+
backgroundColor: 'transparent',
|
|
161
|
+
bottom: 0,
|
|
162
|
+
flex: 1,
|
|
163
|
+
left: 0,
|
|
164
|
+
position: 'absolute',
|
|
165
|
+
right: 0,
|
|
166
|
+
top: 0,
|
|
167
|
+
width: '100%'
|
|
168
|
+
},
|
|
169
|
+
hide: {
|
|
170
|
+
elevation: 0,
|
|
171
|
+
opacity: 0,
|
|
172
|
+
zIndex: -10000
|
|
173
|
+
},
|
|
174
|
+
show: {
|
|
175
|
+
elevation: 10000,
|
|
176
|
+
opacity: 1,
|
|
177
|
+
zIndex: 10000
|
|
178
|
+
},
|
|
179
|
+
webview: {
|
|
180
|
+
backgroundColor: 'transparent',
|
|
181
|
+
flex: 1
|
|
182
|
+
}
|
|
404
183
|
});
|
|
405
184
|
|
|
406
185
|
const SUCCESS_LOG_COOLDOWN_MS = 60 * 60 * 1000;
|
|
@@ -455,225 +234,6 @@ const createSuccessLogCooldown = ({
|
|
|
455
234
|
})
|
|
456
235
|
});
|
|
457
236
|
|
|
458
|
-
const SUCCESS_LOG_COOLDOWN_STORAGE_KEY = 'dynamic.webview.successLogLastEmittedAt';
|
|
459
|
-
/**
|
|
460
|
-
* Emit a `webview.load_succeeded` instrumentation log the first time
|
|
461
|
-
* the SDK signals it's ready, gated by a device-local 1h cooldown so a
|
|
462
|
-
* customer with many WebView mounts (or many app launches) doesn't spam
|
|
463
|
-
* the logs pipeline.
|
|
464
|
-
*
|
|
465
|
-
* The error path is unaffected: failures keep going through
|
|
466
|
-
* `logger.error(WebViewFailedToLoadError, meta)` with no cooldown.
|
|
467
|
-
*
|
|
468
|
-
* The hook intentionally fires only once per mount (`hasEmittedRef`):
|
|
469
|
-
* after a successful boot we don't expect another `sdkHasLoaded` for
|
|
470
|
-
* the same WebView lifetime, and emitting twice for the same load would
|
|
471
|
-
* be misleading.
|
|
472
|
-
*
|
|
473
|
-
* Cooldown gating + log emission are delegated to the shared
|
|
474
|
-
* `successLog` helpers so this hook and the embedded native WebView
|
|
475
|
-
* path stay in lockstep on payload shape and storage semantics; the
|
|
476
|
-
* dedicated storage key here keeps the RN-path cooldown independent
|
|
477
|
-
* from the embedded path's.
|
|
478
|
-
*/
|
|
479
|
-
const useWebViewSuccessLog = ({
|
|
480
|
-
core,
|
|
481
|
-
getSuccessMeta
|
|
482
|
-
}) => {
|
|
483
|
-
const hasEmittedRef = react.useRef(false);
|
|
484
|
-
const getSuccessMetaRef = react.useRef(getSuccessMeta);
|
|
485
|
-
getSuccessMetaRef.current = getSuccessMeta;
|
|
486
|
-
react.useEffect(() => {
|
|
487
|
-
const cooldown = createSuccessLogCooldown({
|
|
488
|
-
name: 'webview',
|
|
489
|
-
storageKey: SUCCESS_LOG_COOLDOWN_STORAGE_KEY
|
|
490
|
-
});
|
|
491
|
-
const handler = message => __awaiter(void 0, void 0, void 0, function* () {
|
|
492
|
-
if (message.origin !== 'webview') return;
|
|
493
|
-
if (message.type !== webviewMessages.sdkHasLoadedEventName) return;
|
|
494
|
-
if (hasEmittedRef.current) return;
|
|
495
|
-
hasEmittedRef.current = true;
|
|
496
|
-
try {
|
|
497
|
-
yield emitSuccessLog({
|
|
498
|
-
cooldown,
|
|
499
|
-
core,
|
|
500
|
-
getMeta: getSuccessMetaRef.current,
|
|
501
|
-
name: 'Webview'
|
|
502
|
-
});
|
|
503
|
-
} catch (err) {
|
|
504
|
-
logger.warn('useWebViewSuccessLog.emitSuccessLog failed', {
|
|
505
|
-
error: String(err)
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
core.messageTransport.on(handler);
|
|
510
|
-
return () => {
|
|
511
|
-
core.messageTransport.off(handler);
|
|
512
|
-
};
|
|
513
|
-
}, [core]);
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
const useWebViewVisibility = core => {
|
|
517
|
-
const webViewVisibilityRequestChannelRef = react.useRef(messageTransport.createRequestChannel(core.messageTransport));
|
|
518
|
-
const [visible, setVisible] = react.useState(false);
|
|
519
|
-
react.useEffect(() => webViewVisibilityRequestChannelRef.current.handle('setVisibility', setVisible), [setVisible]);
|
|
520
|
-
return {
|
|
521
|
-
visible
|
|
522
|
-
};
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
const styles = reactNative.StyleSheet.create({
|
|
526
|
-
container: {
|
|
527
|
-
backgroundColor: 'transparent',
|
|
528
|
-
bottom: 0,
|
|
529
|
-
flex: 1,
|
|
530
|
-
left: 0,
|
|
531
|
-
position: 'absolute',
|
|
532
|
-
right: 0,
|
|
533
|
-
top: 0,
|
|
534
|
-
width: '100%'
|
|
535
|
-
},
|
|
536
|
-
hide: {
|
|
537
|
-
elevation: 0,
|
|
538
|
-
opacity: 0,
|
|
539
|
-
zIndex: -10000
|
|
540
|
-
},
|
|
541
|
-
show: {
|
|
542
|
-
elevation: 10000,
|
|
543
|
-
opacity: 1,
|
|
544
|
-
zIndex: 10000
|
|
545
|
-
},
|
|
546
|
-
webview: {
|
|
547
|
-
backgroundColor: 'transparent',
|
|
548
|
-
flex: 1
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
const setClearStateToUrl = webViewUrl => {
|
|
553
|
-
const newWebViewUrl = new URL(webViewUrl);
|
|
554
|
-
newWebViewUrl.searchParams.set(CLEAR_STATE_PARAM, 'true');
|
|
555
|
-
newWebViewUrl.searchParams.delete('retry');
|
|
556
|
-
return newWebViewUrl;
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
const useWebViewRecoveryTimeout = ({
|
|
560
|
-
core,
|
|
561
|
-
webViewUrl,
|
|
562
|
-
disableRecovery,
|
|
563
|
-
recoveryTimeout,
|
|
564
|
-
setWebViewUrl,
|
|
565
|
-
onFailedToLoadAfterClearState: _onFailedToLoadAfterClearState = () => {}
|
|
566
|
-
}) => {
|
|
567
|
-
const timeoutRef = react.useRef(null);
|
|
568
|
-
const onFailedToLoadAfterClearStateRef = react.useRef(_onFailedToLoadAfterClearState);
|
|
569
|
-
onFailedToLoadAfterClearStateRef.current = _onFailedToLoadAfterClearState;
|
|
570
|
-
/**
|
|
571
|
-
* Clear timeout on unmount
|
|
572
|
-
*/
|
|
573
|
-
react.useEffect(() => () => {
|
|
574
|
-
if (timeoutRef.current) {
|
|
575
|
-
clearTimeout(timeoutRef.current);
|
|
576
|
-
timeoutRef.current = null;
|
|
577
|
-
}
|
|
578
|
-
}, []);
|
|
579
|
-
return react.useCallback(() => {
|
|
580
|
-
if (disableRecovery) {
|
|
581
|
-
logger.debug('timeout recovery: is disabled');
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
if (hasClearStateInUrl(webViewUrl)) {
|
|
585
|
-
logger.instrument('Webview failed to load after clearing its state', {
|
|
586
|
-
environmentId: core.environmentId,
|
|
587
|
-
hostSdkSessionId: core.hostSdkSessionId,
|
|
588
|
-
key: 'webview.failed_to_load_after_clear_state',
|
|
589
|
-
time: 0,
|
|
590
|
-
webviewUrl: webViewUrl.toString()
|
|
591
|
-
});
|
|
592
|
-
onFailedToLoadAfterClearStateRef.current();
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
logger.debug('timeout recovery: registering timeout', webViewUrl.toString());
|
|
596
|
-
if (timeoutRef.current) {
|
|
597
|
-
clearTimeout(timeoutRef.current);
|
|
598
|
-
}
|
|
599
|
-
timeoutRef.current = setTimeout(() => {
|
|
600
|
-
if (!core.messageTransport.isBlocked()) {
|
|
601
|
-
logger.instrument('Webview recovery skipped because the webview did load', {
|
|
602
|
-
environmentId: core.environmentId,
|
|
603
|
-
hostSdkSessionId: core.hostSdkSessionId,
|
|
604
|
-
key: 'webview.recovery_skipped',
|
|
605
|
-
time: 0,
|
|
606
|
-
webviewUrl: webViewUrl.toString()
|
|
607
|
-
});
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
core.messageTransport.block();
|
|
611
|
-
// Increase the retry
|
|
612
|
-
setWebViewUrl(webViewUrl => {
|
|
613
|
-
const newWebViewUrl = setClearStateToUrl(webViewUrl);
|
|
614
|
-
logger.instrument('timeout recovery: reloading webview to clear its state', {
|
|
615
|
-
environmentId: core.environmentId,
|
|
616
|
-
hostSdkSessionId: core.hostSdkSessionId,
|
|
617
|
-
key: 'webview.recovery_by_clear_state',
|
|
618
|
-
time: 0,
|
|
619
|
-
webviewUrl: webViewUrl.toString()
|
|
620
|
-
});
|
|
621
|
-
return newWebViewUrl;
|
|
622
|
-
});
|
|
623
|
-
}, recoveryTimeout);
|
|
624
|
-
}, [core, disableRecovery, recoveryTimeout, setWebViewUrl, webViewUrl]);
|
|
625
|
-
};
|
|
626
|
-
|
|
627
|
-
const increaseRetryToUrl = webViewUrl => {
|
|
628
|
-
var _a;
|
|
629
|
-
const currentRetry = Number((_a = webViewUrl.searchParams.get('retry')) !== null && _a !== void 0 ? _a : '0');
|
|
630
|
-
const newWebViewUrl = new URL(webViewUrl);
|
|
631
|
-
newWebViewUrl.searchParams.delete(CLEAR_STATE_PARAM);
|
|
632
|
-
newWebViewUrl.searchParams.set('retry', String(currentRetry + 1));
|
|
633
|
-
return newWebViewUrl;
|
|
634
|
-
};
|
|
635
|
-
|
|
636
|
-
const useWebViewLoadingTimeout = (timeout, onTimeout) => {
|
|
637
|
-
const timeoutRef = react.useRef(null);
|
|
638
|
-
const onTimeoutRef = react.useRef(onTimeout);
|
|
639
|
-
onTimeoutRef.current = onTimeout;
|
|
640
|
-
const onLoadStart = react.useCallback(() => {
|
|
641
|
-
if (timeoutRef.current) {
|
|
642
|
-
clearTimeout(timeoutRef.current);
|
|
643
|
-
}
|
|
644
|
-
timeoutRef.current = setTimeout(() => {
|
|
645
|
-
onTimeoutRef.current();
|
|
646
|
-
}, timeout);
|
|
647
|
-
}, [timeout]);
|
|
648
|
-
const onLoad = react.useCallback(() => {
|
|
649
|
-
if (timeoutRef.current) {
|
|
650
|
-
clearTimeout(timeoutRef.current);
|
|
651
|
-
timeoutRef.current = null;
|
|
652
|
-
}
|
|
653
|
-
}, []);
|
|
654
|
-
react.useEffect(() => () => {
|
|
655
|
-
if (timeoutRef.current) {
|
|
656
|
-
clearTimeout(timeoutRef.current);
|
|
657
|
-
timeoutRef.current = null;
|
|
658
|
-
}
|
|
659
|
-
}, []);
|
|
660
|
-
return {
|
|
661
|
-
onLoad,
|
|
662
|
-
onLoadStart
|
|
663
|
-
};
|
|
664
|
-
};
|
|
665
|
-
|
|
666
|
-
const assignEnvironmentIdToUrl = (url, environmentId) => {
|
|
667
|
-
const _url = new URL(url);
|
|
668
|
-
_url.searchParams.set(ENVIRONMENT_ID_PARAM, environmentId);
|
|
669
|
-
return _url;
|
|
670
|
-
};
|
|
671
|
-
|
|
672
|
-
const assignStartTimeToUrl = url => {
|
|
673
|
-
url.searchParams.set(WEBVIEW_START_TIME_PARAM, Date.now().toString());
|
|
674
|
-
return url;
|
|
675
|
-
};
|
|
676
|
-
|
|
677
237
|
const waasOrigins = ['https://app.dynamic-preprod.xyz', 'https://app.dynamicauth.com'];
|
|
678
238
|
const turnkeyOrigins = ['https://recovery.turnkey.com', 'https://export.turnkey.com'];
|
|
679
239
|
const isNonProductionBuild = () => process.env['NODE_ENV'] !== 'production';
|
|
@@ -708,158 +268,153 @@ const shouldAllowNavigation = (request, webViewUrl) => {
|
|
|
708
268
|
return false;
|
|
709
269
|
};
|
|
710
270
|
|
|
711
|
-
const
|
|
712
|
-
|
|
271
|
+
const REACT_NATIVE_WEBVIEW_TYPE = 'react_native_webview';
|
|
272
|
+
// react-native-webview's `onError` only carries a `domain` on iOS/macOS.
|
|
273
|
+
// When it is absent (Android) this stable literal stands in so
|
|
274
|
+
// `classifyWebViewLoadError` still receives a domain and classifies the
|
|
275
|
+
// failure by its code alone (unknown domains default to transient).
|
|
276
|
+
const REACT_NATIVE_WEBVIEW_LOAD_ERROR_DOMAIN = 'ReactNativeWebViewLoadError';
|
|
277
|
+
const SUCCESS_LOG_COOLDOWN_STORAGE_KEY = 'dynamic.webview.successLogLastEmittedAt';
|
|
278
|
+
// All load, timeout, retry, telemetry, and terminal-error logic is owned by
|
|
279
|
+
// the webview load controller (`createWebViewLoadController`). This
|
|
280
|
+
// component only owns the platform bindings: the react-native-webview
|
|
281
|
+
// element, its lifecycle-event mapping, the message bridge, visibility
|
|
282
|
+
// handling, and the navigation allowlist.
|
|
713
283
|
const WebView = ({
|
|
714
|
-
webviewUrl
|
|
284
|
+
webviewUrl,
|
|
715
285
|
core,
|
|
716
286
|
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
287
|
+
recoveryTimeout,
|
|
288
|
+
loadingTimeout,
|
|
289
|
+
webviewLoad
|
|
720
290
|
}) => {
|
|
721
291
|
const webViewRef = react.useRef(null);
|
|
722
292
|
const {
|
|
723
293
|
visible
|
|
724
294
|
} = useWebViewVisibility(core);
|
|
725
|
-
|
|
295
|
+
// The URL of the load attempt currently in flight. The controller owns
|
|
296
|
+
// URL construction (environmentId, start time, retry counter); loading a
|
|
297
|
+
// new URL remounts the webview via `key`, which is how reloads work on
|
|
298
|
+
// this path. `null` until the controller issues the first load.
|
|
299
|
+
const [currentAttemptUrl, setCurrentAttemptUrl] = react.useState(null);
|
|
300
|
+
const controllerRef = react.useRef(null);
|
|
726
301
|
const {
|
|
727
302
|
onMessageHandler
|
|
728
303
|
} = useMessageTransportWebViewBridge(core, webViewRef);
|
|
729
|
-
const {
|
|
730
|
-
recordEvent,
|
|
731
|
-
getMeta,
|
|
732
|
-
getSuccessMeta
|
|
733
|
-
} = useWebViewPhaseTimers({
|
|
734
|
-
core,
|
|
735
|
-
loadingTimeout: _loadingTimeout,
|
|
736
|
-
recoveryTimeout: _recoveryTimeout,
|
|
737
|
-
webViewUrl
|
|
738
|
-
});
|
|
739
|
-
useWebViewSuccessLog({
|
|
740
|
-
core,
|
|
741
|
-
getSuccessMeta
|
|
742
|
-
});
|
|
743
|
-
const containerStyles = [styles['container'], visible ? styles.show : styles.hide];
|
|
744
304
|
/**
|
|
745
|
-
*
|
|
305
|
+
* Owns the load-controller lifecycle: created once per mount, torn down
|
|
306
|
+
* on unmount (keeping the unmount warning + transport block so a stray
|
|
307
|
+
* unmount doesn't leave host messages hitting a dead page).
|
|
746
308
|
*/
|
|
747
|
-
react.useEffect(() =>
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
* Should not reload the webview if it is already blocked
|
|
756
|
-
* and loading a new page
|
|
757
|
-
*/
|
|
758
|
-
if (core.messageTransport.isBlocked()) {
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
/**
|
|
762
|
-
* iOS and Android both may trigger this event when the webview
|
|
763
|
-
* is removed due to memory pressure.
|
|
764
|
-
*
|
|
765
|
-
* When the webview is killed by the OS, the message transport must
|
|
766
|
-
* be blocked to prevent the "No handlers registered" error while
|
|
767
|
-
* the webview is reloading
|
|
768
|
-
*/
|
|
769
|
-
core.messageTransport.block();
|
|
770
|
-
setWebViewUrl(webViewUrl => {
|
|
771
|
-
const newWebViewUrl = increaseRetryToUrl(webViewUrl);
|
|
772
|
-
logger.instrument('Reloading webview', {
|
|
773
|
-
environmentId: core.environmentId,
|
|
774
|
-
hostSdkSessionId: core.hostSdkSessionId,
|
|
775
|
-
key: 'webview.reloading',
|
|
776
|
-
newWebViewUrl,
|
|
777
|
-
reason,
|
|
778
|
-
time: 0,
|
|
779
|
-
webviewUrl: webViewUrl.toString()
|
|
780
|
-
});
|
|
781
|
-
return newWebViewUrl;
|
|
309
|
+
react.useEffect(() => {
|
|
310
|
+
const adapter = {
|
|
311
|
+
load: url => setCurrentAttemptUrl(url),
|
|
312
|
+
webviewType: REACT_NATIVE_WEBVIEW_TYPE
|
|
313
|
+
};
|
|
314
|
+
const successLogCooldown = createSuccessLogCooldown({
|
|
315
|
+
name: 'webview',
|
|
316
|
+
storageKey: SUCCESS_LOG_COOLDOWN_STORAGE_KEY
|
|
782
317
|
});
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
318
|
+
const controller = client.createWebViewLoadController(Object.assign(Object.assign(Object.assign(Object.assign({
|
|
319
|
+
adapter,
|
|
320
|
+
core
|
|
321
|
+
}, webviewLoad), loadingTimeout !== undefined && {
|
|
322
|
+
pageLoadTimeoutMs: loadingTimeout
|
|
323
|
+
}), recoveryTimeout !== undefined && {
|
|
324
|
+
sdkReadyTimeoutMs: recoveryTimeout
|
|
325
|
+
}), {
|
|
326
|
+
// The controller exposes no post-emit hook, so the cooldown is
|
|
327
|
+
// recorded as soon as the gate passes — before the log leaves the
|
|
328
|
+
// device. Worst case (a teardown between gate and emit) one log is
|
|
329
|
+
// suppressed, which beats replaying it after a crash.
|
|
330
|
+
shouldEmitSuccessLog: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
331
|
+
if (!(yield successLogCooldown.shouldEmit())) return false;
|
|
332
|
+
yield successLogCooldown.recordEmitted();
|
|
333
|
+
return true;
|
|
334
|
+
}),
|
|
335
|
+
webviewUrl
|
|
794
336
|
}));
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
337
|
+
controllerRef.current = controller;
|
|
338
|
+
// Hands the controller to the client core, which wires the
|
|
339
|
+
// customer-facing `client.sdk.loadState` / `client.sdk.retry()`
|
|
340
|
+
// surface to it.
|
|
341
|
+
core.registerWebViewLoadController(controller);
|
|
342
|
+
controller.start();
|
|
343
|
+
return () => {
|
|
344
|
+
logger.warn(unmountWebViewWarningMessage);
|
|
345
|
+
controller.teardown();
|
|
346
|
+
controllerRef.current = null;
|
|
347
|
+
core.messageTransport.block();
|
|
348
|
+
};
|
|
349
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- the controller lifecycle is strictly mount/unmount
|
|
350
|
+
}, []);
|
|
351
|
+
const dispatch = event => {
|
|
352
|
+
var _a;
|
|
353
|
+
return (_a = controllerRef.current) === null || _a === void 0 ? void 0 : _a.dispatch(event);
|
|
354
|
+
};
|
|
355
|
+
const onError = ({
|
|
356
|
+
nativeEvent
|
|
357
|
+
}) => {
|
|
358
|
+
var _a;
|
|
359
|
+
return dispatch({
|
|
360
|
+
code: nativeEvent.code,
|
|
361
|
+
description: nativeEvent.description,
|
|
362
|
+
domain: (_a = nativeEvent.domain) !== null && _a !== void 0 ? _a : REACT_NATIVE_WEBVIEW_LOAD_ERROR_DOMAIN,
|
|
363
|
+
failedUrl: nativeEvent.url,
|
|
364
|
+
type: 'native_error'
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
const onHttpError = ({
|
|
368
|
+
nativeEvent
|
|
369
|
+
}) => dispatch({
|
|
370
|
+
code: nativeEvent.statusCode,
|
|
371
|
+
description: nativeEvent.description,
|
|
372
|
+
domain: client.EMBEDDED_WEBVIEW_HTTP_ERROR_DOMAIN,
|
|
373
|
+
failedUrl: nativeEvent.url,
|
|
374
|
+
type: 'native_error'
|
|
375
|
+
});
|
|
801
376
|
/**
|
|
802
|
-
*
|
|
803
|
-
*
|
|
377
|
+
* iOS and Android both may kill the webview content process under memory
|
|
378
|
+
* pressure. The controller routes the signal through the bounded
|
|
379
|
+
* backed-off reload cycle, blocking the message transport while the
|
|
380
|
+
* reload is in flight (the machine handles it both mid-load and
|
|
381
|
+
* post-ready).
|
|
804
382
|
*/
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
disableRecovery: _disableRecovery,
|
|
808
|
-
onFailedToLoadAfterClearState: setWebViewLoadErrorWithPhase('after_clear_state'),
|
|
809
|
-
recoveryTimeout: _recoveryTimeout,
|
|
810
|
-
setWebViewUrl,
|
|
811
|
-
webViewUrl
|
|
383
|
+
const onProcessTerminated = () => dispatch({
|
|
384
|
+
type: 'recovery_requested'
|
|
812
385
|
});
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
recordEvent('native_error');
|
|
817
|
-
/**
|
|
818
|
-
* This is the first attempt to load the webview, do not throw an error
|
|
819
|
-
* because the recovery system will attempt to reload the webview
|
|
820
|
-
*/
|
|
821
|
-
if (webViewLoadErrorCountRef.current === 1) return;
|
|
822
|
-
setWebViewLoadError('native_error');
|
|
823
|
-
}, [recordEvent, setWebViewLoadError]);
|
|
824
|
-
const {
|
|
825
|
-
onLoad: onLoadFromTimeout,
|
|
826
|
-
onLoadStart: onLoadStartFromTimeout
|
|
827
|
-
} = useWebViewLoadingTimeout(_loadingTimeout, setWebViewLoadErrorWithPhase('html_load'));
|
|
828
|
-
const onLoadStart = react.useCallback(() => {
|
|
829
|
-
recordEvent('load_start');
|
|
830
|
-
onLoadStartFromTimeout();
|
|
831
|
-
}, [onLoadStartFromTimeout, recordEvent]);
|
|
832
|
-
const onLoad = react.useCallback(() => {
|
|
833
|
-
recordEvent('load');
|
|
834
|
-
onLoadFromTimeout();
|
|
835
|
-
}, [onLoadFromTimeout, recordEvent]);
|
|
836
|
-
const onLoadEnd = react.useCallback(() => {
|
|
837
|
-
recordEvent('load_end');
|
|
838
|
-
startRecoveryTimeout();
|
|
839
|
-
}, [recordEvent, startRecoveryTimeout]);
|
|
840
|
-
const webViewUrlString = react.useMemo(() => webViewUrl.toString(), [webViewUrl]);
|
|
386
|
+
const containerStyles = [styles['container'], visible ? styles.show : styles.hide];
|
|
387
|
+
if (currentAttemptUrl === null) return null;
|
|
388
|
+
const currentAttemptUrlString = currentAttemptUrl.toString();
|
|
841
389
|
return /*#__PURE__*/jsxRuntime.jsx(reactNativeWebview.WebView, {
|
|
842
390
|
ref: webViewRef,
|
|
843
391
|
source: {
|
|
844
|
-
uri:
|
|
392
|
+
uri: currentAttemptUrlString
|
|
845
393
|
},
|
|
846
394
|
containerStyle: containerStyles,
|
|
847
395
|
style: styles['webview'],
|
|
848
396
|
onMessage: onMessageHandler,
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
397
|
+
onLoadStart: () => dispatch({
|
|
398
|
+
type: 'load_started'
|
|
399
|
+
}),
|
|
400
|
+
onLoad: () => dispatch({
|
|
401
|
+
type: 'response_committed'
|
|
402
|
+
}),
|
|
403
|
+
onLoadEnd: () => dispatch({
|
|
404
|
+
type: 'page_finished'
|
|
405
|
+
}),
|
|
852
406
|
hideKeyboardAccessoryView: true,
|
|
853
407
|
webviewDebuggingEnabled: _webviewDebuggingEnabled,
|
|
854
|
-
onContentProcessDidTerminate:
|
|
855
|
-
onRenderProcessGone:
|
|
856
|
-
onError:
|
|
408
|
+
onContentProcessDidTerminate: onProcessTerminated,
|
|
409
|
+
onRenderProcessGone: onProcessTerminated,
|
|
410
|
+
onError: onError,
|
|
411
|
+
onHttpError: onHttpError,
|
|
857
412
|
setSupportMultipleWindows: false,
|
|
858
413
|
onShouldStartLoadWithRequest: request => shouldAllowNavigation({
|
|
859
414
|
isTopFrame: request.isTopFrame,
|
|
860
415
|
url: request.url
|
|
861
|
-
},
|
|
862
|
-
},
|
|
416
|
+
}, currentAttemptUrl)
|
|
417
|
+
}, currentAttemptUrlString);
|
|
863
418
|
};
|
|
864
419
|
const createWebView = internalProps => {
|
|
865
420
|
const WebViewWrapper = props => /*#__PURE__*/jsxRuntime.jsx(WebView, _extends({}, internalProps, props));
|
|
@@ -918,53 +473,10 @@ const getEmbeddedWebView = () => {
|
|
|
918
473
|
}
|
|
919
474
|
};
|
|
920
475
|
|
|
921
|
-
/**
|
|
922
|
-
* Non-React equivalent of `useWebViewPhaseTimers` for the embedded native
|
|
923
|
-
* webview path. The embedded path is a singleton wired by
|
|
924
|
-
* `setupEmbeddedWebView` (not a React component), so we expose the same
|
|
925
|
-
* phase-timing surface as a plain factory.
|
|
926
|
-
*
|
|
927
|
-
* Both this factory and the hook delegate the actual state machine to
|
|
928
|
-
* {@link createWebViewPhaseTimerCore}; the only path-specific bits are:
|
|
929
|
-
*
|
|
930
|
-
* - `hadClearState` is always `false` (embedded path has no clear-state
|
|
931
|
-
* recovery model — that's exclusive to the React `<WebView>` path's
|
|
932
|
-
* loading-timeout recovery flow)
|
|
933
|
-
* - `retryCount` is always `0` (embedded path has no retry counter URL
|
|
934
|
-
* query param either)
|
|
935
|
-
*/
|
|
936
|
-
const createEmbeddedWebViewPhaseTimers = ({
|
|
937
|
-
core,
|
|
938
|
-
loadingTimeoutMs,
|
|
939
|
-
recoveryTimeoutMs,
|
|
940
|
-
webViewUrl
|
|
941
|
-
}) => {
|
|
942
|
-
const phaseCore = createWebViewPhaseTimerCore({
|
|
943
|
-
core
|
|
944
|
-
});
|
|
945
|
-
const getSuccessMeta = () => buildSuccessMeta({
|
|
946
|
-
core: phaseCore,
|
|
947
|
-
hadClearState: false,
|
|
948
|
-
loadingTimeoutMs,
|
|
949
|
-
recoveryTimeoutMs,
|
|
950
|
-
retryCount: 0,
|
|
951
|
-
webviewUrl: webViewUrl.toString()
|
|
952
|
-
});
|
|
953
|
-
return {
|
|
954
|
-
dispose: phaseCore.dispose,
|
|
955
|
-
getMeta: ({
|
|
956
|
-
phase
|
|
957
|
-
}) => Object.assign(Object.assign({}, getSuccessMeta()), {
|
|
958
|
-
phase
|
|
959
|
-
}),
|
|
960
|
-
getSuccessMeta,
|
|
961
|
-
recordEvent: phaseCore.recordEvent
|
|
962
|
-
};
|
|
963
|
-
};
|
|
964
|
-
|
|
965
476
|
const EMBEDDED_SUCCESS_LOG_COOLDOWN_STORAGE_KEY = 'dynamic.embeddedWebView.successLogLastEmittedAt';
|
|
966
|
-
|
|
967
|
-
|
|
477
|
+
// The platform discriminator the load controller attaches to every
|
|
478
|
+
// consolidated `webview.load_*` telemetry event emitted for this path.
|
|
479
|
+
const EMBEDDED_WEBVIEW_TYPE = 'embedded_native';
|
|
968
480
|
// Debug log labels matching the react-native-webview bridge
|
|
969
481
|
// (`useMessageTransportWebViewBridge`) so both paths surface the same
|
|
970
482
|
// HOST/WEBVIEW message trace under `core.debug.messageTransport`.
|
|
@@ -975,6 +487,11 @@ const WEBVIEW_TO_HOST_LOG = 'WEBVIEW >>>> HOST';
|
|
|
975
487
|
// outside the React tree (in a dedicated UIWindow on iOS), so its setup runs
|
|
976
488
|
// once when the SDK is initialised — there's nothing to mount or unmount.
|
|
977
489
|
//
|
|
490
|
+
// All load, timeout, retry, telemetry, and terminal-error logic is owned by
|
|
491
|
+
// the webview load controller (`createWebViewLoadController`). This module
|
|
492
|
+
// only owns the platform bindings: the native-module wiring, the message
|
|
493
|
+
// bridge, visibility handling, and the navigation allowlist.
|
|
494
|
+
//
|
|
978
495
|
// Returns a teardown that removes every JS-side subscription. The native
|
|
979
496
|
// singleton is intentionally NOT destroyed — re-running setup with a fresh
|
|
980
497
|
// `core` (e.g. on client recreation with a new environmentId) just reloads
|
|
@@ -983,61 +500,48 @@ const setupEmbeddedWebView = ({
|
|
|
983
500
|
webviewUrl,
|
|
984
501
|
core,
|
|
985
502
|
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
986
|
-
|
|
987
|
-
recoveryTimeoutMs: _recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS
|
|
503
|
+
webviewLoad
|
|
988
504
|
}) => {
|
|
989
505
|
const native = getEmbeddedWebView();
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
506
|
+
// The URL of the load attempt currently in flight. The controller owns
|
|
507
|
+
// URL construction (environmentId, start time, retry counter), so the
|
|
508
|
+
// navigation allowlist below must compare against the URL the native
|
|
509
|
+
// webview was actually pointed at. Until the first `adapter.load` the
|
|
510
|
+
// base URL stands in — only its origin is ever compared.
|
|
511
|
+
let currentAttemptUrl = new URL(webviewUrl);
|
|
512
|
+
const adapter = {
|
|
513
|
+
load: url => {
|
|
514
|
+
var _a;
|
|
515
|
+
currentAttemptUrl = url;
|
|
516
|
+
logger.debug(`[EmbeddedWebView] calling setUrl: ${url.toString()}`);
|
|
517
|
+
(_a = native.setUrl(url.toString())) === null || _a === void 0 ? void 0 : _a.catch(err => {
|
|
518
|
+
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
519
|
+
});
|
|
520
|
+
},
|
|
521
|
+
webviewType: EMBEDDED_WEBVIEW_TYPE
|
|
522
|
+
};
|
|
997
523
|
const successLogCooldown = createSuccessLogCooldown({
|
|
998
524
|
name: 'embedded webview',
|
|
999
525
|
storageKey: EMBEDDED_SUCCESS_LOG_COOLDOWN_STORAGE_KEY
|
|
1000
526
|
});
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
// (e.g. recovery timeout firing after html_load already failed) are
|
|
1020
|
-
// suppressed so we don't spam the initialization-error setter with
|
|
1021
|
-
// duplicate logs for the same load attempt.
|
|
1022
|
-
const raiseFailure = (phase, extraMeta = {}) => {
|
|
1023
|
-
if (hasFailed) return;
|
|
1024
|
-
hasFailed = true;
|
|
1025
|
-
clearLoadingTimer();
|
|
1026
|
-
clearRecoveryTimer();
|
|
1027
|
-
logger.debug(`[EmbeddedWebView] raiseFailure phase=${phase}`);
|
|
1028
|
-
const meta = phaseTimers.getMeta({
|
|
1029
|
-
phase
|
|
1030
|
-
});
|
|
1031
|
-
logger.instrument('Embedded webview load error', Object.assign({
|
|
1032
|
-
environmentId: core.environmentId,
|
|
1033
|
-
hostSdkSessionId: core.hostSdkSessionId,
|
|
1034
|
-
key: 'webview.embedded.load_error',
|
|
1035
|
-
phase,
|
|
1036
|
-
time: 0,
|
|
1037
|
-
webviewUrl: builtUrl.toString()
|
|
1038
|
-
}, extraMeta));
|
|
1039
|
-
core.initialization.error = new WebViewFailedToLoadError(Object.assign(Object.assign({}, meta), extraMeta));
|
|
1040
|
-
};
|
|
527
|
+
const controller = client.createWebViewLoadController(Object.assign(Object.assign({
|
|
528
|
+
adapter,
|
|
529
|
+
core
|
|
530
|
+
}, webviewLoad), {
|
|
531
|
+
// The controller exposes no post-emit hook, so the cooldown is recorded
|
|
532
|
+
// as soon as the gate passes — before the log leaves the device. Worst
|
|
533
|
+
// case (a teardown between gate and emit) one log is suppressed, which
|
|
534
|
+
// beats replaying it after a crash.
|
|
535
|
+
shouldEmitSuccessLog: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
536
|
+
if (!(yield successLogCooldown.shouldEmit())) return false;
|
|
537
|
+
yield successLogCooldown.recordEmitted();
|
|
538
|
+
return true;
|
|
539
|
+
}),
|
|
540
|
+
webviewUrl
|
|
541
|
+
}));
|
|
542
|
+
// Hands the controller to the client core, which wires the customer-facing
|
|
543
|
+
// `client.sdk.loadState` / `client.sdk.retry()` surface to it.
|
|
544
|
+
core.registerWebViewLoadController(controller);
|
|
1041
545
|
const visibilityChannel = messageTransport.createRequestChannel(core.messageTransport);
|
|
1042
546
|
const removeVisibilityHandler = visibilityChannel.handle('setVisibility', visible => {
|
|
1043
547
|
native.setVisible(visible).catch(err => {
|
|
@@ -1055,31 +559,6 @@ const setupEmbeddedWebView = ({
|
|
|
1055
559
|
});
|
|
1056
560
|
};
|
|
1057
561
|
core.messageTransport.on(handleHostMessage);
|
|
1058
|
-
// Watch for the webview-side SDK ready signal so we can clear the
|
|
1059
|
-
// recovery timer and emit the success log. The phase-timer factory
|
|
1060
|
-
// also subscribes for its own bookkeeping — both subscriptions are
|
|
1061
|
-
// independent and idempotent. The factory subscribes first and so
|
|
1062
|
-
// sees `sdkReadyAt` before this handler runs, which means the meta
|
|
1063
|
-
// we emit below has the freshest `sdkReadyMs`. We only emit once
|
|
1064
|
-
// per setup; a second SDK-ready event for the same load wouldn't
|
|
1065
|
-
// represent a new successful boot.
|
|
1066
|
-
const handleSdkReady = message => {
|
|
1067
|
-
if (message.origin !== 'webview') return;
|
|
1068
|
-
if (message.type !== webviewMessages.sdkHasLoadedEventName) return;
|
|
1069
|
-
logger.debug('[EmbeddedWebView] SDK ready signal received');
|
|
1070
|
-
clearRecoveryTimer();
|
|
1071
|
-
if (hasEmittedSuccessLog || hasFailed) return;
|
|
1072
|
-
hasEmittedSuccessLog = true;
|
|
1073
|
-
emitSuccessLog({
|
|
1074
|
-
cooldown: successLogCooldown,
|
|
1075
|
-
core,
|
|
1076
|
-
getMeta: phaseTimers.getSuccessMeta,
|
|
1077
|
-
name: 'Embedded webview'
|
|
1078
|
-
}).catch(err => {
|
|
1079
|
-
logger.warn('EmbeddedWebView.emitSuccessLog failed', err);
|
|
1080
|
-
});
|
|
1081
|
-
};
|
|
1082
|
-
core.messageTransport.on(handleSdkReady);
|
|
1083
562
|
const onMessageSub = native.addListener('onMessage', event => {
|
|
1084
563
|
var _a;
|
|
1085
564
|
let parsed = null;
|
|
@@ -1101,7 +580,7 @@ const setupEmbeddedWebView = ({
|
|
|
1101
580
|
const allow = shouldAllowNavigation({
|
|
1102
581
|
isTopFrame: event.isTopFrame,
|
|
1103
582
|
url: event.url
|
|
1104
|
-
},
|
|
583
|
+
}, currentAttemptUrl);
|
|
1105
584
|
logger.debug(`[EmbeddedWebView] onShouldStartLoad id=${event.id} url=${event.url} isTopFrame=${event.isTopFrame} allow=${allow}`);
|
|
1106
585
|
native.respondToShouldStartLoad(event.id, allow).catch(err => {
|
|
1107
586
|
logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
|
|
@@ -1109,86 +588,57 @@ const setupEmbeddedWebView = ({
|
|
|
1109
588
|
});
|
|
1110
589
|
const onLoadStartSub = native.addListener('onLoadStart', () => {
|
|
1111
590
|
logger.debug('[EmbeddedWebView] onLoadStart');
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
// resets the html_load window cleanly without leaving a stale timer
|
|
1116
|
-
// around from the previous attempt.
|
|
1117
|
-
clearLoadingTimer();
|
|
1118
|
-
clearRecoveryTimer();
|
|
1119
|
-
loadingTimer = setTimeout(() => {
|
|
1120
|
-
raiseFailure('html_load');
|
|
1121
|
-
}, _loadingTimeoutMs);
|
|
591
|
+
controller.dispatch({
|
|
592
|
+
type: 'load_started'
|
|
593
|
+
});
|
|
1122
594
|
});
|
|
1123
595
|
const onLoadSub = native.addListener('onLoad', () => {
|
|
1124
596
|
logger.debug('[EmbeddedWebView] onLoad (response committed)');
|
|
1125
|
-
|
|
1126
|
-
|
|
597
|
+
controller.dispatch({
|
|
598
|
+
type: 'response_committed'
|
|
599
|
+
});
|
|
1127
600
|
});
|
|
1128
601
|
const onLoadEndSub = native.addListener('onLoadEnd', () => {
|
|
1129
602
|
logger.debug('[EmbeddedWebView] onLoadEnd (page finished)');
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
recoveryTimer = setTimeout(() => {
|
|
1134
|
-
raiseFailure('sdk_bootstrap');
|
|
1135
|
-
}, _recoveryTimeoutMs);
|
|
603
|
+
controller.dispatch({
|
|
604
|
+
type: 'page_finished'
|
|
605
|
+
});
|
|
1136
606
|
});
|
|
1137
607
|
const onLoadErrorSub = native.addListener('onLoadError', event => {
|
|
1138
608
|
logger.debug(`[EmbeddedWebView] onLoadError domain=${event.domain} code=${event.code} url=${event.url}`);
|
|
1139
|
-
phaseTimers.recordEvent('native_error');
|
|
1140
609
|
logger.warn('EmbeddedWebView load error', event);
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
610
|
+
controller.dispatch({
|
|
611
|
+
code: event.code,
|
|
612
|
+
description: event.description,
|
|
613
|
+
domain: event.domain,
|
|
614
|
+
failedUrl: event.url,
|
|
615
|
+
isProvisional: event.isProvisional,
|
|
616
|
+
type: 'native_error'
|
|
1147
617
|
});
|
|
1148
618
|
});
|
|
1149
619
|
native.setDebuggingEnabled(_webviewDebuggingEnabled).catch(err => {
|
|
1150
620
|
logger.warn('EmbeddedWebView.setDebuggingEnabled failed', err);
|
|
1151
621
|
});
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
// timer) never triggered. The onLoadStart handler clears and re-arms this
|
|
1157
|
-
// timer when it fires, so behaviour after onLoadStart is unchanged.
|
|
1158
|
-
logger.debug(`[EmbeddedWebView] arming loading timer (${_loadingTimeoutMs}ms) before setUrl`);
|
|
1159
|
-
loadingTimer = setTimeout(() => {
|
|
1160
|
-
logger.debug('[EmbeddedWebView] loading timer fired — raising html_load failure');
|
|
1161
|
-
raiseFailure('html_load');
|
|
1162
|
-
}, _loadingTimeoutMs);
|
|
1163
|
-
// Defer setUrl by one event-loop tick so the JS thread has fully settled
|
|
1164
|
-
// after listener registration. On cold boot the main thread is congested
|
|
1165
|
-
// with bundle evaluation — yielding here gives pending microtasks and
|
|
1166
|
-
// native bridge batches a chance to flush before the native side starts
|
|
622
|
+
// Defer the initial load by one event-loop tick so the JS thread has fully
|
|
623
|
+
// settled after listener registration. On cold boot the main thread is
|
|
624
|
+
// congested with bundle evaluation — yielding here gives pending microtasks
|
|
625
|
+
// and native bridge batches a chance to flush before the native side starts
|
|
1167
626
|
// firing navigation events that require a JS round-trip response.
|
|
1168
627
|
const warmupTimer = setTimeout(() => {
|
|
1169
|
-
|
|
1170
|
-
if (tornDown) return;
|
|
1171
|
-
logger.debug(`[EmbeddedWebView] calling setUrl: ${builtUrl.toString()}`);
|
|
1172
|
-
(_a = native.setUrl(builtUrl.toString())) === null || _a === void 0 ? void 0 : _a.catch(err => {
|
|
1173
|
-
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
1174
|
-
});
|
|
628
|
+
controller.start();
|
|
1175
629
|
}, 0);
|
|
1176
630
|
return () => {
|
|
1177
631
|
logger.debug('[EmbeddedWebView] teardown — removing listeners');
|
|
1178
|
-
tornDown = true;
|
|
1179
632
|
clearTimeout(warmupTimer);
|
|
1180
|
-
|
|
1181
|
-
clearRecoveryTimer();
|
|
633
|
+
controller.teardown();
|
|
1182
634
|
removeVisibilityHandler();
|
|
1183
635
|
core.messageTransport.off(handleHostMessage);
|
|
1184
|
-
core.messageTransport.off(handleSdkReady);
|
|
1185
636
|
onMessageSub.remove();
|
|
1186
637
|
onShouldStartLoadSub.remove();
|
|
1187
638
|
onLoadStartSub.remove();
|
|
1188
639
|
onLoadSub.remove();
|
|
1189
640
|
onLoadEndSub.remove();
|
|
1190
641
|
onLoadErrorSub.remove();
|
|
1191
|
-
phaseTimers.dispose();
|
|
1192
642
|
};
|
|
1193
643
|
};
|
|
1194
644
|
|
|
@@ -1532,7 +982,8 @@ const ReactNativeExtension = ({
|
|
|
1532
982
|
webviewUrl: _webviewUrl = defaultWebviewUrl,
|
|
1533
983
|
webviewDebuggingEnabled,
|
|
1534
984
|
appOrigin,
|
|
1535
|
-
embeddedWebView: _embeddedWebView = false
|
|
985
|
+
embeddedWebView: _embeddedWebView = false,
|
|
986
|
+
webviewLoad
|
|
1536
987
|
} = {}) => (_, core) => {
|
|
1537
988
|
const isPlatformSupportedByWebView = reactNative.Platform.OS === 'android' || reactNative.Platform.OS === 'ios';
|
|
1538
989
|
/**
|
|
@@ -1583,6 +1034,7 @@ const ReactNativeExtension = ({
|
|
|
1583
1034
|
previousEmbeddedWebViewTeardown = setupEmbeddedWebView({
|
|
1584
1035
|
core,
|
|
1585
1036
|
webviewDebuggingEnabled,
|
|
1037
|
+
webviewLoad,
|
|
1586
1038
|
webviewUrl: _webviewUrl
|
|
1587
1039
|
});
|
|
1588
1040
|
} catch (err) {
|
|
@@ -1603,6 +1055,7 @@ const ReactNativeExtension = ({
|
|
|
1603
1055
|
WebView: createWebView({
|
|
1604
1056
|
core,
|
|
1605
1057
|
webviewDebuggingEnabled,
|
|
1058
|
+
webviewLoad,
|
|
1606
1059
|
webviewUrl: _webviewUrl
|
|
1607
1060
|
})
|
|
1608
1061
|
}
|
|
@@ -1611,5 +1064,8 @@ const ReactNativeExtension = ({
|
|
|
1611
1064
|
|
|
1612
1065
|
assertPackageVersion.assertPackageVersion('@dynamic-labs/react-native-extension', version);
|
|
1613
1066
|
|
|
1067
|
+
Object.defineProperty(exports, 'WebViewFailedToLoadError', {
|
|
1068
|
+
enumerable: true,
|
|
1069
|
+
get: function () { return client.WebViewFailedToLoadError; }
|
|
1070
|
+
});
|
|
1614
1071
|
exports.ReactNativeExtension = ReactNativeExtension;
|
|
1615
|
-
exports.WebViewFailedToLoadError = WebViewFailedToLoadError;
|