@dynamic-labs/react-native-extension 4.83.2-alpha.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynamic-labs/react-native-extension",
3
- "version": "4.83.2-alpha.0",
3
+ "version": "4.84.0",
4
4
  "main": "./index.cjs",
5
5
  "module": "./index.js",
6
6
  "types": "./src/index.d.ts",
@@ -18,11 +18,11 @@
18
18
  "@turnkey/react-native-passkey-stamper": "1.2.7",
19
19
  "@react-native-documents/picker": "^11.0.0",
20
20
  "react-native-fs": ">=2.20.0",
21
- "@dynamic-labs/assert-package-version": "4.83.2-alpha.0",
22
- "@dynamic-labs/client": "4.83.2-alpha.0",
23
- "@dynamic-labs/logger": "4.83.2-alpha.0",
24
- "@dynamic-labs/message-transport": "4.83.2-alpha.0",
25
- "@dynamic-labs/webview-messages": "4.83.2-alpha.0"
21
+ "@dynamic-labs/assert-package-version": "4.84.0",
22
+ "@dynamic-labs/client": "4.84.0",
23
+ "@dynamic-labs/logger": "4.84.0",
24
+ "@dynamic-labs/message-transport": "4.84.0",
25
+ "@dynamic-labs/webview-messages": "4.84.0"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "react": ">=18.0.0 <20.0.0",
@@ -1,24 +1,31 @@
1
1
  import { Core } from '@dynamic-labs/client';
2
2
  import { WebViewFailedToLoadErrorMeta, WebViewFailedToLoadErrorPhase } from '../../../../errors/WebViewFailedToLoadError';
3
+ import { WebViewLoadSuccessMeta, WebViewPhaseEvent } from '../../webViewPhaseTimerCore';
3
4
  /**
4
- * Per-attempt lifecycle event recorded by
5
- * {@link createEmbeddedWebViewPhaseTimers}. `load_start` resets the per-attempt
6
- * state (so the timings reflect the current load); `native_error` and
7
- * `os_kill` increment cumulative counters that persist across reloads.
5
+ * Path-agnostic success-log meta. Re-exported here for callsite
6
+ * ergonomics so `setupEmbeddedWebView` doesn't need to reach into
7
+ * `webViewPhaseTimerCore` directly.
8
8
  */
9
- export type EmbeddedWebViewPhaseEvent = 'load_start' | 'load' | 'load_end' | 'native_error' | 'os_kill';
9
+ export type EmbeddedWebViewLoadSuccessMeta = WebViewLoadSuccessMeta;
10
+ /**
11
+ * Re-exported for callsite ergonomics: lets `setupEmbeddedWebView` import
12
+ * a single `EmbeddedWebViewPhaseEvent` type rather than reach into
13
+ * `webViewPhaseTimerCore` directly.
14
+ */
15
+ export type EmbeddedWebViewPhaseEvent = WebViewPhaseEvent;
10
16
  type EmbeddedWebViewPhaseTimersProps = {
11
17
  core: Core;
12
- webViewUrl: URL;
13
18
  loadingTimeoutMs: number;
14
19
  recoveryTimeoutMs: number;
20
+ webViewUrl: URL;
15
21
  };
