@dynamic-labs/react-native-extension 4.83.1 → 4.84.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 (32) hide show
  1. package/android/EmbeddedWebViewController.kt +26 -0
  2. package/android/EmbeddedWebViewModule.kt +8 -1
  3. package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
  4. package/android/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
  5. package/android/embeddedwebview/EmbeddedWebViewController.kt +26 -0
  6. package/android/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
  7. package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
  8. package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
  9. package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
  10. package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
  11. package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
  12. package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
  13. package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
  14. package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
  15. package/index.cjs +601 -57
  16. package/index.js +602 -58
  17. package/ios/EmbeddedWebViewController.swift +22 -0
  18. package/ios/EmbeddedWebViewModule.swift +8 -1
  19. package/package.json +6 -6
  20. package/src/components/WebView/EmbeddedWebView/EmbeddedWebView.d.ts +20 -1
  21. package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/embeddedWebViewPhaseTimers.d.ts +66 -0
  22. package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/index.d.ts +1 -0
  23. package/src/components/WebView/successLog/emitSuccessLog.d.ts +35 -0
  24. package/src/components/WebView/successLog/index.d.ts +3 -0
  25. package/src/components/WebView/successLog/successLogCooldown.d.ts +39 -0
  26. package/src/components/WebView/useWebViewPhaseTimers/index.d.ts +1 -0
  27. package/src/components/WebView/useWebViewPhaseTimers/useWebViewPhaseTimers.d.ts +46 -0
  28. package/src/components/WebView/useWebViewSuccessLog/index.d.ts +1 -0
  29. package/src/components/WebView/useWebViewSuccessLog/useWebViewSuccessLog.d.ts +28 -0
  30. package/src/components/WebView/webViewPhaseTimerCore/index.d.ts +2 -0
  31. package/src/components/WebView/webViewPhaseTimerCore/webViewPhaseTimerCore.d.ts +123 -0
  32. package/src/nativeModules/EmbeddedWebView.d.ts +27 -2
package/index.js CHANGED
@@ -1,18 +1,19 @@
1
1
  import { assertPackageVersion } from '@dynamic-labs/assert-package-version';
2
2
  import { StyleSheet, Platform } from 'react-native';
3
- import { useEffect, useRef, useState, useCallback } from 'react';
3
+ import { useEffect, useRef, useCallback, useState, useMemo } from 'react';
4
4
  import { WebView as WebView$1 } from 'react-native-webview';
5
5
  import { Logger } from '@dynamic-labs/logger';
6
6
  import { messageTransportDataJsonReviver, parseMessageTransportData, messageTransportDataJsonReplacer, createRequestChannel } from '@dynamic-labs/message-transport';
7
+ import { sdkHasLoadedEventName } from '@dynamic-labs/webview-messages';
8
+ import { setItemAsync, getItemAsync, deleteItemAsync } from 'expo-secure-store';
7
9
  import { jsx } from 'react/jsx-runtime';
8
10
  import { requireNativeModule } from 'expo-modules-core';
9
11
  import { Passkey } from 'react-native-passkey';
10
12
  import { createURL, getInitialURL, addEventListener, openURL } from 'expo-linking';
11
13
  import { openAuthSessionAsync } from 'expo-web-browser';
12
- import { getItemAsync, deleteItemAsync, setItemAsync } from 'expo-secure-store';
13
14
  import { createPasskey, PasskeyStamper } from '@turnkey/react-native-passkey-stamper';
14
15
 
15
- var version = "4.83.1";
16
+ var version = "4.84.0";
16
17
 
