@dynamic-labs/react-native-extension 4.83.1 → 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/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -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/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -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/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/index.cjs +350 -24
- package/index.js +351 -25
- 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/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.
|
|
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) {
|
|
@@ -122,6 +123,122 @@ const useMessageTransportWebViewBridge = (core, webViewRef) => {
|
|
|
122
123
|
};
|
|
123
124
|
};
|
|
124
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
|
+
|
|
125
242
|
const useWebViewVisibility = core => {
|
|
126
243
|
const webViewVisibilityRequestChannelRef = react.useRef(messageTransport.createRequestChannel(core.messageTransport));
|
|
127
244
|
const [visible, setVisible] = react.useState(false);
|
|
@@ -158,10 +275,6 @@ const styles = reactNative.StyleSheet.create({
|
|
|
158
275
|
}
|
|
159
276
|
});
|
|
160
277
|
|
|
161
|
-
const CLEAR_STATE_PARAM = 'clear-state';
|
|
162
|
-
const ENVIRONMENT_ID_PARAM = 'environmentId';
|
|
163
|
-
const WEBVIEW_START_TIME_PARAM = 'webviewStartTime';
|
|
164
|
-
|
|
165
278
|
const setClearStateToUrl = webViewUrl => {
|
|
166
279
|
const newWebViewUrl = new URL(webViewUrl);
|
|
167
280
|
newWebViewUrl.searchParams.set(CLEAR_STATE_PARAM, 'true');
|
|
@@ -169,8 +282,6 @@ const setClearStateToUrl = webViewUrl => {
|
|
|
169
282
|
return newWebViewUrl;
|
|
170
283
|
};
|
|
171
284
|
|
|
172
|
-
const hasClearStateInUrl = webViewUrl => Boolean(webViewUrl.searchParams.get(CLEAR_STATE_PARAM));
|
|
173
|
-
|
|
174
285
|
const useWebViewRecoveryTimeout = ({
|
|
175
286
|
core,
|
|
176
287
|
webViewUrl,
|
|
@@ -323,15 +434,15 @@ const shouldAllowNavigation = (request, webViewUrl) => {
|
|
|
323
434
|
return false;
|
|
324
435
|
};
|
|
325
436
|
|
|
326
|
-
const DEFAULT_RECOVERY_TIMEOUT_MS = 20000;
|
|
327
|
-
const DEFAULT_LOADING_TIMEOUT_MS = 20000;
|
|
437
|
+
const DEFAULT_RECOVERY_TIMEOUT_MS$1 = 20000;
|
|
438
|
+
const DEFAULT_LOADING_TIMEOUT_MS$1 = 20000;
|
|
328
439
|
const WebView = ({
|
|
329
440
|
webviewUrl: initialWebViewUrl,
|
|
330
441
|
core,
|
|
331
442
|
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
332
443
|
disableRecovery: _disableRecovery = false,
|
|
333
|
-
recoveryTimeout: _recoveryTimeout = DEFAULT_RECOVERY_TIMEOUT_MS,
|
|
334
|
-
loadingTimeout: _loadingTimeout = DEFAULT_LOADING_TIMEOUT_MS
|
|
444
|
+
recoveryTimeout: _recoveryTimeout = DEFAULT_RECOVERY_TIMEOUT_MS$1,
|
|
445
|
+
loadingTimeout: _loadingTimeout = DEFAULT_LOADING_TIMEOUT_MS$1
|
|
335
446
|
}) => {
|
|
336
447
|
const webViewRef = react.useRef(null);
|
|
337
448
|
const {
|
|
@@ -341,6 +452,15 @@ const WebView = ({
|
|
|
341
452
|
const {
|
|
342
453
|
onMessageHandler
|
|
343
454
|
} = useMessageTransportWebViewBridge(core, webViewRef);
|
|
455
|
+
const {
|
|
456
|
+
recordEvent,
|
|
457
|
+
getMeta
|
|
458
|
+
} = useWebViewPhaseTimers({
|
|
459
|
+
core,
|
|
460
|
+
loadingTimeout: _loadingTimeout,
|
|
461
|
+
recoveryTimeout: _recoveryTimeout,
|
|
462
|
+
webViewUrl
|
|
463
|
+
});
|
|
344
464
|
const containerStyles = [styles['container'], visible ? styles.show : styles.hide];
|
|
345
465
|
/**
|
|
346
466
|
* Block the message transport when the webview is unmounted
|
|
@@ -382,13 +502,23 @@ const WebView = ({
|
|
|
382
502
|
return newWebViewUrl;
|
|
383
503
|
});
|
|
384
504
|
}, [core]);
|
|
385
|
-
const blockAndReloadWebViewOsKill = react.useCallback(() =>
|
|
505
|
+
const blockAndReloadWebViewOsKill = react.useCallback(() => {
|
|
506
|
+
recordEvent('os_kill');
|
|
507
|
+
blockAndReloadWebView('os_kill');
|
|
508
|
+
}, [blockAndReloadWebView, recordEvent]);
|
|
386
509
|
react.useEffect(() => core.messageTransport.recoveryManager.onRecoveryRequested(() => blockAndReloadWebView('recovery')), [core, blockAndReloadWebView]);
|
|
387
|
-
const setWebViewLoadError = react.useCallback(
|
|
510
|
+
const setWebViewLoadError = react.useCallback(phase => {
|
|
388
511
|
// Error was already thrown, do not throw again
|
|
389
512
|
if (core.initialization.error instanceof WebViewFailedToLoadError) return;
|
|
390
|
-
core.initialization.error = new WebViewFailedToLoadError(
|
|
391
|
-
|
|
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]);
|
|
392
522
|
/**
|
|
393
523
|
* Reload the webview with a clean state when a timeout is reached
|
|
394
524
|
* and the webview did not get to the loaded state yet
|
|
@@ -396,7 +526,7 @@ const WebView = ({
|
|
|
396
526
|
const startRecoveryTimeout = useWebViewRecoveryTimeout({
|
|
397
527
|
core,
|
|
398
528
|
disableRecovery: _disableRecovery,
|
|
399
|
-
onFailedToLoadAfterClearState:
|
|
529
|
+
onFailedToLoadAfterClearState: setWebViewLoadErrorWithPhase('after_clear_state'),
|
|
400
530
|
recoveryTimeout: _recoveryTimeout,
|
|
401
531
|
setWebViewUrl,
|
|
402
532
|
webViewUrl
|
|
@@ -404,17 +534,30 @@ const WebView = ({
|
|
|
404
534
|
const webViewLoadErrorCountRef = react.useRef(0);
|
|
405
535
|
const onWebViewLoadError = react.useCallback(() => {
|
|
406
536
|
webViewLoadErrorCountRef.current = webViewLoadErrorCountRef.current + 1;
|
|
537
|
+
recordEvent('native_error');
|
|
407
538
|
/**
|
|
408
539
|
* This is the first attempt to load the webview, do not throw an error
|
|
409
540
|
* because the recovery system will attempt to reload the webview
|
|
410
541
|
*/
|
|
411
542
|
if (webViewLoadErrorCountRef.current === 1) return;
|
|
412
|
-
setWebViewLoadError();
|
|
413
|
-
}, [setWebViewLoadError]);
|
|
543
|
+
setWebViewLoadError('native_error');
|
|
544
|
+
}, [recordEvent, setWebViewLoadError]);
|
|
414
545
|
const {
|
|
415
|
-
onLoad,
|
|
416
|
-
onLoadStart
|
|
417
|
-
} = 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]);
|
|
418
561
|
return /*#__PURE__*/jsxRuntime.jsx(reactNativeWebview.WebView, {
|
|
419
562
|
ref: webViewRef,
|
|
420
563
|
source: {
|
|
@@ -423,7 +566,7 @@ const WebView = ({
|
|
|
423
566
|
containerStyle: containerStyles,
|
|
424
567
|
style: styles['webview'],
|
|
425
568
|
onMessage: onMessageHandler,
|
|
426
|
-
onLoadEnd:
|
|
569
|
+
onLoadEnd: onLoadEnd,
|
|
427
570
|
onLoadStart: onLoadStart,
|
|
428
571
|
onLoad: onLoad,
|
|
429
572
|
hideKeyboardAccessoryView: true,
|
|
@@ -475,6 +618,105 @@ const getEmbeddedWebView = () => {
|
|
|
475
618
|
}
|
|
476
619
|
};
|
|
477
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;
|
|
478
720
|
// Wires the JS-side bridge to the native WKWebView singleton owned by Swift.
|
|
479
721
|
// This is intentionally not a React component: the embedded webview lives
|
|
480
722
|
// outside the React tree (in a dedicated UIWindow on iOS), so its setup runs
|
|
@@ -487,10 +729,47 @@ const getEmbeddedWebView = () => {
|
|
|
487
729
|
const setupEmbeddedWebView = ({
|
|
488
730
|
webviewUrl,
|
|
489
731
|
core,
|
|
490
|
-
webviewDebuggingEnabled: _webviewDebuggingEnabled = false
|
|
732
|
+
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
733
|
+
loadingTimeoutMs: _loadingTimeoutMs = DEFAULT_LOADING_TIMEOUT_MS,
|
|
734
|
+
recoveryTimeoutMs: _recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS
|
|
491
735
|
}) => {
|
|
492
736
|
const native = getEmbeddedWebView();
|
|
493
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
|
+
};
|
|
494
773
|
const visibilityChannel = messageTransport.createRequestChannel(core.messageTransport);
|
|
495
774
|
const removeVisibilityHandler = visibilityChannel.handle('setVisibility', visible => {
|
|
496
775
|
native.setVisible(visible).catch(err => {
|
|
@@ -504,6 +783,15 @@ const setupEmbeddedWebView = ({
|
|
|
504
783
|
});
|
|
505
784
|
};
|
|
506
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);
|
|
507
795
|
const onMessageSub = native.addListener('onMessage', event => {
|
|
508
796
|
let parsed = null;
|
|
509
797
|
try {
|
|
@@ -526,7 +814,32 @@ const setupEmbeddedWebView = ({
|
|
|
526
814
|
logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
|
|
527
815
|
});
|
|
528
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
|
+
});
|
|
529
841
|
const onLoadErrorSub = native.addListener('onLoadError', event => {
|
|
842
|
+
phaseTimers.recordEvent('native_error');
|
|
530
843
|
logger.warn('EmbeddedWebView load error', event);
|
|
531
844
|
logger.instrument('Embedded webview load error', {
|
|
532
845
|
environmentId: core.environmentId,
|
|
@@ -540,7 +853,13 @@ const setupEmbeddedWebView = ({
|
|
|
540
853
|
time: 0,
|
|
541
854
|
webviewUrl: builtUrl.toString()
|
|
542
855
|
});
|
|
543
|
-
|
|
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
|
+
});
|
|
544
863
|
});
|
|
545
864
|
native.setDebuggingEnabled(_webviewDebuggingEnabled).catch(err => {
|
|
546
865
|
logger.warn('EmbeddedWebView.setDebuggingEnabled failed', err);
|
|
@@ -549,11 +868,18 @@ const setupEmbeddedWebView = ({
|
|
|
549
868
|
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
550
869
|
});
|
|
551
870
|
return () => {
|
|
871
|
+
clearLoadingTimer();
|
|
872
|
+
clearRecoveryTimer();
|
|
552
873
|
removeVisibilityHandler();
|
|
553
874
|
core.messageTransport.off(handleHostMessage);
|
|
875
|
+
core.messageTransport.off(handleSdkReady);
|
|
554
876
|
onMessageSub.remove();
|
|
555
877
|
onShouldStartLoadSub.remove();
|
|
878
|
+
onLoadStartSub.remove();
|
|
879
|
+
onLoadSub.remove();
|
|
880
|
+
onLoadEndSub.remove();
|
|
556
881
|
onLoadErrorSub.remove();
|
|
882
|
+
phaseTimers.dispose();
|
|
557
883
|
};
|
|
558
884
|
};
|
|
559
885
|
|