16
22
  export type EmbeddedWebViewPhaseTimers = {
17
23
  /**
18
- * Record a per-attempt lifecycle event. See {@link EmbeddedWebViewPhaseEvent}
19
- * for the semantics of each event.
24
+ * Detach from the message transport. Call this on teardown so the
25
+ * embedded webview controller can be re-created without leaking
26
+ * subscriptions.
20
27
  */
21
- recordEvent: (event: EmbeddedWebViewPhaseEvent) => void;
28
+ dispose: () => void;
22
29
  /**
23
30
  * Build the structured meta to attach to
24
31
  * {@link WebViewFailedToLoadError}. The `phase` argument reflects the
@@ -29,24 +36,31 @@ export type EmbeddedWebViewPhaseTimers = {
29
36
  phase: WebViewFailedToLoadErrorPhase;
30
37
  }) => WebViewFailedToLoadErrorMeta;
31
38
  /**
32
- * Detach from the message transport. Call this on teardown so the
33
- * embedded webview controller can be re-created without leaking
34
- * subscriptions.
39
+ * Build the structured meta to attach to the `webview.load_succeeded`
40
+ * instrumentation log. Same timing fields as {@link getMeta} but without
41
+ * the failure-specific `phase` discriminator.
35
42
  */
36
- dispose: () => void;
43
+ getSuccessMeta: () => EmbeddedWebViewLoadSuccessMeta;
44
+ /**
45
+ * Record a per-attempt lifecycle event. See {@link WebViewPhaseEvent}
46
+ * for the semantics of each event.
47
+ */
48
+ recordEvent: (event: EmbeddedWebViewPhaseEvent) => void;
37
49
  };
38
50
  /**
39
51
  * Non-React equivalent of `useWebViewPhaseTimers` for the embedded native
40
- * webview path. The embedded path is a singleton wired by `setupEmbeddedWebView`
41
- * (not a React component), so we expose the same phase-timing surface as a
42
- * plain factory.
52
+ * webview path. The embedded path is a singleton wired by
53
+ * `setupEmbeddedWebView` (not a React component), so we expose the same
54
+ * phase-timing surface as a plain factory.
55
+ *
56
+ * Both this factory and the hook delegate the actual state machine to
57
+ * {@link createWebViewPhaseTimerCore}; the only path-specific bits are:
43
58
  *
44
- * The hook and the factory are intentionally separate: the embedded path does
45
- * not have a retry or clear-state model, so `retryCount` is always `0` and
46
- * `hadClearState` is always `false` in the meta produced here. Sharing one
47
- * implementation would require either pulling React into a hot non-React path
48
- * or threading a stateless adapter through both consumers \u2014 the duplication
49
- * is small enough that keeping them separate stays cleaner.
59
+ * - `hadClearState` is always `false` (embedded path has no clear-state
60
+ * recovery model that's exclusive to the React `<WebView>` path's
61
+ * loading-timeout recovery flow)
62
+ * - `retryCount` is always `0` (embedded path has no retry counter URL
63
+ * query param either)
50
64
  */
51
- export declare const createEmbeddedWebViewPhaseTimers: ({ core, webViewUrl, loadingTimeoutMs, recoveryTimeoutMs, }: EmbeddedWebViewPhaseTimersProps) => EmbeddedWebViewPhaseTimers;
65
+ export declare const createEmbeddedWebViewPhaseTimers: ({ core, loadingTimeoutMs, recoveryTimeoutMs, webViewUrl, }: EmbeddedWebViewPhaseTimersProps) => EmbeddedWebViewPhaseTimers;
52
66
  export {};