17
18
  function _extends() {
18
19
  return _extends = Object.assign ? Object.assign.bind() : function (n) {
@@ -100,6 +101,396 @@ const useMessageTransportWebViewBridge = (core, webViewRef) => {
100
101
  };
101
102
  };
102
103
 
104
+ const CLEAR_STATE_PARAM = 'clear-state';
105
+ const ENVIRONMENT_ID_PARAM = 'environmentId';
106
+ const WEBVIEW_START_TIME_PARAM = 'webviewStartTime';
107
+
108
+ const hasClearStateInUrl = webViewUrl => Boolean(webViewUrl.searchParams.get(CLEAR_STATE_PARAM));
109
+
110
+ /**
111
+ * Wire-format `type` of a webview-side `manifest` request.
112
+ *
113
+ * `requestChannel.request('manifest')` sends a `MessageTransportData` whose
114
+ * `type` is the literal request name. The host replies with `manifest__ack`,
115
+ * which we ignore: receiving the request is sufficient evidence that the
116
+ * webview JS bundle is alive and the message bridge works.
117
+ */
118
+ const MANIFEST_MESSAGE_TYPE = 'manifest';
119
+ const emptyTimings = () => ({
120
+ htmlLoadStartedAt: null,
121
+ htmlLoadedAt: null,
122
+ manifestReceivedAt: null,
123
+ onLoadEndAt: null,
124
+ sdkReadyAt: null
125
+ });
126
+ const diffMs = (later, earlier) => later !== null && earlier !== null ? later - earlier : null;
127
+ /**
128
+ * Derive cross-phase durations from raw timestamps. The four durations
129
+ * are what end up in the failure / success log meta:
130
+ *
131
+ * - `htmlLoadMs` — `onLoadStart` to `onLoad`; native HTML fetch
132
+ * - `onLoadToOnLoadEndMs` — `onLoad` to `onLoadEnd`; native render/parse
133
+ * - `manifestReceivedMs` — `onLoadEnd` to first `manifest` request from
134
+ * the webview JS; "JS bundle is alive" signal
135
+ * - `sdkReadyMs` — `onLoadEnd` to first `sdkHasLoaded` event from the
136
+ * webview JS; "SDK fully bootstrapped" signal
137
+ */
138
+ const computePhaseDurations = timings => ({
139
+ htmlLoadMs: diffMs(timings.htmlLoadedAt, timings.htmlLoadStartedAt),
140
+ manifestReceivedMs: diffMs(timings.manifestReceivedAt, timings.onLoadEndAt),
141
+ onLoadToOnLoadEndMs: diffMs(timings.onLoadEndAt, timings.htmlLoadedAt),
142
+ sdkReadyMs: diffMs(timings.sdkReadyAt, timings.onLoadEndAt)
143
+ });
144
+ /**
145
+ * Build the success-shape meta from a {@link WebViewPhaseTimerCore}
146
+ * snapshot plus the path-specific fields. Both call sites call this
147
+ * with their own context (RN reads `hadClearState` / `retryCount` from
148
+ * the URL; embedded passes constants) so the success-log payload stays
149
+ * identical across the two paths.
150
+ */
151
+ const buildSuccessMeta = ({
152
+ core,
153
+ hadClearState,
154
+ loadingTimeoutMs,
155
+ recoveryTimeoutMs,
156
+ retryCount,
157
+ webviewUrl
158
+ }) => Object.assign(Object.assign({
159
+ hadClearState,
160
+ loadingTimeoutMs,
161
+ recoveryTimeoutMs,
162
+ retryCount,
163
+ webviewUrl
164
+ }, computePhaseDurations(core.getTimings())), core.getCounters());
165
+ /**
166
+ * Shared state machine driving WebView load-phase instrumentation.
167
+ *
168
+ * Both the React `<WebView>` (RN bridge) and the native embedded WebView
169
+ * (native bridge) feed lifecycle events into this core and produce
170
+ * matching meta from the same timings + counters. Each path layers its
171
+ * own path-specific fields (`hadClearState`, `retryCount`, etc.) on top
172
+ * of the raw state this core exposes via `getTimings()` / `getCounters()`.
173
+ *
174
+ * The core owns the `core.messageTransport` subscription that captures
175
+ * the two webview-originated signals bracketing SDK bootstrap
176
+ * (`manifest` once the JS bundle is alive; `sdkHasLoadedEventName`
177
+ * once the SDK is fully ready). Native bridge lifecycle events come in
178
+ * via `recordEvent` instead.
179
+ */
180
+ const createWebViewPhaseTimerCore = ({
181
+ core
182
+ }) => {
183
+ let timings = emptyTimings();
184
+ let nativeErrorCount = 0;
185
+ let osKillCount = 0;
186
+ /**
187
+ * We only capture the first occurrence per attempt so that a healthy
188
+ * reload doesn't overwrite the timing from the attempt that ultimately
189
+ * failed.
190
+ */
191
+ const handleMessage = message => {
192
+ if (message.origin !== 'webview') return;
193
+ if (message.type === MANIFEST_MESSAGE_TYPE && timings.manifestReceivedAt === null) {
194
+ timings.manifestReceivedAt = Date.now();
195
+ }
196
+ if (message.type === sdkHasLoadedEventName && timings.sdkReadyAt === null) {
197
+ timings.sdkReadyAt = Date.now();
198
+ }
199
+ };
200
+ core.messageTransport.on(handleMessage);
201
+ return {
202
+ dispose: () => {
203
+ core.messageTransport.off(handleMessage);
204
+ },
205
+ getCounters: () => ({
206
+ nativeErrorCount,
207
+ osKillCount
208
+ }),
209
+ getTimings: () => timings,
210
+ recordEvent: event => {
211
+ const now = Date.now();
212
+ switch (event) {
213
+ case 'load_start':
214
+ timings = emptyTimings();
215
+ timings.htmlLoadStartedAt = now;
216
+ break;
217
+ case 'load':
218
+ timings.htmlLoadedAt = now;
219
+ break;
220
+ case 'load_end':
221
+ timings.onLoadEndAt = now;
222
+ break;
223
+ case 'native_error':
224
+ nativeErrorCount += 1;
225
+ break;
226
+ case 'os_kill':
227
+ osKillCount += 1;
228
+ break;
229
+ }
230
+ }
231
+ };
232
+ };
233
+
234
+ const RETRY_QUERY_PARAM = 'retry';
235
+ const readRetryCount = webViewUrl => {
236
+ const raw = webViewUrl.searchParams.get(RETRY_QUERY_PARAM);
237
+ if (raw === null) return 0;
238
+ const parsed = Number(raw);
239
+ return Number.isFinite(parsed) ? parsed : 0;
240
+ };
241
+ /**
242
+ * Snapshot returned by the phase-timer core when it hasn't been
243
+ * instantiated yet (briefly true between hook construction and the
244
+ * `useEffect` running). Lets `getSuccessMeta` / `getMeta` produce a
245
+ * well-formed empty meta in that window without special-casing each
246
+ * field.
247
+ */
248
+ const EMPTY_CORE_SNAPSHOT = {
249
+ getCounters: () => ({
250
+ nativeErrorCount: 0,
251
+ osKillCount: 0
252
+ }),
253
+ getTimings: () => ({
254
+ htmlLoadStartedAt: null,
255
+ htmlLoadedAt: null,
256
+ manifestReceivedAt: null,
257
+ onLoadEndAt: null,
258
+ sdkReadyAt: null
259
+ })
260
+ };
261
+ /**
262
+ * Tracks how long each step of the WebView load took, so when we raise
263
+ * `WebViewFailedToLoadError` we can attach the timings to the error log.
264
+ *
265
+ * The timings cover the host-side phases of the load — the webview-side
266
+ * timings (`webview.time_to_load_manifest`, `webview.time_to_sdk_ready`)
267
+ * still come from the webview itself once it boots, but those don't help
268
+ * when the webview never boots in the first place.
269
+ *
270
+ * The actual state machine lives in {@link createWebViewPhaseTimerCore},
271
+ * which is also reused by the embedded native WebView path. This hook
272
+ * only layers the React-specific bits (ref-based core lifetime tied to
273
+ * `useEffect`, URL-derived `hadClearState` / `retryCount`).
274
+ */
275
+ const useWebViewPhaseTimers = ({
276
+ core,
277
+ loadingTimeout,
278
+ recoveryTimeout,
279
+ webViewUrl
280
+ }) => {
281
+ const phaseCoreRef = useRef(null);
282
+ const webViewUrlRef = useRef(webViewUrl);
283
+ webViewUrlRef.current = webViewUrl;
284
+ /**
285
+ * Owning the message-transport subscription in `useEffect` matches the
286
+ * lifetime semantics any other React subscription would have, and lets
287
+ * us cleanly re-subscribe if `core` is ever swapped at runtime.
288
+ */
289
+ useEffect(() => {
290
+ const phaseCore = createWebViewPhaseTimerCore({
291
+ core
292
+ });
293
+ phaseCoreRef.current = phaseCore;
294
+ return () => {
295
+ phaseCore.dispose();
296
+ if (phaseCoreRef.current === phaseCore) {
297
+ phaseCoreRef.current = null;
298
+ }
299
+ };
300
+ }, [core]);
301
+ const recordEvent = useCallback(event => {
302
+ var _a;
303
+ (_a = phaseCoreRef.current) === null || _a === void 0 ? void 0 : _a.recordEvent(event);
304
+ }, []);
305
+ const getSuccessMeta = useCallback(() => {
306
+ var _a;
307
+ const url = webViewUrlRef.current;
308
+ return buildSuccessMeta({
309
+ core: (_a = phaseCoreRef.current) !== null && _a !== void 0 ? _a : EMPTY_CORE_SNAPSHOT,
310
+ hadClearState: hasClearStateInUrl(url),
311
+ loadingTimeoutMs: loadingTimeout,
312
+ recoveryTimeoutMs: recoveryTimeout,
313
+ retryCount: readRetryCount(url),
314
+ webviewUrl: url.toString()
315
+ });
316
+ }, [loadingTimeout, recoveryTimeout]);
317
+ const getMeta = useCallback(({
318
+ phase
319
+ }) => Object.assign(Object.assign({}, getSuccessMeta()), {
320
+ phase
321
+ }), [getSuccessMeta]);
322
+ return {
323
+ getMeta,
324
+ getSuccessMeta,
325
+ recordEvent
326
+ };
327
+ };
328
+
329
+ /******************************************************************************
330
+ Copyright (c) Microsoft Corporation.
331
+
332
+ Permission to use, copy, modify, and/or distribute this software for any
333
+ purpose with or without fee is hereby granted.
334
+
335
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
336
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
337
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
338
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
339
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
340
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
341
+ PERFORMANCE OF THIS SOFTWARE.
342
+ ***************************************************************************** */
343
+
344
+ function __awaiter(thisArg, _arguments, P, generator) {
345
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
346
+ return new (P || (P = Promise))(function (resolve, reject) {
347
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
348
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
349
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
350
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
351
+ });
352
+ }
353
+
354
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
355
+ var e = new Error(message);
356
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
357
+ };
358
+
359
+ const SUCCESS_LOG_KEY = 'webview.load_succeeded';
360
+ /**
361
+ * Shared emission pattern for the `webview.load_succeeded`
362
+ * instrumentation log. Both the RN `<WebView>` and the embedded
363
+ * WebView fire the same event with the same payload shape after
364
+ * checking the same cooldown gate — this helper centralises that
365
+ * sequence so the two call sites stay aligned and any future field we
366
+ * add to the success log lands in both places automatically.
367
+ */
368
+ const emitSuccessLog = _a => __awaiter(void 0, [_a], void 0, function* ({
369
+ cooldown,
370
+ core,
371
+ getMeta,
372
+ name
373
+ }) {
374
+ if (!(yield cooldown.shouldEmit())) return;
375
+ logger.instrument(`${name} loaded successfully`, Object.assign({
376
+ environmentId: core.environmentId,
377
+ hostSdkSessionId: core.hostSdkSessionId,
378
+ key: SUCCESS_LOG_KEY,
379
+ time: 0
380
+ }, getMeta()));
381
+ yield cooldown.recordEmitted();
382
+ });
383
+
384
+ const SUCCESS_LOG_COOLDOWN_MS = 60 * 60 * 1000;
385
+ const parseStoredTimestamp = raw => {
386
+ if (raw === null) return null;
387
+ const parsed = Number(raw);
388
+ return Number.isFinite(parsed) ? parsed : null;
389
+ };
390
+ /**
391
+ * Create a device-local cooldown gate for a `webview.load_succeeded`
392
+ * instrumentation log.
393
+ *
394
+ * Both `<WebView>` (RN bridge) and `<EmbeddedWebView>` (native bridge)
395
+ * emit the same success log; each instantiates its own cooldown with a
396
+ * dedicated storage key so emitting one log doesn't starve the other
397
+ * for an app that mounts both.
398
+ *
399
+ * - `shouldEmit` returns `true` when the cooldown has elapsed (or no
400
+ * timestamp has ever been stored) and `false` while inside the
401
+ * window. If secure-store throws on read we default to emitting —
402
+ * losing a diagnostic log is worse than the rare duplicate.
403
+ * - `recordEmitted` persists the current timestamp so subsequent calls
404
+ * within the window are suppressed. Write failures are swallowed
405
+ * (logged as a warning) so they don't break the host wrapper —
406
+ * worst case the next emit also goes through.
407
+ */
408
+ const createSuccessLogCooldown = ({
409
+ name,
410
+ storageKey
411
+ }) => ({
412
+ recordEmitted: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (now = Date.now()) {
413
+ try {
414
+ yield setItemAsync(storageKey, now.toString());
415
+ } catch (error) {
416
+ logger.warn(`Failed to persist ${name} success-log cooldown timestamp`, {
417
+ error: String(error)
418
+ });
419
+ }
420
+ }),
421
+ shouldEmit: (...args_2) => __awaiter(void 0, [...args_2], void 0, function* (now = Date.now()) {
422
+ try {
423
+ const raw = yield getItemAsync(storageKey);
424
+ const lastEmittedAt = parseStoredTimestamp(raw);
425
+ if (lastEmittedAt === null) return true;
426
+ return now - lastEmittedAt >= SUCCESS_LOG_COOLDOWN_MS;
427
+ } catch (error) {
428
+ logger.warn(`Failed to read ${name} success-log cooldown timestamp; emitting anyway`, {
429
+ error: String(error)
430
+ });
431
+ return true;
432
+ }
433
+ })
434
+ });
435
+
436
+ const SUCCESS_LOG_COOLDOWN_STORAGE_KEY = 'dynamic.webview.successLogLastEmittedAt';
437
+ /**
438
+ * Emit a `webview.load_succeeded` instrumentation log the first time
439
+ * the SDK signals it's ready, gated by a device-local 1h cooldown so a
440
+ * customer with many WebView mounts (or many app launches) doesn't spam
441
+ * the logs pipeline.
442
+ *
443
+ * The error path is unaffected: failures keep going through
444
+ * `logger.error(WebViewFailedToLoadError, meta)` with no cooldown.
445
+ *
446
+ * The hook intentionally fires only once per mount (`hasEmittedRef`):
447
+ * after a successful boot we don't expect another `sdkHasLoaded` for
448
+ * the same WebView lifetime, and emitting twice for the same load would
449
+ * be misleading.
450
+ *
451
+ * Cooldown gating + log emission are delegated to the shared
452
+ * `successLog` helpers so this hook and the embedded native WebView
453
+ * path stay in lockstep on payload shape and storage semantics; the
454
+ * dedicated storage key here keeps the RN-path cooldown independent
455
+ * from the embedded path's.
456
+ */
457
+ const useWebViewSuccessLog = ({
458
+ core,
459
+ getSuccessMeta
460
+ }) => {
461
+ const hasEmittedRef = useRef(false);
462
+ const getSuccessMetaRef = useRef(getSuccessMeta);
463
+ getSuccessMetaRef.current = getSuccessMeta;
464
+ useEffect(() => {
465
+ const cooldown = createSuccessLogCooldown({
466
+ name: 'webview',
467
+ storageKey: SUCCESS_LOG_COOLDOWN_STORAGE_KEY
468
+ });
469
+ const handler = message => __awaiter(void 0, void 0, void 0, function* () {
470
+ if (message.origin !== 'webview') return;
471
+ if (message.type !== sdkHasLoadedEventName) return;
472
+ if (hasEmittedRef.current) return;
473
+ hasEmittedRef.current = true;
474
+ try {
475
+ yield emitSuccessLog({
476
+ cooldown,
477
+ core,
478
+ getMeta: getSuccessMetaRef.current,
479
+ name: 'Webview'
480
+ });
481
+ } catch (err) {
482
+ logger.warn('useWebViewSuccessLog.emitSuccessLog failed', {
483
+ error: String(err)
484
+ });
485
+ }
486
+ });
487
+ core.messageTransport.on(handler);
488
+ return () => {
489
+ core.messageTransport.off(handler);
490
+ };
491
+ }, [core]);
492
+ };
493
+
103
494
  const useWebViewVisibility = core => {
104
495
  const webViewVisibilityRequestChannelRef = useRef(createRequestChannel(core.messageTransport));
105
496
  const [visible, setVisible] = useState(false);
@@ -136,10 +527,6 @@ const styles = StyleSheet.create({
136
527
  }
137
528
  });
