@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.
Files changed (33) hide show
  1. package/android/EmbeddedWebViewController.kt +26 -0
  2. package/android/EmbeddedWebViewModule.kt +8 -1
  3. package/android/KeyStoreKeyManager.kt +7 -1
  4. package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
  5. package/android/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
  6. package/android/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  7. package/android/embeddedwebview/EmbeddedWebViewController.kt +26 -0
  8. package/android/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
  9. package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
  10. package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
  11. package/android/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  12. package/android/keychain/KeyStoreKeyManager.kt +7 -1
  13. package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
  14. package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
  15. package/android/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  16. package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
  17. package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
  18. package/android/src/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  19. package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
  20. package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
  21. package/android/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  22. package/index.cjs +352 -23
  23. package/index.js +353 -24
  24. package/ios/EmbeddedWebViewController.swift +22 -0
  25. package/ios/EmbeddedWebViewModule.swift +8 -1
  26. package/package.json +6 -6
  27. package/src/components/WebView/EmbeddedWebView/EmbeddedWebView.d.ts +20 -1
  28. package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/embeddedWebViewPhaseTimers.d.ts +52 -0
  29. package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/index.d.ts +1 -0
  30. package/src/components/WebView/useWebViewPhaseTimers/index.d.ts +1 -0
  31. package/src/components/WebView/useWebViewPhaseTimers/useWebViewPhaseTimers.d.ts +45 -0
  32. package/src/errors/WebViewFailedToLoadError.d.ts +84 -1
  33. 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 = 8000,
331
- loadingTimeout: _loadingTimeout = 10000
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(() => blockAndReloadWebView('os_kill'), [blockAndReloadWebView]);
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
- }, [core]);
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: setWebViewLoadError,
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, setWebViewLoadError);
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: () => startRecoveryTimeout(),
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
- core.initialization.error = new WebViewFailedToLoadError();
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