@@ -0,0 +1,35 @@
1
+ import { Core } from '@dynamic-labs/client';
2
+ import { SuccessLogCooldown } from './successLogCooldown';
3
+ export declare const SUCCESS_LOG_KEY = "webview.load_succeeded";
4
+ type SuccessLogMeta = Record<string, unknown>;
5
+ type EmitSuccessLogArgs<Meta extends SuccessLogMeta> = {
6
+ /**
7
+ * Device-local cooldown gate. If `shouldEmit` returns false the log
8
+ * is suppressed; on emit, the cooldown is recorded immediately so a
9
+ * crash between emit and `recordEmitted` doesn't replay the log.
10
+ */
11
+ cooldown: SuccessLogCooldown;
12
+ core: Core;
13
+ /**
14
+ * Late-bound meta builder. We call this only after the cooldown
15
+ * passes so paths that maintain expensive snapshots don't pay the
16
+ * cost when they're going to be suppressed.
17
+ */
18
+ getMeta: () => Meta;
19
+ /**
20
+ * Short identifier describing the path. Appears in the log message
21
+ * (`<name> loaded successfully`) so the two streams are
22
+ * distinguishable in Datadog without a per-path `key` field.
23
+ */
24
+ name: string;
25
+ };
26
+ /**
27
+ * Shared emission pattern for the `webview.load_succeeded`
28
+ * instrumentation log. Both the RN `<WebView>` and the embedded
29
+ * WebView fire the same event with the same payload shape after
30
+ * checking the same cooldown gate — this helper centralises that
31
+ * sequence so the two call sites stay aligned and any future field we
32
+ * add to the success log lands in both places automatically.
33
+ */
34
+ export declare const emitSuccessLog: <Meta extends SuccessLogMeta>({ cooldown, core, getMeta, name, }: EmitSuccessLogArgs<Meta>) => Promise<void>;
35
+ export {};
@@ -0,0 +1,3 @@
1
+ export { emitSuccessLog, SUCCESS_LOG_KEY } from './emitSuccessLog';
2
+ export { createSuccessLogCooldown, SUCCESS_LOG_COOLDOWN_MS, } from './successLogCooldown';
3
+ export type { SuccessLogCooldown } from './successLogCooldown';
@@ -0,0 +1,39 @@
1
+ export declare const SUCCESS_LOG_COOLDOWN_MS: number;
2
+ export type SuccessLogCooldown = {
3
+ recordEmitted: (now?: number) => Promise<void>;
4
+ shouldEmit: (now?: number) => Promise<boolean>;
5
+ };
6
+ type CreateSuccessLogCooldownArgs = {
7
+ /**
8
+ * Short identifier used only in warning log messages so storage
9
+ * failures are distinguishable in Datadog (e.g. `'webview'` vs
10
+ * `'embedded webview'`).
11
+ */
12
+ name: string;
13
+ /**
14
+ * Dedicated `expo-secure-store` key under which the timestamp of the
15
+ * last successful emit is persisted. Each call site MUST use a
16
+ * distinct key so the two log streams cooldown independently.
17
+ */
18
+ storageKey: string;
19
+ };
20
+ /**
21
+ * Create a device-local cooldown gate for a `webview.load_succeeded`
22
+ * instrumentation log.
23
+ *
24
+ * Both `<WebView>` (RN bridge) and `<EmbeddedWebView>` (native bridge)
25
+ * emit the same success log; each instantiates its own cooldown with a
26
+ * dedicated storage key so emitting one log doesn't starve the other
27
+ * for an app that mounts both.
28
+ *
29
+ * - `shouldEmit` returns `true` when the cooldown has elapsed (or no
30
+ * timestamp has ever been stored) and `false` while inside the
31
+ * window. If secure-store throws on read we default to emitting —
32
+ * losing a diagnostic log is worse than the rare duplicate.
33
+ * - `recordEmitted` persists the current timestamp so subsequent calls
34
+ * within the window are suppressed. Write failures are swallowed
35
+ * (logged as a warning) so they don't break the host wrapper —
36
+ * worst case the next emit also goes through.
37
+ */
38
+ export declare const createSuccessLogCooldown: ({ name, storageKey, }: CreateSuccessLogCooldownArgs) => SuccessLogCooldown;
39
+ export {};
@@ -1,28 +1,14 @@
1
1
  import { Core } from '@dynamic-labs/client';
2
2
  import { WebViewFailedToLoadErrorMeta, WebViewFailedToLoadErrorPhase } from '../../../errors/WebViewFailedToLoadError';