138
529
 
139
- const CLEAR_STATE_PARAM = 'clear-state';
140
- const ENVIRONMENT_ID_PARAM = 'environmentId';
141
- const WEBVIEW_START_TIME_PARAM = 'webviewStartTime';
142
-
143
530
  const setClearStateToUrl = webViewUrl => {
144
531
  const newWebViewUrl = new URL(webViewUrl);
145
532
  newWebViewUrl.searchParams.set(CLEAR_STATE_PARAM, 'true');
@@ -147,8 +534,6 @@ const setClearStateToUrl = webViewUrl => {
147
534
  return newWebViewUrl;
148
535
  };
149
536
 
150
- const hasClearStateInUrl = webViewUrl => Boolean(webViewUrl.searchParams.get(CLEAR_STATE_PARAM));
151
-
152
537
  const useWebViewRecoveryTimeout = ({
153
538
  core,
154
539
  webViewUrl,
@@ -301,15 +686,15 @@ const shouldAllowNavigation = (request, webViewUrl) => {
301
686
  return false;
302
687
  };
303
688
 
304
- const DEFAULT_RECOVERY_TIMEOUT_MS = 20000;
305
- const DEFAULT_LOADING_TIMEOUT_MS = 20000;
689
+ const DEFAULT_RECOVERY_TIMEOUT_MS$1 = 20000;
690
+ const DEFAULT_LOADING_TIMEOUT_MS$1 = 20000;
306
691
  const WebView = ({
307
692
  webviewUrl: initialWebViewUrl,
308
693
  core,
309
694
  webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
310
695
  disableRecovery: _disableRecovery = false,
311
- recoveryTimeout: _recoveryTimeout = DEFAULT_RECOVERY_TIMEOUT_MS,
312
- loadingTimeout: _loadingTimeout = DEFAULT_LOADING_TIMEOUT_MS
696
+ recoveryTimeout: _recoveryTimeout = DEFAULT_RECOVERY_TIMEOUT_MS$1,
697
+ loadingTimeout: _loadingTimeout = DEFAULT_LOADING_TIMEOUT_MS$1
313
698
  }) => {
314
699
  const webViewRef = useRef(null);
315
700
  const {
@@ -319,6 +704,20 @@ const WebView = ({
319
704
  const {
320
705
  onMessageHandler
321
706
  } = useMessageTransportWebViewBridge(core, webViewRef);
707
+ const {
708
+ recordEvent,
709
+ getMeta,
710
+ getSuccessMeta
711
+ } = useWebViewPhaseTimers({
712
+ core,
713
+ loadingTimeout: _loadingTimeout,
714
+ recoveryTimeout: _recoveryTimeout,
715
+ webViewUrl
716
+ });
717
+ useWebViewSuccessLog({
718
+ core,
719
+ getSuccessMeta
720
+ });
322
721
  const containerStyles = [styles['container'], visible ? styles.show : styles.hide];
323
722
  /**
324
723
  * Block the message transport when the webview is unmounted
@@ -360,13 +759,23 @@ const WebView = ({
360
759
  return newWebViewUrl;
361
760
  });
362
761
  }, [core]);
363
- const blockAndReloadWebViewOsKill = useCallback(() => blockAndReloadWebView('os_kill'), [blockAndReloadWebView]);
762
+ const blockAndReloadWebViewOsKill = useCallback(() => {
763
+ recordEvent('os_kill');
764
+ blockAndReloadWebView('os_kill');
765
+ }, [blockAndReloadWebView, recordEvent]);
364
766
  useEffect(() => core.messageTransport.recoveryManager.onRecoveryRequested(() => blockAndReloadWebView('recovery')), [core, blockAndReloadWebView]);
365
- const setWebViewLoadError = useCallback(() => {
767
+ const setWebViewLoadError = useCallback(phase => {
366
768
  // Error was already thrown, do not throw again
367
769
  if (core.initialization.error instanceof WebViewFailedToLoadError) return;
368
- core.initialization.error = new WebViewFailedToLoadError();
369
- }, [core]);
770
+ core.initialization.error = new WebViewFailedToLoadError(getMeta({
771
+ phase
772
+ }));
773
+ }, [core, getMeta]);
774
+ /**
775
+ * Wraps `setWebViewLoadError` so the consumer doesn't need to know
776
+ * about the phase enum; each callsite below binds its own phase.
777
+ */
778
+ const setWebViewLoadErrorWithPhase = useCallback(phase => () => setWebViewLoadError(phase), [setWebViewLoadError]);
370
779
  /**
371
780
  * Reload the webview with a clean state when a timeout is reached
372
781
  * and the webview did not get to the loaded state yet
@@ -374,7 +783,7 @@ const WebView = ({
374
783
  const startRecoveryTimeout = useWebViewRecoveryTimeout({
375
784
  core,
376
785
  disableRecovery: _disableRecovery,
377
- onFailedToLoadAfterClearState: setWebViewLoadError,
786
+ onFailedToLoadAfterClearState: setWebViewLoadErrorWithPhase('after_clear_state'),
378
787
  recoveryTimeout: _recoveryTimeout,
379
788
  setWebViewUrl,
380
789
  webViewUrl
@@ -382,26 +791,40 @@ const WebView = ({
382
791
  const webViewLoadErrorCountRef = useRef(0);
383
792
  const onWebViewLoadError = useCallback(() => {
384
793
  webViewLoadErrorCountRef.current = webViewLoadErrorCountRef.current + 1;
794
+ recordEvent('native_error');
385
795
  /**
386
796
  * This is the first attempt to load the webview, do not throw an error
387
797
  * because the recovery system will attempt to reload the webview
388
798
  */
389
799
  if (webViewLoadErrorCountRef.current === 1) return;
390
- setWebViewLoadError();
391
- }, [setWebViewLoadError]);
800
+ setWebViewLoadError('native_error');
801
+ }, [recordEvent, setWebViewLoadError]);
392
802
  const {
393
- onLoad,
394
- onLoadStart
395
- } = useWebViewLoadingTimeout(_loadingTimeout, setWebViewLoadError);
803
+ onLoad: onLoadFromTimeout,
804
+ onLoadStart: onLoadStartFromTimeout
805
+ } = useWebViewLoadingTimeout(_loadingTimeout, setWebViewLoadErrorWithPhase('html_load'));
806
+ const onLoadStart = useCallback(() => {
807
+ recordEvent('load_start');
808
+ onLoadStartFromTimeout();
809
+ }, [onLoadStartFromTimeout, recordEvent]);
810
+ const onLoad = useCallback(() => {
811
+ recordEvent('load');
812
+ onLoadFromTimeout();
813
+ }, [onLoadFromTimeout, recordEvent]);
814
+ const onLoadEnd = useCallback(() => {
815
+ recordEvent('load_end');
816
+ startRecoveryTimeout();
817
+ }, [recordEvent, startRecoveryTimeout]);
818
+ const webViewUrlString = useMemo(() => webViewUrl.toString(), [webViewUrl]);
396
819
  return /*#__PURE__*/jsx(WebView$1, {
397
820
  ref: webViewRef,
398
821
  source: {
399
- uri: webViewUrl.toString()
822
+ uri: webViewUrlString
400
823
  },
401
824
  containerStyle: containerStyles,
402
825
  style: styles['webview'],
403
826
  onMessage: onMessageHandler,
404
- onLoadEnd: () => startRecoveryTimeout(),
827
+ onLoadEnd: onLoadEnd,
405
828
  onLoadStart: onLoadStart,
406
829
  onLoad: onLoad,
407
830
  hideKeyboardAccessoryView: true,
@@ -414,7 +837,7 @@ const WebView = ({
414
837
  isTopFrame: request.isTopFrame,
415
838
  url: request.url
416
839
  }, webViewUrl)
417
- });
840
+ }, webViewUrlString);
418
841
  };
