@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.
Files changed (40) hide show
  1. package/android/EmbeddedWebViewController.kt +19 -2
  2. package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
  3. package/android/embeddedwebview/EmbeddedWebViewController.kt +19 -2
  4. package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
  5. package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
  6. package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
  7. package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
  8. package/index.cjs +252 -796
  9. package/index.js +251 -797
  10. package/ios/EmbeddedWebViewController.swift +7 -6
  11. package/package.json +6 -6
  12. package/src/ReactNativeExtension/ReactNativeExtension.d.ts +11 -2
  13. package/src/components/WebView/EmbeddedWebView/EmbeddedWebView.d.ts +6 -19
  14. package/src/components/WebView/WebView.d.ts +17 -6
  15. package/src/errors/WebViewFailedToLoadError.d.ts +1 -86
  16. package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/embeddedWebViewPhaseTimers.d.ts +0 -66
  17. package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/index.d.ts +0 -1
  18. package/src/components/WebView/constants.d.ts +0 -3
  19. package/src/components/WebView/successLog/emitSuccessLog.d.ts +0 -35
  20. package/src/components/WebView/successLog/index.d.ts +0 -3
  21. package/src/components/WebView/useWebViewLoadingTimeout/index.d.ts +0 -1
  22. package/src/components/WebView/useWebViewLoadingTimeout/useWebViewLoadingTimeout.d.ts +0 -4
  23. package/src/components/WebView/useWebViewPhaseTimers/index.d.ts +0 -1
  24. package/src/components/WebView/useWebViewPhaseTimers/useWebViewPhaseTimers.d.ts +0 -46
  25. package/src/components/WebView/useWebViewRecoveryTimeout/index.d.ts +0 -1
  26. package/src/components/WebView/useWebViewRecoveryTimeout/useWebViewRecoveryTimeout.d.ts +0 -12
  27. package/src/components/WebView/useWebViewSuccessLog/index.d.ts +0 -1
  28. package/src/components/WebView/useWebViewSuccessLog/useWebViewSuccessLog.d.ts +0 -28
  29. package/src/components/WebView/utils/assignEnvironmentIdToUrl/assignEnvironmentIdToUrl.d.ts +0 -1
  30. package/src/components/WebView/utils/assignEnvironmentIdToUrl/index.d.ts +0 -1
  31. package/src/components/WebView/utils/assignStartTimeToUrl/assignStartTimeToUrl.d.ts +0 -1
  32. package/src/components/WebView/utils/assignStartTimeToUrl/index.d.ts +0 -1
  33. package/src/components/WebView/utils/hasClearStateInUrl/hasClearStateInUrl.d.ts +0 -1
  34. package/src/components/WebView/utils/hasClearStateInUrl/index.d.ts +0 -1
  35. package/src/components/WebView/utils/increaseRetryToUrl/increaseRetryToUrl.d.ts +0 -1
  36. package/src/components/WebView/utils/increaseRetryToUrl/index.d.ts +0 -1
  37. package/src/components/WebView/utils/setClearStateToUrl/index.d.ts +0 -1
  38. package/src/components/WebView/utils/setClearStateToUrl/setClearStateToUrl.d.ts +0 -1
  39. package/src/components/WebView/webViewPhaseTimerCore/index.d.ts +0 -2
  40. 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.92.3";
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
- class WebViewFailedToLoadError extends Error {
51
- constructor(meta) {
52
- super('Could not load Dynamic WebView');
53
- this.meta = meta;
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 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
- /**
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
- getMeta,
346
- getSuccessMeta,
347
- recordEvent
154
+ visible
348
155
  };
349
156
  };
350
157
 
351
- /******************************************************************************
352
- Copyright (c) Microsoft Corporation.
353
-
354
- Permission to use, copy, modify, and/or distribute this software for any
355
- purpose with or without fee is hereby granted.
356
-
357
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
358
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
359
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
360
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
361
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
362
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
363
- PERFORMANCE OF THIS SOFTWARE.
364
- ***************************************************************************** */
365
-
366
- function __awaiter(thisArg, _arguments, P, generator) {
367
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
368
- return new (P || (P = Promise))(function (resolve, reject) {
369
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
370
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
371
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
372
- step((generator = generator.apply(thisArg, _arguments || [])).next());
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 DEFAULT_RECOVERY_TIMEOUT_MS$1 = 20000;
712
- const DEFAULT_LOADING_TIMEOUT_MS$1 = 20000;
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: initialWebViewUrl,
284
+ webviewUrl,
715
285
  core,
716
286
  webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
717
- disableRecovery: _disableRecovery = false,
718
- recoveryTimeout: _recoveryTimeout = DEFAULT_RECOVERY_TIMEOUT_MS$1,
719
- loadingTimeout: _loadingTimeout = DEFAULT_LOADING_TIMEOUT_MS$1
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
- const [webViewUrl, setWebViewUrl] = react.useState(() => assignStartTimeToUrl(assignEnvironmentIdToUrl(initialWebViewUrl, core.environmentId)));
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
- * Block the message transport when the webview is unmounted
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
- logger.warn(unmountWebViewWarningMessage);
749
- core.messageTransport.block();
750
- },
751
- // eslint-disable-next-line react-hooks/exhaustive-deps -- we only care about the unmount callback
752
- []);
753
- const blockAndReloadWebView = react.useCallback(reason => {
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
- }, [core]);
784
- const blockAndReloadWebViewOsKill = react.useCallback(() => {
785
- recordEvent('os_kill');
786
- blockAndReloadWebView('os_kill');
787
- }, [blockAndReloadWebView, recordEvent]);
788
- react.useEffect(() => core.messageTransport.recoveryManager.onRecoveryRequested(() => blockAndReloadWebView('recovery')), [core, blockAndReloadWebView]);
789
- const setWebViewLoadError = react.useCallback(phase => {
790
- // Error was already thrown, do not throw again
791
- if (core.initialization.error instanceof WebViewFailedToLoadError) return;
792
- core.initialization.error = new WebViewFailedToLoadError(getMeta({
793
- phase
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
- }, [core, getMeta]);
796
- /**
797
- * Wraps `setWebViewLoadError` so the consumer doesn't need to know
798
- * about the phase enum; each callsite below binds its own phase.
799
- */
800
- const setWebViewLoadErrorWithPhase = react.useCallback(phase => () => setWebViewLoadError(phase), [setWebViewLoadError]);
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
- * Reload the webview with a clean state when a timeout is reached
803
- * and the webview did not get to the loaded state yet
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 startRecoveryTimeout = useWebViewRecoveryTimeout({
806
- core,
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 webViewLoadErrorCountRef = react.useRef(0);
814
- const onWebViewLoadError = react.useCallback(() => {
815
- webViewLoadErrorCountRef.current = webViewLoadErrorCountRef.current + 1;
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: webViewUrlString
392
+ uri: currentAttemptUrlString
845
393
  },
846
394
  containerStyle: containerStyles,
847
395
  style: styles['webview'],
848
396
  onMessage: onMessageHandler,
849
- onLoadEnd: onLoadEnd,
850
- onLoadStart: onLoadStart,
851
- onLoad: onLoad,
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: blockAndReloadWebViewOsKill,
855
- onRenderProcessGone: blockAndReloadWebViewOsKill,
856
- onError: onWebViewLoadError,
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
- }, webViewUrl)
862
- }, webViewUrlString);
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
- const DEFAULT_LOADING_TIMEOUT_MS = 20000;
967
- const DEFAULT_RECOVERY_TIMEOUT_MS = 20000;
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
- loadingTimeoutMs: _loadingTimeoutMs = DEFAULT_LOADING_TIMEOUT_MS,
987
- recoveryTimeoutMs: _recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS
503
+ webviewLoad
988
504
  }) => {
989
505
  const native = getEmbeddedWebView();
990
- const builtUrl = assignStartTimeToUrl(assignEnvironmentIdToUrl(webviewUrl, core.environmentId));
991
- const phaseTimers = createEmbeddedWebViewPhaseTimers({
992
- core,
993
- loadingTimeoutMs: _loadingTimeoutMs,
994
- recoveryTimeoutMs: _recoveryTimeoutMs,
995
- webViewUrl: builtUrl
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
- let loadingTimer = null;
1002
- let recoveryTimer = null;
1003
- let hasFailed = false;
1004
- let hasEmittedSuccessLog = false;
1005
- let tornDown = false;
1006
- const clearLoadingTimer = () => {
1007
- if (loadingTimer !== null) {
1008
- clearTimeout(loadingTimer);
1009
- loadingTimer = null;
1010
- }
1011
- };
1012
- const clearRecoveryTimer = () => {
1013
- if (recoveryTimer !== null) {
1014
- clearTimeout(recoveryTimer);
1015
- recoveryTimer = null;
1016
- }
1017
- };
1018
- // Raise `WebViewFailedToLoadError` once per setup. Subsequent triggers
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
- }, builtUrl);
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
- phaseTimers.recordEvent('load_start');
1113
- // Re-arm both timers on every load_start. A retry from the native
1114
- // side (e.g. a redirect, or a reload after a transient failure)
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
- phaseTimers.recordEvent('load');
1126
- clearLoadingTimer();
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
- phaseTimers.recordEvent('load_end');
1131
- clearLoadingTimer();
1132
- clearRecoveryTimer();
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
- raiseFailure('embedded_native_error', {
1142
- nativeErrorCode: event.code,
1143
- nativeErrorDescription: event.description,
1144
- nativeErrorDomain: event.domain,
1145
- nativeErrorFailedUrl: event.url,
1146
- nativeErrorIsProvisional: event.isProvisional
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
- // Arm the loading timer before calling setUrl so the pre-onLoadStart window
1153
- // is covered. On cold boot the native 2s navigation-decision timeout can
1154
- // fire before JS responds to onShouldStartLoad, silently cancelling the load
1155
- // without emitting onLoadError leaving onLoadStart (and therefore the
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
- var _a;
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
- clearLoadingTimer();
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;