3
- /**
4
- * Wire-format `type` of a webview-side `manifest` request.
5
- *
6
- * `requestChannel.request('manifest')` sends a `MessageTransportData` whose
7
- * `type` is the literal request name. The host replies with `manifest__ack`,
8
- * which we ignore: receiving the request is sufficient evidence that the
9
- * webview JS bundle is alive and the message bridge works.
10
- */
11
- type WebViewPhaseEvent = 'load_start' | 'load' | 'load_end' | 'native_error' | 'os_kill';
3
+ import { WebViewLoadSuccessMeta, WebViewPhaseEvent } from '../webViewPhaseTimerCore';
4
+ export type { WebViewLoadSuccessMeta };
12
5
  type UseWebViewPhaseTimersProps = {
13
6
  core: Core;
14
- webViewUrl: URL;
15
7
  loadingTimeout: number;
16
8
  recoveryTimeout: number;
9
+ webViewUrl: URL;
17
10
  };
18
11
  export type WebViewPhaseTimers = {
19
- /**
20
- * Record a per-attempt lifecycle event. `load_start` resets the
21
- * per-attempt state (so the timings reflect the current reload), while
22
- * `native_error` and `os_kill` increment cumulative counters that
23
- * persist across reloads.
24
- */
25
- recordEvent: (event: WebViewPhaseEvent) => void;
26
12
  /**
27
13
  * Build the structured meta to attach to {@link WebViewFailedToLoadError}.
28
14
  * Pass `phase` explicitly from the callsite (the phase reflects the
@@ -31,6 +17,17 @@ export type WebViewPhaseTimers = {
31
17
  getMeta: (args: {
32
18
  phase: WebViewFailedToLoadErrorPhase;
33
19
  }) => WebViewFailedToLoadErrorMeta;
20
+ /**
21
+ * Build the structured meta to attach to the `webview.load_succeeded`
22
+ * instrumentation log. Same timing fields as {@link getMeta} but without
23
+ * the failure-specific `phase` discriminator.
24
+ */
25
+ getSuccessMeta: () => WebViewLoadSuccessMeta;
26
+ /**
27
+ * Record a per-attempt lifecycle event. See {@link WebViewPhaseEvent}
28
+ * for the semantics of each event.
29
+ */
30
+ recordEvent: (event: WebViewPhaseEvent) => void;
34
31
  };
35
32
  /**
36
33
  * Tracks how long each step of the WebView load took, so when we raise
@@ -40,6 +37,10 @@ export type WebViewPhaseTimers = {
40
37
  * timings (`webview.time_to_load_manifest`, `webview.time_to_sdk_ready`)
41
38
  * still come from the webview itself once it boots, but those don't help
42
39
  * when the webview never boots in the first place.
40
+ *
41
+ * The actual state machine lives in {@link createWebViewPhaseTimerCore},
42
+ * which is also reused by the embedded native WebView path. This hook
43
+ * only layers the React-specific bits (ref-based core lifetime tied to
44
+ * `useEffect`, URL-derived `hadClearState` / `retryCount`).
43
45
  */