419
842
  const createWebView = internalProps => {
420
843
  const WebViewWrapper = props => /*#__PURE__*/jsx(WebView, _extends({}, internalProps, props));
@@ -453,6 +876,53 @@ const getEmbeddedWebView = () => {
453
876
  }
454
877
  };
455
878
 
879
+ /**
880
+ * Non-React equivalent of `useWebViewPhaseTimers` for the embedded native
881
+ * webview path. The embedded path is a singleton wired by
882
+ * `setupEmbeddedWebView` (not a React component), so we expose the same
883
+ * phase-timing surface as a plain factory.
884
+ *
885
+ * Both this factory and the hook delegate the actual state machine to
886
+ * {@link createWebViewPhaseTimerCore}; the only path-specific bits are:
887
+ *
888
+ * - `hadClearState` is always `false` (embedded path has no clear-state
889
+ * recovery model — that's exclusive to the React `<WebView>` path's
890
+ * loading-timeout recovery flow)
891
+ * - `retryCount` is always `0` (embedded path has no retry counter URL
892
+ * query param either)
893
+ */
894
+ const createEmbeddedWebViewPhaseTimers = ({
895
+ core,
896
+ loadingTimeoutMs,
897
+ recoveryTimeoutMs,
898
+ webViewUrl
899
+ }) => {
900
+ const phaseCore = createWebViewPhaseTimerCore({
901
+ core
902
+ });
903
+ const getSuccessMeta = () => buildSuccessMeta({
904
+ core: phaseCore,
905
+ hadClearState: false,
906
+ loadingTimeoutMs,
907
+ recoveryTimeoutMs,
908
+ retryCount: 0,
909
+ webviewUrl: webViewUrl.toString()
910
+ });
911
+ return {
912
+ dispose: phaseCore.dispose,
913
+ getMeta: ({
914
+ phase
915
+ }) => Object.assign(Object.assign({}, getSuccessMeta()), {
916
+ phase
917
+ }),
918
+ getSuccessMeta,
919
+ recordEvent: phaseCore.recordEvent
920
+ };
921
+ };
922
+
923
+ const EMBEDDED_SUCCESS_LOG_COOLDOWN_STORAGE_KEY = 'dynamic.embeddedWebView.successLogLastEmittedAt';
924
+ const DEFAULT_LOADING_TIMEOUT_MS = 20000;
925
+ const DEFAULT_RECOVERY_TIMEOUT_MS = 20000;
456
926
  // Wires the JS-side bridge to the native WKWebView singleton owned by Swift.
