@dynamic-labs/react-native-extension 4.83.0 → 4.83.2-alpha.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 +26 -0
- package/android/EmbeddedWebViewModule.kt +8 -1
- package/android/KeyStoreKeyManager.kt +7 -1
- package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/android/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/android/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/android/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/android/src/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/android/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/index.cjs +352 -23
- package/index.js +353 -24
- package/ios/EmbeddedWebViewController.swift +22 -0
- package/ios/EmbeddedWebViewModule.swift +8 -1
- package/package.json +6 -6
- package/src/components/WebView/EmbeddedWebView/EmbeddedWebView.d.ts +20 -1
- package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/embeddedWebViewPhaseTimers.d.ts +52 -0
- package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/index.d.ts +1 -0
- package/src/components/WebView/useWebViewPhaseTimers/index.d.ts +1 -0
- package/src/components/WebView/useWebViewPhaseTimers/useWebViewPhaseTimers.d.ts +45 -0
- package/src/errors/WebViewFailedToLoadError.d.ts +84 -1
- package/src/nativeModules/EmbeddedWebView.d.ts +27 -2
package/index.cjs
CHANGED
|
@@ -8,6 +8,7 @@ var react = require('react');
|
|
|
8
8
|
var reactNativeWebview = require('react-native-webview');
|
|
9
9
|
var logger$1 = require('@dynamic-labs/logger');
|
|
10
10
|
var messageTransport = require('@dynamic-labs/message-transport');
|
|
11
|
+
var webviewMessages = require('@dynamic-labs/webview-messages');
|
|
11
12
|
var jsxRuntime = require('react/jsx-runtime');
|
|
12
13
|
var expoModulesCore = require('expo-modules-core');
|
|
13
14
|
var reactNativePasskey = require('react-native-passkey');
|
|
@@ -34,7 +35,7 @@ function _interopNamespace(e) {
|
|
|
34
35
|
return Object.freeze(n);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
var version = "4.83.0";
|
|
38
|
+
var version = "4.83.2-alpha.0";
|
|
38
39
|
|
|
39
40
|
function _extends() {
|
|
40
41
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -47,8 +48,9 @@ function _extends() {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
class WebViewFailedToLoadError extends Error {
|
|
50
|
-
constructor() {
|
|
51
|
+
constructor(meta) {
|
|
51
52
|
super('Could not load Dynamic WebView');
|
|
53
|
+
this.meta = meta;
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -121,6 +123,122 @@ const useMessageTransportWebViewBridge = (core, webViewRef) => {
|
|
|
121
123
|
};
|
|
122
124
|
};
|
|
123
125
|
|
|
126
|
+
const CLEAR_STATE_PARAM = 'clear-state';
|
|
127
|
+
const ENVIRONMENT_ID_PARAM = 'environmentId';
|
|
128
|
+
const WEBVIEW_START_TIME_PARAM = 'webviewStartTime';
|
|
129
|
+
|
|
130
|
+
const hasClearStateInUrl = webViewUrl => Boolean(webViewUrl.searchParams.get(CLEAR_STATE_PARAM));
|
|
131
|
+
|
|
132
|
+
const MANIFEST_MESSAGE_TYPE$1 = 'manifest';
|
|
133
|
+
const RETRY_QUERY_PARAM = 'retry';
|
|
134
|
+
const emptyPerAttemptState$1 = () => ({
|
|
135
|
+
htmlLoadStartedAt: null,
|
|
136
|
+
htmlLoadedAt: null,
|
|
137
|
+
manifestReceivedAt: null,
|
|
138
|
+
onLoadEndAt: null,
|
|
139
|
+
sdkReadyAt: null
|
|
140
|
+
});
|
|
141
|
+
const diffMs$1 = (later, earlier) => later !== null && earlier !== null ? later - earlier : null;
|
|
142
|
+
const readRetryCount = webViewUrl => {
|
|
143
|
+
const raw = webViewUrl.searchParams.get(RETRY_QUERY_PARAM);
|
|
144
|
+
if (raw === null) return 0;
|
|
145
|
+
const parsed = Number(raw);
|
|
146
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Tracks how long each step of the WebView load took, so when we raise
|
|
150
|
+
* `WebViewFailedToLoadError` we can attach the timings to the error log.
|
|
151
|
+
*
|
|
152
|
+
* The timings cover the host-side phases of the load — the webview-side
|
|
153
|
+
* timings (`webview.time_to_load_manifest`, `webview.time_to_sdk_ready`)
|
|
154
|
+
* still come from the webview itself once it boots, but those don't help
|
|
155
|
+
* when the webview never boots in the first place.
|
|
156
|
+
*/
|
|
157
|
+
const useWebViewPhaseTimers = ({
|
|
158
|
+
core,
|
|
159
|
+
webViewUrl,
|
|
160
|
+
loadingTimeout,
|
|
161
|
+
recoveryTimeout
|
|
162
|
+
}) => {
|
|
163
|
+
const perAttemptRef = react.useRef(emptyPerAttemptState$1());
|
|
164
|
+
const nativeErrorCountRef = react.useRef(0);
|
|
165
|
+
const osKillCountRef = react.useRef(0);
|
|
166
|
+
const webViewUrlRef = react.useRef(webViewUrl);
|
|
167
|
+
webViewUrlRef.current = webViewUrl;
|
|
168
|
+
/**
|
|
169
|
+
* Listen on the message transport for the two webview-originated
|
|
170
|
+
* messages that mark "JS bundle alive" and "SDK ready".
|
|
171
|
+
*
|
|
172
|
+
* We only record the first occurrence per attempt so that a healthy
|
|
173
|
+
* reload doesn't overwrite the timing from the attempt that ultimately
|
|
174
|
+
* failed.
|
|
175
|
+
*/
|
|
176
|
+
react.useEffect(() => {
|
|
177
|
+
const handler = message => {
|
|
178
|
+
if (message.origin !== 'webview') return;
|
|
179
|
+
if (message.type === MANIFEST_MESSAGE_TYPE$1 && perAttemptRef.current.manifestReceivedAt === null) {
|
|
180
|
+
perAttemptRef.current.manifestReceivedAt = Date.now();
|
|
181
|
+
}
|
|
182
|
+
if (message.type === webviewMessages.sdkHasLoadedEventName && perAttemptRef.current.sdkReadyAt === null) {
|
|
183
|
+
perAttemptRef.current.sdkReadyAt = Date.now();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
core.messageTransport.on(handler);
|
|
187
|
+
return () => core.messageTransport.off(handler);
|
|
188
|
+
}, [core]);
|
|
189
|
+
const recordEvent = react.useCallback(event => {
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
switch (event) {
|
|
192
|
+
case 'load_start':
|
|
193
|
+
perAttemptRef.current = emptyPerAttemptState$1();
|
|
194
|
+
perAttemptRef.current.htmlLoadStartedAt = now;
|
|
195
|
+
break;
|
|
196
|
+
case 'load':
|
|
197
|
+
perAttemptRef.current.htmlLoadedAt = now;
|
|
198
|
+
break;
|
|
199
|
+
case 'load_end':
|
|
200
|
+
perAttemptRef.current.onLoadEndAt = now;
|
|
201
|
+
break;
|
|
202
|
+
case 'native_error':
|
|
203
|
+
nativeErrorCountRef.current += 1;
|
|
204
|
+
break;
|
|
205
|
+
case 'os_kill':
|
|
206
|
+
osKillCountRef.current += 1;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}, []);
|
|
210
|
+
const getMeta = react.useCallback(({
|
|
211
|
+
phase
|
|
212
|
+
}) => {
|
|
213
|
+
const url = webViewUrlRef.current;
|
|
214
|
+
const {
|
|
215
|
+
htmlLoadStartedAt,
|
|
216
|
+
htmlLoadedAt,
|
|
217
|
+
onLoadEndAt,
|
|
218
|
+
manifestReceivedAt,
|
|
219
|
+
sdkReadyAt
|
|
220
|
+
} = perAttemptRef.current;
|
|
221
|
+
return {
|
|
222
|
+
hadClearState: hasClearStateInUrl(url),
|
|
223
|
+
htmlLoadMs: diffMs$1(htmlLoadedAt, htmlLoadStartedAt),
|
|
224
|
+
loadingTimeoutMs: loadingTimeout,
|
|
225
|
+
manifestReceivedMs: diffMs$1(manifestReceivedAt, onLoadEndAt),
|
|
226
|
+
nativeErrorCount: nativeErrorCountRef.current,
|
|
227
|
+
onLoadToOnLoadEndMs: diffMs$1(onLoadEndAt, htmlLoadedAt),
|
|
228
|
+
osKillCount: osKillCountRef.current,
|
|
229
|
+
phase,
|
|
230
|
+
recoveryTimeoutMs: recoveryTimeout,
|
|
231
|
+
retryCount: readRetryCount(url),
|
|
232
|
+
sdkReadyMs: diffMs$1(sdkReadyAt, onLoadEndAt),
|
|
233
|
+
webviewUrl: url.toString()
|
|
234
|
+
};
|
|
235
|
+
}, [loadingTimeout, recoveryTimeout]);
|
|
236
|
+
return {
|
|
237
|
+
getMeta,
|
|
238
|
+
recordEvent
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
124
242
|
const useWebViewVisibility = core => {
|
|
125
243
|
const webViewVisibilityRequestChannelRef = react.useRef(messageTransport.createRequestChannel(core.messageTransport));
|
|
126
244
|
const [visible, setVisible] = react.useState(false);
|
|
@@ -157,10 +275,6 @@ const styles = reactNative.StyleSheet.create({
|
|
|
157
275
|
}
|
|
158
276
|
});
|
|
159
277
|
|
|
160
|
-
const CLEAR_STATE_PARAM = 'clear-state';
|
|
161
|
-
const ENVIRONMENT_ID_PARAM = 'environmentId';
|
|
162
|
-
const WEBVIEW_START_TIME_PARAM = 'webviewStartTime';
|
|
163
|
-
|
|
164
278
|
const setClearStateToUrl = webViewUrl => {
|
|
165
279
|
const newWebViewUrl = new URL(webViewUrl);
|
|
166
280
|
newWebViewUrl.searchParams.set(CLEAR_STATE_PARAM, 'true');
|
|
@@ -168,8 +282,6 @@ const setClearStateToUrl = webViewUrl => {
|
|
|
168
282
|
return newWebViewUrl;
|
|
169
283
|
};
|
|
170
284
|
|
|
171
|
-
const hasClearStateInUrl = webViewUrl => Boolean(webViewUrl.searchParams.get(CLEAR_STATE_PARAM));
|
|
172
|
-
|
|
173
285
|
const useWebViewRecoveryTimeout = ({
|
|
174
286
|
core,
|
|
175
287
|
webViewUrl,
|
|
@@ -322,13 +434,15 @@ const shouldAllowNavigation = (request, webViewUrl) => {
|
|
|
322
434
|
return false;
|
|
323
435
|
};
|
|
324
436
|
|
|
437
|
+
const DEFAULT_RECOVERY_TIMEOUT_MS$1 = 20000;
|
|
438
|
+
const DEFAULT_LOADING_TIMEOUT_MS$1 = 20000;
|
|
325
439
|
const WebView = ({
|
|
326
440
|
webviewUrl: initialWebViewUrl,
|
|
327
441
|
core,
|
|
328
442
|
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
329
443
|
disableRecovery: _disableRecovery = false,
|
|
330
|
-
recoveryTimeout: _recoveryTimeout =
|
|
331
|
-
loadingTimeout: _loadingTimeout =
|
|
444
|
+
recoveryTimeout: _recoveryTimeout = DEFAULT_RECOVERY_TIMEOUT_MS$1,
|
|
445
|
+
loadingTimeout: _loadingTimeout = DEFAULT_LOADING_TIMEOUT_MS$1
|
|
332
446
|
}) => {
|
|
333
447
|
const webViewRef = react.useRef(null);
|
|
334
448
|
const {
|
|
@@ -338,6 +452,15 @@ const WebView = ({
|
|
|
338
452
|
const {
|
|
339
453
|
onMessageHandler
|
|
340
454
|
} = useMessageTransportWebViewBridge(core, webViewRef);
|
|
455
|
+
const {
|
|
456
|
+
recordEvent,
|
|
457
|
+
getMeta
|
|
458
|
+
} = useWebViewPhaseTimers({
|
|
459
|
+
core,
|
|
460
|
+
loadingTimeout: _loadingTimeout,
|
|
461
|
+
recoveryTimeout: _recoveryTimeout,
|
|
462
|
+
webViewUrl
|
|
463
|
+
});
|
|
341
464
|
const containerStyles = [styles['container'], visible ? styles.show : styles.hide];
|
|
342
465
|
/**
|
|
343
466
|
* Block the message transport when the webview is unmounted
|
|
@@ -379,13 +502,23 @@ const WebView = ({
|
|
|
379
502
|
return newWebViewUrl;
|
|
380
503
|
});
|
|
381
504
|
}, [core]);
|
|
382
|
-
const blockAndReloadWebViewOsKill = react.useCallback(() =>
|
|
505
|
+
const blockAndReloadWebViewOsKill = react.useCallback(() => {
|
|
506
|
+
recordEvent('os_kill');
|
|
507
|
+
blockAndReloadWebView('os_kill');
|
|
508
|
+
}, [blockAndReloadWebView, recordEvent]);
|
|
383
509
|
react.useEffect(() => core.messageTransport.recoveryManager.onRecoveryRequested(() => blockAndReloadWebView('recovery')), [core, blockAndReloadWebView]);
|
|
384
|
-
const setWebViewLoadError = react.useCallback(
|
|
510
|
+
const setWebViewLoadError = react.useCallback(phase => {
|
|
385
511
|
// Error was already thrown, do not throw again
|
|
386
512
|
if (core.initialization.error instanceof WebViewFailedToLoadError) return;
|
|
387
|
-
core.initialization.error = new WebViewFailedToLoadError(
|
|
388
|
-
|
|
513
|
+
core.initialization.error = new WebViewFailedToLoadError(getMeta({
|
|
514
|
+
phase
|
|
515
|
+
}));
|
|
516
|
+
}, [core, getMeta]);
|
|
517
|
+
/**
|
|
518
|
+
* Wraps `setWebViewLoadError` so the consumer doesn't need to know
|
|
519
|
+
* about the phase enum; each callsite below binds its own phase.
|
|
520
|
+
*/
|
|
521
|
+
const setWebViewLoadErrorWithPhase = react.useCallback(phase => () => setWebViewLoadError(phase), [setWebViewLoadError]);
|
|
389
522
|
/**
|
|
390
523
|
* Reload the webview with a clean state when a timeout is reached
|
|
391
524
|
* and the webview did not get to the loaded state yet
|
|
@@ -393,7 +526,7 @@ const WebView = ({
|
|
|
393
526
|
const startRecoveryTimeout = useWebViewRecoveryTimeout({
|
|
394
527
|
core,
|
|
395
528
|
disableRecovery: _disableRecovery,
|
|
396
|
-
onFailedToLoadAfterClearState:
|
|
529
|
+
onFailedToLoadAfterClearState: setWebViewLoadErrorWithPhase('after_clear_state'),
|
|
397
530
|
recoveryTimeout: _recoveryTimeout,
|
|
398
531
|
setWebViewUrl,
|
|
399
532
|
webViewUrl
|
|
@@ -401,17 +534,30 @@ const WebView = ({
|
|
|
401
534
|
const webViewLoadErrorCountRef = react.useRef(0);
|
|
402
535
|
const onWebViewLoadError = react.useCallback(() => {
|
|
403
536
|
webViewLoadErrorCountRef.current = webViewLoadErrorCountRef.current + 1;
|
|
537
|
+
recordEvent('native_error');
|
|
404
538
|
/**
|
|
405
539
|
* This is the first attempt to load the webview, do not throw an error
|
|
406
540
|
* because the recovery system will attempt to reload the webview
|
|
407
541
|
*/
|
|
408
542
|
if (webViewLoadErrorCountRef.current === 1) return;
|
|
409
|
-
setWebViewLoadError();
|
|
410
|
-
}, [setWebViewLoadError]);
|
|
543
|
+
setWebViewLoadError('native_error');
|
|
544
|
+
}, [recordEvent, setWebViewLoadError]);
|
|
411
545
|
const {
|
|
412
|
-
onLoad,
|
|
413
|
-
onLoadStart
|
|
414
|
-
} = useWebViewLoadingTimeout(_loadingTimeout,
|
|
546
|
+
onLoad: onLoadFromTimeout,
|
|
547
|
+
onLoadStart: onLoadStartFromTimeout
|
|
548
|
+
} = useWebViewLoadingTimeout(_loadingTimeout, setWebViewLoadErrorWithPhase('html_load'));
|
|
549
|
+
const onLoadStart = react.useCallback(() => {
|
|
550
|
+
recordEvent('load_start');
|
|
551
|
+
onLoadStartFromTimeout();
|
|
552
|
+
}, [onLoadStartFromTimeout, recordEvent]);
|
|
553
|
+
const onLoad = react.useCallback(() => {
|
|
554
|
+
recordEvent('load');
|
|
555
|
+
onLoadFromTimeout();
|
|
556
|
+
}, [onLoadFromTimeout, recordEvent]);
|
|
557
|
+
const onLoadEnd = react.useCallback(() => {
|
|
558
|
+
recordEvent('load_end');
|
|
559
|
+
startRecoveryTimeout();
|
|
560
|
+
}, [recordEvent, startRecoveryTimeout]);
|
|
415
561
|
return /*#__PURE__*/jsxRuntime.jsx(reactNativeWebview.WebView, {
|
|
416
562
|
ref: webViewRef,
|
|
417
563
|
source: {
|
|
@@ -420,7 +566,7 @@ const WebView = ({
|
|
|
420
566
|
containerStyle: containerStyles,
|
|
421
567
|
style: styles['webview'],
|
|
422
568
|
onMessage: onMessageHandler,
|
|
423
|
-
onLoadEnd:
|
|
569
|
+
onLoadEnd: onLoadEnd,
|
|
424
570
|
onLoadStart: onLoadStart,
|
|
425
571
|
onLoad: onLoad,
|
|
426
572
|
hideKeyboardAccessoryView: true,
|
|
@@ -472,6 +618,105 @@ const getEmbeddedWebView = () => {
|
|
|
472
618
|
}
|
|
473
619
|
};
|
|
474
620
|
|
|
621
|
+
const MANIFEST_MESSAGE_TYPE = 'manifest';
|
|
622
|
+
const emptyPerAttemptState = () => ({
|
|
623
|
+
htmlLoadStartedAt: null,
|
|
624
|
+
htmlLoadedAt: null,
|
|
625
|
+
manifestReceivedAt: null,
|
|
626
|
+
onLoadEndAt: null,
|
|
627
|
+
sdkReadyAt: null
|
|
628
|
+
});
|
|
629
|
+
const diffMs = (later, earlier) => later !== null && earlier !== null ? later - earlier : null;
|
|
630
|
+
/**
|
|
631
|
+
* Non-React equivalent of `useWebViewPhaseTimers` for the embedded native
|
|
632
|
+
* webview path. The embedded path is a singleton wired by `setupEmbeddedWebView`
|
|
633
|
+
* (not a React component), so we expose the same phase-timing surface as a
|
|
634
|
+
* plain factory.
|
|
635
|
+
*
|
|
636
|
+
* The hook and the factory are intentionally separate: the embedded path does
|
|
637
|
+
* not have a retry or clear-state model, so `retryCount` is always `0` and
|
|
638
|
+
* `hadClearState` is always `false` in the meta produced here. Sharing one
|
|
639
|
+
* implementation would require either pulling React into a hot non-React path
|
|
640
|
+
* or threading a stateless adapter through both consumers \u2014 the duplication
|
|
641
|
+
* is small enough that keeping them separate stays cleaner.
|
|
642
|
+
*/
|
|
643
|
+
const createEmbeddedWebViewPhaseTimers = ({
|
|
644
|
+
core,
|
|
645
|
+
webViewUrl,
|
|
646
|
+
loadingTimeoutMs,
|
|
647
|
+
recoveryTimeoutMs
|
|
648
|
+
}) => {
|
|
649
|
+
let perAttempt = emptyPerAttemptState();
|
|
650
|
+
let nativeErrorCount = 0;
|
|
651
|
+
let osKillCount = 0;
|
|
652
|
+
const handleMessage = message => {
|
|
653
|
+
if (message.origin !== 'webview') return;
|
|
654
|
+
if (message.type === MANIFEST_MESSAGE_TYPE && perAttempt.manifestReceivedAt === null) {
|
|
655
|
+
perAttempt.manifestReceivedAt = Date.now();
|
|
656
|
+
}
|
|
657
|
+
if (message.type === webviewMessages.sdkHasLoadedEventName && perAttempt.sdkReadyAt === null) {
|
|
658
|
+
perAttempt.sdkReadyAt = Date.now();
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
core.messageTransport.on(handleMessage);
|
|
662
|
+
const recordEvent = event => {
|
|
663
|
+
const now = Date.now();
|
|
664
|
+
switch (event) {
|
|
665
|
+
case 'load_start':
|
|
666
|
+
perAttempt = emptyPerAttemptState();
|
|
667
|
+
perAttempt.htmlLoadStartedAt = now;
|
|
668
|
+
break;
|
|
669
|
+
case 'load':
|
|
670
|
+
perAttempt.htmlLoadedAt = now;
|
|
671
|
+
break;
|
|
672
|
+
case 'load_end':
|
|
673
|
+
perAttempt.onLoadEndAt = now;
|
|
674
|
+
break;
|
|
675
|
+
case 'native_error':
|
|
676
|
+
nativeErrorCount += 1;
|
|
677
|
+
break;
|
|
678
|
+
case 'os_kill':
|
|
679
|
+
osKillCount += 1;
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
const getMeta = ({
|
|
684
|
+
phase
|
|
685
|
+
}) => {
|
|
686
|
+
const {
|
|
687
|
+
htmlLoadStartedAt,
|
|
688
|
+
htmlLoadedAt,
|
|
689
|
+
onLoadEndAt,
|
|
690
|
+
manifestReceivedAt,
|
|
691
|
+
sdkReadyAt
|
|
692
|
+
} = perAttempt;
|
|
693
|
+
return {
|
|
694
|
+
hadClearState: false,
|
|
695
|
+
htmlLoadMs: diffMs(htmlLoadedAt, htmlLoadStartedAt),
|
|
696
|
+
loadingTimeoutMs,
|
|
697
|
+
manifestReceivedMs: diffMs(manifestReceivedAt, onLoadEndAt),
|
|
698
|
+
nativeErrorCount,
|
|
699
|
+
onLoadToOnLoadEndMs: diffMs(onLoadEndAt, htmlLoadedAt),
|
|
700
|
+
osKillCount,
|
|
701
|
+
phase,
|
|
702
|
+
recoveryTimeoutMs,
|
|
703
|
+
retryCount: 0,
|
|
704
|
+
sdkReadyMs: diffMs(sdkReadyAt, onLoadEndAt),
|
|
705
|
+
webviewUrl: webViewUrl.toString()
|
|
706
|
+
};
|
|
707
|
+
};
|
|
708
|
+
const dispose = () => {
|
|
709
|
+
core.messageTransport.off(handleMessage);
|
|
710
|
+
};
|
|
711
|
+
return {
|
|
712
|
+
dispose,
|
|
713
|
+
getMeta,
|
|
714
|
+
recordEvent
|
|
715
|
+
};
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
const DEFAULT_LOADING_TIMEOUT_MS = 20000;
|
|
719
|
+
const DEFAULT_RECOVERY_TIMEOUT_MS = 20000;
|
|
475
720
|
// Wires the JS-side bridge to the native WKWebView singleton owned by Swift.
|
|
476
721
|
// This is intentionally not a React component: the embedded webview lives
|
|
477
722
|
// outside the React tree (in a dedicated UIWindow on iOS), so its setup runs
|
|
@@ -484,10 +729,47 @@ const getEmbeddedWebView = () => {
|
|
|
484
729
|
const setupEmbeddedWebView = ({
|
|
485
730
|
webviewUrl,
|
|
486
731
|
core,
|
|
487
|
-
webviewDebuggingEnabled: _webviewDebuggingEnabled = false
|
|
732
|
+
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
733
|
+
loadingTimeoutMs: _loadingTimeoutMs = DEFAULT_LOADING_TIMEOUT_MS,
|
|
734
|
+
recoveryTimeoutMs: _recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS
|
|
488
735
|
}) => {
|
|
489
736
|
const native = getEmbeddedWebView();
|
|
490
737
|
const builtUrl = assignStartTimeToUrl(assignEnvironmentIdToUrl(webviewUrl, core.environmentId));
|
|
738
|
+
const phaseTimers = createEmbeddedWebViewPhaseTimers({
|
|
739
|
+
core,
|
|
740
|
+
loadingTimeoutMs: _loadingTimeoutMs,
|
|
741
|
+
recoveryTimeoutMs: _recoveryTimeoutMs,
|
|
742
|
+
webViewUrl: builtUrl
|
|
743
|
+
});
|
|
744
|
+
let loadingTimer = null;
|
|
745
|
+
let recoveryTimer = null;
|
|
746
|
+
let hasFailed = false;
|
|
747
|
+
const clearLoadingTimer = () => {
|
|
748
|
+
if (loadingTimer !== null) {
|
|
749
|
+
clearTimeout(loadingTimer);
|
|
750
|
+
loadingTimer = null;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
const clearRecoveryTimer = () => {
|
|
754
|
+
if (recoveryTimer !== null) {
|
|
755
|
+
clearTimeout(recoveryTimer);
|
|
756
|
+
recoveryTimer = null;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
// Raise `WebViewFailedToLoadError` once per setup. Subsequent triggers
|
|
760
|
+
// (e.g. recovery timeout firing after html_load already failed) are
|
|
761
|
+
// suppressed so we don't spam the initialization-error setter with
|
|
762
|
+
// duplicate logs for the same load attempt.
|
|
763
|
+
const raiseFailure = (phase, extraMeta = {}) => {
|
|
764
|
+
if (hasFailed) return;
|
|
765
|
+
hasFailed = true;
|
|
766
|
+
clearLoadingTimer();
|
|
767
|
+
clearRecoveryTimer();
|
|
768
|
+
const meta = phaseTimers.getMeta({
|
|
769
|
+
phase
|
|
770
|
+
});
|
|
771
|
+
core.initialization.error = new WebViewFailedToLoadError(Object.assign(Object.assign({}, meta), extraMeta));
|
|
772
|
+
};
|
|
491
773
|
const visibilityChannel = messageTransport.createRequestChannel(core.messageTransport);
|
|
492
774
|
const removeVisibilityHandler = visibilityChannel.handle('setVisibility', visible => {
|
|
493
775
|
native.setVisible(visible).catch(err => {
|
|
@@ -501,6 +783,15 @@ const setupEmbeddedWebView = ({
|
|
|
501
783
|
});
|
|
502
784
|
};
|
|
503
785
|
core.messageTransport.on(handleHostMessage);
|
|
786
|
+
// Watch for the webview-side SDK ready signal so we can clear the
|
|
787
|
+
// recovery timer. The phase-timer factory also subscribes for its own
|
|
788
|
+
// bookkeeping — both subscriptions are independent and idempotent.
|
|
789
|
+
const handleSdkReady = message => {
|
|
790
|
+
if (message.origin !== 'webview') return;
|
|
791
|
+
if (message.type !== webviewMessages.sdkHasLoadedEventName) return;
|
|
792
|
+
clearRecoveryTimer();
|
|
793
|
+
};
|
|
794
|
+
core.messageTransport.on(handleSdkReady);
|
|
504
795
|
const onMessageSub = native.addListener('onMessage', event => {
|
|
505
796
|
let parsed = null;
|
|
506
797
|
try {
|
|
@@ -523,7 +814,32 @@ const setupEmbeddedWebView = ({
|
|
|
523
814
|
logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
|
|
524
815
|
});
|
|
525
816
|
});
|
|
817
|
+
const onLoadStartSub = native.addListener('onLoadStart', () => {
|
|
818
|
+
phaseTimers.recordEvent('load_start');
|
|
819
|
+
// Re-arm both timers on every load_start. A retry from the native
|
|
820
|
+
// side (e.g. a redirect, or a reload after a transient failure)
|
|
821
|
+
// resets the html_load window cleanly without leaving a stale timer
|
|
822
|
+
// around from the previous attempt.
|
|
823
|
+
clearLoadingTimer();
|
|
824
|
+
clearRecoveryTimer();
|
|
825
|
+
loadingTimer = setTimeout(() => {
|
|
826
|
+
raiseFailure('html_load');
|
|
827
|
+
}, _loadingTimeoutMs);
|
|
828
|
+
});
|
|
829
|
+
const onLoadSub = native.addListener('onLoad', () => {
|
|
830
|
+
phaseTimers.recordEvent('load');
|
|
831
|
+
clearLoadingTimer();
|
|
832
|
+
});
|
|
833
|
+
const onLoadEndSub = native.addListener('onLoadEnd', () => {
|
|
834
|
+
phaseTimers.recordEvent('load_end');
|
|
835
|
+
clearLoadingTimer();
|
|
836
|
+
clearRecoveryTimer();
|
|
837
|
+
recoveryTimer = setTimeout(() => {
|
|
838
|
+
raiseFailure('sdk_bootstrap');
|
|
839
|
+
}, _recoveryTimeoutMs);
|
|
840
|
+
});
|
|
526
841
|
const onLoadErrorSub = native.addListener('onLoadError', event => {
|
|
842
|
+
phaseTimers.recordEvent('native_error');
|
|
527
843
|
logger.warn('EmbeddedWebView load error', event);
|
|
528
844
|
logger.instrument('Embedded webview load error', {
|
|
529
845
|
environmentId: core.environmentId,
|
|
@@ -537,7 +853,13 @@ const setupEmbeddedWebView = ({
|
|
|
537
853
|
time: 0,
|
|
538
854
|
webviewUrl: builtUrl.toString()
|
|
539
855
|
});
|
|
540
|
-
|
|
856
|
+
raiseFailure('embedded_native_error', {
|
|
857
|
+
nativeErrorCode: event.code,
|
|
858
|
+
nativeErrorDescription: event.description,
|
|
859
|
+
nativeErrorDomain: event.domain,
|
|
860
|
+
nativeErrorFailedUrl: event.url,
|
|
861
|
+
nativeErrorIsProvisional: event.isProvisional
|
|
862
|
+
});
|
|
541
863
|
});
|
|
542
864
|
native.setDebuggingEnabled(_webviewDebuggingEnabled).catch(err => {
|
|
543
865
|
logger.warn('EmbeddedWebView.setDebuggingEnabled failed', err);
|
|
@@ -546,11 +868,18 @@ const setupEmbeddedWebView = ({
|
|
|
546
868
|
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
547
869
|
});
|
|
548
870
|
return () => {
|
|
871
|
+
clearLoadingTimer();
|
|
872
|
+
clearRecoveryTimer();
|
|
549
873
|
removeVisibilityHandler();
|
|
550
874
|
core.messageTransport.off(handleHostMessage);
|
|
875
|
+
core.messageTransport.off(handleSdkReady);
|
|
551
876
|
onMessageSub.remove();
|
|
552
877
|
onShouldStartLoadSub.remove();
|
|
878
|
+
onLoadStartSub.remove();
|
|
879
|
+
onLoadSub.remove();
|
|
880
|
+
onLoadEndSub.remove();
|
|
553
881
|
onLoadErrorSub.remove();
|
|
882
|
+
phaseTimers.dispose();
|
|
554
883
|
};
|
|
555
884
|
};
|
|
556
885
|
|