44
- export declare const useWebViewPhaseTimers: ({ core, webViewUrl, loadingTimeout, recoveryTimeout, }: UseWebViewPhaseTimersProps) => WebViewPhaseTimers;
45
- export {};
46
+ export declare const useWebViewPhaseTimers: ({ core, loadingTimeout, recoveryTimeout, webViewUrl, }: UseWebViewPhaseTimersProps) => WebViewPhaseTimers;
@@ -0,0 +1 @@
1
+ export { useWebViewSuccessLog } from './useWebViewSuccessLog';
@@ -0,0 +1,28 @@
1
+ import { Core } from '@dynamic-labs/client';
2
+ import { WebViewLoadSuccessMeta } from '../useWebViewPhaseTimers/useWebViewPhaseTimers';
3
+ type UseWebViewSuccessLogProps = {
4
+ core: Core;
5
+ getSuccessMeta: () => WebViewLoadSuccessMeta;
6
+ };
7
+ /**
8
+ * Emit a `webview.load_succeeded` instrumentation log the first time
9
+ * the SDK signals it's ready, gated by a device-local 1h cooldown so a
10
+ * customer with many WebView mounts (or many app launches) doesn't spam
11
+ * the logs pipeline.
12
+ *
13
+ * The error path is unaffected: failures keep going through
14
+ * `logger.error(WebViewFailedToLoadError, meta)` with no cooldown.
15
+ *
16
+ * The hook intentionally fires only once per mount (`hasEmittedRef`):
17
+ * after a successful boot we don't expect another `sdkHasLoaded` for
18
+ * the same WebView lifetime, and emitting twice for the same load would
19
+ * be misleading.
20
+ *
21
+ * Cooldown gating + log emission are delegated to the shared
22
+ * `successLog` helpers so this hook and the embedded native WebView
23
+ * path stay in lockstep on payload shape and storage semantics; the
24
+ * dedicated storage key here keeps the RN-path cooldown independent
25
+ * from the embedded path's.
26
+ */
27
+ export declare const useWebViewSuccessLog: ({ core, getSuccessMeta, }: UseWebViewSuccessLogProps) => void;
28
+ export {};
@@ -0,0 +1,2 @@
1
+ export { buildSuccessMeta, computePhaseDurations, createWebViewPhaseTimerCore, } from './webViewPhaseTimerCore';
2
+ export type { CumulativeCounters, PerAttemptTimings, PhaseDurations, WebViewLoadSuccessMeta, WebViewPhaseEvent, WebViewPhaseTimerCore, } from './webViewPhaseTimerCore';
@@ -0,0 +1,123 @@
1
+ import { Core } from '@dynamic-labs/client';
2
+ /**
3
+ * Per-attempt lifecycle event recorded by the phase-timer core.
4
+ *
5
+ * - `load_start` — native bridge fired `onLoadStart` (resets per-attempt
6
+ * state so timings reflect the current attempt)
7
+ * - `load` — native bridge fired `onLoad` (HTML byte stream received)
8
+ * - `load_end` — native bridge fired `onLoadEnd` (page finished
9
+ * rendering)
10
+ * - `native_error` — native bridge raised an `onLoadError`; increments a
11
+ * counter that persists across reloads
12
+ * - `os_kill` — host detected an OS-level WebView kill (e.g. backgrounded
13
+ * for too long); also a cumulative counter
14
+ */
15
+ export type WebViewPhaseEvent = 'load' | 'load_end' | 'load_start' | 'native_error' | 'os_kill';
16
+ export type PerAttemptTimings = {
17
+ htmlLoadStartedAt: number | null;
18
+ htmlLoadedAt: number | null;
19
+ manifestReceivedAt: number | null;
20
+ onLoadEndAt: number | null;
21
+ sdkReadyAt: number | null;
22
+ };
23
+ export type CumulativeCounters = {
24
+ nativeErrorCount: number;
25
+ osKillCount: number;
26
+ };
27
+ export type PhaseDurations = {
28
+ htmlLoadMs: number | null;
29
+ manifestReceivedMs: number | null;
30
+ onLoadToOnLoadEndMs: number | null;
31
+ sdkReadyMs: number | null;
32
+ };
33
+ /**
34
+ * Derive cross-phase durations from raw timestamps. The four durations
35
+ * are what end up in the failure / success log meta:
36
+ *
37
+ * - `htmlLoadMs` — `onLoadStart` to `onLoad`; native HTML fetch
38
+ * - `onLoadToOnLoadEndMs` — `onLoad` to `onLoadEnd`; native render/parse
39
+ * - `manifestReceivedMs` — `onLoadEnd` to first `manifest` request from
40
+ * the webview JS; "JS bundle is alive" signal
41
+ * - `sdkReadyMs` — `onLoadEnd` to first `sdkHasLoaded` event from the
42
+ * webview JS; "SDK fully bootstrapped" signal
43
+ */
44
+ export declare const computePhaseDurations: (timings: PerAttemptTimings) => PhaseDurations;
45
+ export type WebViewPhaseTimerCore = {
46
+ /** Detach the message-transport subscription. */
47
+ dispose: () => void;
48
+ getCounters: () => CumulativeCounters;
49
+ getTimings: () => PerAttemptTimings;
50
+ recordEvent: (event: WebViewPhaseEvent) => void;
51
+ };
52
+ /**
53
+ * Path-agnostic shape of the `webview.load_succeeded` instrumentation
54
+ * log meta. Both the React `<WebView>` and the embedded native WebView
55
+ * paths emit this exact surface so the two log streams can be queried
56
+ * together in Datadog without per-path field translation.
57
+ *
58
+ * The failure meta (`WebViewFailedToLoadErrorMeta`) is this shape plus
59
+ * a `phase` discriminator — see `buildSuccessMeta` callers.
60
+ */
61
+ export type WebViewLoadSuccessMeta = {
62
+ hadClearState: boolean;
63
+ htmlLoadMs: number | null;
64
+ loadingTimeoutMs: number;
65
+ manifestReceivedMs: number | null;
66
+ nativeErrorCount: number;
67
+ onLoadToOnLoadEndMs: number | null;
68
+ osKillCount: number;
69
+ recoveryTimeoutMs: number;
70
+ retryCount: number;
71
+ sdkReadyMs: number | null;
72
+ webviewUrl: string;
73
+ };
74
+ type BuildSuccessMetaArgs = {
75
+ /**
76
+ * Snapshot source for raw timings + cumulative counters. Typically a
77
+ * live {@link WebViewPhaseTimerCore} instance, but the type only
78
+ * requires `getTimings` / `getCounters` so callers can fall back to
79
+ * empty values if the core hasn't been created yet.
80
+ */
81
+ core: Pick<WebViewPhaseTimerCore, 'getCounters' | 'getTimings'>;
82
+ /**
83
+ * Whether the current attempt was kicked off by the React
84
+ * `<WebView>`'s clear-state recovery flow. Embedded path is always
85
+ * `false`.
86
+ */
87
+ hadClearState: boolean;
88
+ loadingTimeoutMs: number;
89
+ recoveryTimeoutMs: number;
90
+ /**
91
+ * Number of times the React `<WebView>` re-mounted via the `?retry=`
92
+ * URL query param. Embedded path is always `0`.
93
+ */
94
+ retryCount: number;
95
+ webviewUrl: string;
96
+ };
97
+ /**
98
+ * Build the success-shape meta from a {@link WebViewPhaseTimerCore}
99
+ * snapshot plus the path-specific fields. Both call sites call this
100
+ * with their own context (RN reads `hadClearState` / `retryCount` from
101
+ * the URL; embedded passes constants) so the success-log payload stays
102
+ * identical across the two paths.
103
+ */
104
+ export declare const buildSuccessMeta: ({ core, hadClearState, loadingTimeoutMs, recoveryTimeoutMs, retryCount, webviewUrl, }: BuildSuccessMetaArgs) => WebViewLoadSuccessMeta;
105
+ /**
106
+ * Shared state machine driving WebView load-phase instrumentation.
107
+ *
108
+ * Both the React `<WebView>` (RN bridge) and the native embedded WebView
109
+ * (native bridge) feed lifecycle events into this core and produce
110
+ * matching meta from the same timings + counters. Each path layers its
111
+ * own path-specific fields (`hadClearState`, `retryCount`, etc.) on top
112
+ * of the raw state this core exposes via `getTimings()` / `getCounters()`.
113
+ *
114
+ * The core owns the `core.messageTransport` subscription that captures
115
+ * the two webview-originated signals bracketing SDK bootstrap
116
+ * (`manifest` once the JS bundle is alive; `sdkHasLoadedEventName`
117
+ * once the SDK is fully ready). Native bridge lifecycle events come in
118
+ * via `recordEvent` instead.
119
+ */
120
+ export declare const createWebViewPhaseTimerCore: ({ core, }: {
121
+ core: Core;
122
+ }) => WebViewPhaseTimerCore;
123
+ export {};