457
927
  // This is intentionally not a React component: the embedded webview lives
458
928
  // outside the React tree (in a dedicated UIWindow on iOS), so its setup runs
@@ -465,10 +935,52 @@ const getEmbeddedWebView = () => {
465
935
  const setupEmbeddedWebView = ({
466
936
  webviewUrl,
467
937
  core,
468
- webviewDebuggingEnabled: _webviewDebuggingEnabled = false
938
+ webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
939
+ loadingTimeoutMs: _loadingTimeoutMs = DEFAULT_LOADING_TIMEOUT_MS,
940
+ recoveryTimeoutMs: _recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS
469
941
  }) => {
470
942
  const native = getEmbeddedWebView();
471
943
  const builtUrl = assignStartTimeToUrl(assignEnvironmentIdToUrl(webviewUrl, core.environmentId));
944
+ const phaseTimers = createEmbeddedWebViewPhaseTimers({
945
+ core,
946
+ loadingTimeoutMs: _loadingTimeoutMs,
947
+ recoveryTimeoutMs: _recoveryTimeoutMs,
948
+ webViewUrl: builtUrl
949
+ });
950
+ const successLogCooldown = createSuccessLogCooldown({
951
+ name: 'embedded webview',
952
+ storageKey: EMBEDDED_SUCCESS_LOG_COOLDOWN_STORAGE_KEY
953
+ });
954
+ let loadingTimer = null;
955
+ let recoveryTimer = null;
956
+ let hasFailed = false;
957
+ let hasEmittedSuccessLog = false;
958
+ const clearLoadingTimer = () => {
959
+ if (loadingTimer !== null) {
960
+ clearTimeout(loadingTimer);
961
+ loadingTimer = null;
962
+ }
963
+ };
964
+ const clearRecoveryTimer = () => {
965
+ if (recoveryTimer !== null) {
966
+ clearTimeout(recoveryTimer);
967
+ recoveryTimer = null;
968
+ }
969
+ };
970
+ // Raise `WebViewFailedToLoadError` once per setup. Subsequent triggers
971
+ // (e.g. recovery timeout firing after html_load already failed) are
972
+ // suppressed so we don't spam the initialization-error setter with
973
+ // duplicate logs for the same load attempt.
974
+ const raiseFailure = (phase, extraMeta = {}) => {
975
+ if (hasFailed) return;
976
+ hasFailed = true;
977
+ clearLoadingTimer();
978
+ clearRecoveryTimer();
979
+ const meta = phaseTimers.getMeta({
980
+ phase
981
+ });
982
+ core.initialization.error = new WebViewFailedToLoadError(Object.assign(Object.assign({}, meta), extraMeta));
983
+ };
472
984
  const visibilityChannel = createRequestChannel(core.messageTransport);
473
985
  const removeVisibilityHandler = visibilityChannel.handle('setVisibility', visible => {
474
986
  native.setVisible(visible).catch(err => {
@@ -482,6 +994,30 @@ const setupEmbeddedWebView = ({
482
994
  });
483
995
  };
484
996
  core.messageTransport.on(handleHostMessage);
997
+ // Watch for the webview-side SDK ready signal so we can clear the
998
+ // recovery timer and emit the success log. The phase-timer factory
999
+ // also subscribes for its own bookkeeping — both subscriptions are
1000
+ // independent and idempotent. The factory subscribes first and so
1001
+ // sees `sdkReadyAt` before this handler runs, which means the meta
1002
+ // we emit below has the freshest `sdkReadyMs`. We only emit once
1003
+ // per setup; a second SDK-ready event for the same load wouldn't
1004
+ // represent a new successful boot.
1005
+ const handleSdkReady = message => {
1006
+ if (message.origin !== 'webview') return;
1007
+ if (message.type !== sdkHasLoadedEventName) return;
1008
+ clearRecoveryTimer();
1009
+ if (hasEmittedSuccessLog || hasFailed) return;
1010
+ hasEmittedSuccessLog = true;
1011
+ emitSuccessLog({
1012
+ cooldown: successLogCooldown,
1013
+ core,
1014
+ getMeta: phaseTimers.getSuccessMeta,
1015
+ name: 'Embedded webview'
1016
+ }).catch(err => {
1017
+ logger.warn('EmbeddedWebView.emitSuccessLog failed', err);
1018
+ });
1019
+ };
1020
+ core.messageTransport.on(handleSdkReady);
485
1021
  const onMessageSub = native.addListener('onMessage', event => {
486
1022
  let parsed = null;
487
1023
  try {
@@ -504,7 +1040,32 @@ const setupEmbeddedWebView = ({
504
1040
  logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
505
1041
  });
506
1042
  });
1043
+ const onLoadStartSub = native.addListener('onLoadStart', () => {
1044
+ phaseTimers.recordEvent('load_start');
1045
+ // Re-arm both timers on every load_start. A retry from the native
1046
+ // side (e.g. a redirect, or a reload after a transient failure)
1047
+ // resets the html_load window cleanly without leaving a stale timer
1048
+ // around from the previous attempt.
1049
+ clearLoadingTimer();
1050
+ clearRecoveryTimer();
1051
+ loadingTimer = setTimeout(() => {
1052
+ raiseFailure('html_load');
1053
+ }, _loadingTimeoutMs);
1054
+ });
1055
+ const onLoadSub = native.addListener('onLoad', () => {
1056
+ phaseTimers.recordEvent('load');
1057
+ clearLoadingTimer();
1058
+ });
1059
+ const onLoadEndSub = native.addListener('onLoadEnd', () => {
1060
+ phaseTimers.recordEvent('load_end');
1061
+ clearLoadingTimer();
1062
+ clearRecoveryTimer();
1063
+ recoveryTimer = setTimeout(() => {
1064
+ raiseFailure('sdk_bootstrap');
1065
+ }, _recoveryTimeoutMs);
1066
+ });
507
1067
  const onLoadErrorSub = native.addListener('onLoadError', event => {
1068
+ phaseTimers.recordEvent('native_error');
508
1069
  logger.warn('EmbeddedWebView load error', event);
509
1070
  logger.instrument('Embedded webview load error', {
510
1071
  environmentId: core.environmentId,
@@ -518,7 +1079,13 @@ const setupEmbeddedWebView = ({
518
1079
  time: 0,
519
1080
  webviewUrl: builtUrl.toString()
520
1081
  });
521
- core.initialization.error = new WebViewFailedToLoadError();
1082
+ raiseFailure('embedded_native_error', {
1083
+ nativeErrorCode: event.code,
1084
+ nativeErrorDescription: event.description,
1085
+ nativeErrorDomain: event.domain,
1086
+ nativeErrorFailedUrl: event.url,
1087
+ nativeErrorIsProvisional: event.isProvisional
1088
+ });
522
1089
  });
523
1090
  native.setDebuggingEnabled(_webviewDebuggingEnabled).catch(err => {
524
1091
  logger.warn('EmbeddedWebView.setDebuggingEnabled failed', err);
@@ -527,44 +1094,21 @@ const setupEmbeddedWebView = ({
527
1094
  logger.warn('EmbeddedWebView.setUrl failed', err);
528
1095
  });
529
1096
  return () => {
1097
+ clearLoadingTimer();
1098
+ clearRecoveryTimer();
530
1099
  removeVisibilityHandler();
531
1100
  core.messageTransport.off(handleHostMessage);
1101
+ core.messageTransport.off(handleSdkReady);
532
1102
  onMessageSub.remove();
533
1103
  onShouldStartLoadSub.remove();
1104
+ onLoadStartSub.remove();
1105
+ onLoadSub.remove();
1106
+ onLoadEndSub.remove();
534
1107
  onLoadErrorSub.remove();
1108
+ phaseTimers.dispose();
535
1109
  };
536
1110
  };
537
1111
 
538
- /******************************************************************************
539
- Copyright (c) Microsoft Corporation.
540
-
541
- Permission to use, copy, modify, and/or distribute this software for any
542
- purpose with or without fee is hereby granted.
543
-
544
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
545
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
546
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
547
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
548
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
549
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
550
- PERFORMANCE OF THIS SOFTWARE.
551
- ***************************************************************************** */
552
-
553
- function __awaiter(thisArg, _arguments, P, generator) {
554
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
555
- return new (P || (P = Promise))(function (resolve, reject) {
556
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
557
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
558
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
559
- step((generator = generator.apply(thisArg, _arguments || [])).next());
560
- });
561
- }
562
-
563
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
564
- var e = new Error(message);
565
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
566
- };
567
-
568
1112
  const setupPasskeyHandler = core => {
569
1113
  const passkeyRequestChannel = createRequestChannel(core.messageTransport);
570
1114
  passkeyRequestChannel.handle('passkey_register', _a => __awaiter(void 0, [_a], void 0, function* ({