@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/index.cjs +402 -184
- package/index.js +403 -185
- package/package.json +6 -6
- package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/embeddedWebViewPhaseTimers.d.ts +37 -23
- package/src/components/WebView/successLog/emitSuccessLog.d.ts +35 -0
- package/src/components/WebView/successLog/index.d.ts +3 -0
- package/src/components/WebView/successLog/successLogCooldown.d.ts +39 -0
- package/src/components/WebView/useWebViewPhaseTimers/useWebViewPhaseTimers.d.ts +20 -19
- package/src/components/WebView/useWebViewSuccessLog/index.d.ts +1 -0
- package/src/components/WebView/useWebViewSuccessLog/useWebViewSuccessLog.d.ts +28 -0
- package/src/components/WebView/webViewPhaseTimerCore/index.d.ts +2 -0
- package/src/components/WebView/webViewPhaseTimerCore/webViewPhaseTimerCore.d.ts +123 -0
package/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { assertPackageVersion } from '@dynamic-labs/assert-package-version';
|
|
2
2
|
import { StyleSheet, Platform } from 'react-native';
|
|
3
|
-
import { useEffect, useRef, useCallback, useState } 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
7
|
import { sdkHasLoadedEventName } from '@dynamic-labs/webview-messages';
|
|
8
|
+
import { setItemAsync, getItemAsync, deleteItemAsync } from 'expo-secure-store';
|
|
8
9
|
import { jsx } from 'react/jsx-runtime';
|
|
9
10
|
import { requireNativeModule } from 'expo-modules-core';
|
|
10
11
|
import { Passkey } from 'react-native-passkey';
|
|
11
12
|
import { createURL, getInitialURL, addEventListener, openURL } from 'expo-linking';
|
|
12
13
|
import { openAuthSessionAsync } from 'expo-web-browser';
|
|
13
|
-
import { getItemAsync, deleteItemAsync, setItemAsync } from 'expo-secure-store';
|
|
14
14
|
import { createPasskey, PasskeyStamper } from '@turnkey/react-native-passkey-stamper';
|
|
15
15
|
|
|
16
|
-
var version = "4.
|
|
16
|
+
var version = "4.84.0";
|
|
17
17
|
|
|
18
18
|
function _extends() {
|
|
19
19
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -107,22 +107,157 @@ const WEBVIEW_START_TIME_PARAM = 'webviewStartTime';
|
|
|
107
107
|
|
|
108
108
|
const hasClearStateInUrl = webViewUrl => Boolean(webViewUrl.searchParams.get(CLEAR_STATE_PARAM));
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 = () => ({
|
|
113
120
|
htmlLoadStartedAt: null,
|
|
114
121
|
htmlLoadedAt: null,
|
|
115
122
|
manifestReceivedAt: null,
|
|
116
123
|
onLoadEndAt: null,
|
|
117
124
|
sdkReadyAt: null
|
|
118
125
|
});
|
|
119
|
-
const diffMs
|
|
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';
|
|
120
235
|
const readRetryCount = webViewUrl => {
|
|
121
236
|
const raw = webViewUrl.searchParams.get(RETRY_QUERY_PARAM);
|
|
122
237
|
if (raw === null) return 0;
|
|
123
238
|
const parsed = Number(raw);
|
|
124
239
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
125
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
|
+
};
|
|
126
261
|
/**
|
|
127
262
|
* Tracks how long each step of the WebView load took, so when we raise
|
|
128
263
|
* `WebViewFailedToLoadError` we can attach the timings to the error log.
|
|
@@ -131,92 +266,231 @@ const readRetryCount = webViewUrl => {
|
|
|
131
266
|
* timings (`webview.time_to_load_manifest`, `webview.time_to_sdk_ready`)
|
|
132
267
|
* still come from the webview itself once it boots, but those don't help
|
|
133
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`).
|
|
134
274
|
*/
|
|
135
275
|
const useWebViewPhaseTimers = ({
|
|
136
276
|
core,
|
|
137
|
-
webViewUrl,
|
|
138
277
|
loadingTimeout,
|
|
139
|
-
recoveryTimeout
|
|
278
|
+
recoveryTimeout,
|
|
279
|
+
webViewUrl
|
|
140
280
|
}) => {
|
|
141
|
-
const
|
|
142
|
-
const nativeErrorCountRef = useRef(0);
|
|
143
|
-
const osKillCountRef = useRef(0);
|
|
281
|
+
const phaseCoreRef = useRef(null);
|
|
144
282
|
const webViewUrlRef = useRef(webViewUrl);
|
|
145
283
|
webViewUrlRef.current = webViewUrl;
|
|
146
284
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* We only record the first occurrence per attempt so that a healthy
|
|
151
|
-
* reload doesn't overwrite the timing from the attempt that ultimately
|
|
152
|
-
* failed.
|
|
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.
|
|
153
288
|
*/
|
|
154
289
|
useEffect(() => {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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;
|
|
162
298
|
}
|
|
163
299
|
};
|
|
164
|
-
core.messageTransport.on(handler);
|
|
165
|
-
return () => core.messageTransport.off(handler);
|
|
166
300
|
}, [core]);
|
|
167
301
|
const recordEvent = useCallback(event => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
case 'load_start':
|
|
171
|
-
perAttemptRef.current = emptyPerAttemptState$1();
|
|
172
|
-
perAttemptRef.current.htmlLoadStartedAt = now;
|
|
173
|
-
break;
|
|
174
|
-
case 'load':
|
|
175
|
-
perAttemptRef.current.htmlLoadedAt = now;
|
|
176
|
-
break;
|
|
177
|
-
case 'load_end':
|
|
178
|
-
perAttemptRef.current.onLoadEndAt = now;
|
|
179
|
-
break;
|
|
180
|
-
case 'native_error':
|
|
181
|
-
nativeErrorCountRef.current += 1;
|
|
182
|
-
break;
|
|
183
|
-
case 'os_kill':
|
|
184
|
-
osKillCountRef.current += 1;
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
302
|
+
var _a;
|
|
303
|
+
(_a = phaseCoreRef.current) === null || _a === void 0 ? void 0 : _a.recordEvent(event);
|
|
187
304
|
}, []);
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
}) => {
|
|
305
|
+
const getSuccessMeta = useCallback(() => {
|
|
306
|
+
var _a;
|
|
191
307
|
const url = webViewUrlRef.current;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
htmlLoadedAt,
|
|
195
|
-
onLoadEndAt,
|
|
196
|
-
manifestReceivedAt,
|
|
197
|
-
sdkReadyAt
|
|
198
|
-
} = perAttemptRef.current;
|
|
199
|
-
return {
|
|
308
|
+
return buildSuccessMeta({
|
|
309
|
+
core: (_a = phaseCoreRef.current) !== null && _a !== void 0 ? _a : EMPTY_CORE_SNAPSHOT,
|
|
200
310
|
hadClearState: hasClearStateInUrl(url),
|
|
201
|
-
htmlLoadMs: diffMs$1(htmlLoadedAt, htmlLoadStartedAt),
|
|
202
311
|
loadingTimeoutMs: loadingTimeout,
|
|
203
|
-
manifestReceivedMs: diffMs$1(manifestReceivedAt, onLoadEndAt),
|
|
204
|
-
nativeErrorCount: nativeErrorCountRef.current,
|
|
205
|
-
onLoadToOnLoadEndMs: diffMs$1(onLoadEndAt, htmlLoadedAt),
|
|
206
|
-
osKillCount: osKillCountRef.current,
|
|
207
|
-
phase,
|
|
208
312
|
recoveryTimeoutMs: recoveryTimeout,
|
|
209
313
|
retryCount: readRetryCount(url),
|
|
210
|
-
sdkReadyMs: diffMs$1(sdkReadyAt, onLoadEndAt),
|
|
211
314
|
webviewUrl: url.toString()
|
|
212
|
-
};
|
|
315
|
+
});
|
|
213
316
|
}, [loadingTimeout, recoveryTimeout]);
|
|
317
|
+
const getMeta = useCallback(({
|
|
318
|
+
phase
|
|
319
|
+
}) => Object.assign(Object.assign({}, getSuccessMeta()), {
|
|
320
|
+
phase
|
|
321
|
+
}), [getSuccessMeta]);
|
|
214
322
|
return {
|
|
215
323
|
getMeta,
|
|
324
|
+
getSuccessMeta,
|
|
216
325
|
recordEvent
|
|
217
326
|
};
|
|
218
327
|
};
|
|
219
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
|
+
|
|
220
494
|
const useWebViewVisibility = core => {
|
|
221
495
|
const webViewVisibilityRequestChannelRef = useRef(createRequestChannel(core.messageTransport));
|
|
222
496
|
const [visible, setVisible] = useState(false);
|
|
@@ -432,13 +706,18 @@ const WebView = ({
|
|
|
432
706
|
} = useMessageTransportWebViewBridge(core, webViewRef);
|
|
433
707
|
const {
|
|
434
708
|
recordEvent,
|
|
435
|
-
getMeta
|
|
709
|
+
getMeta,
|
|
710
|
+
getSuccessMeta
|
|
436
711
|
} = useWebViewPhaseTimers({
|
|
437
712
|
core,
|
|
438
713
|
loadingTimeout: _loadingTimeout,
|
|
439
714
|
recoveryTimeout: _recoveryTimeout,
|
|
440
715
|
webViewUrl
|
|
441
716
|
});
|
|
717
|
+
useWebViewSuccessLog({
|
|
718
|
+
core,
|
|
719
|
+
getSuccessMeta
|
|
720
|
+
});
|
|
442
721
|
const containerStyles = [styles['container'], visible ? styles.show : styles.hide];
|
|
443
722
|
/**
|
|
444
723
|
* Block the message transport when the webview is unmounted
|
|
@@ -536,10 +815,11 @@ const WebView = ({
|
|
|
536
815
|
recordEvent('load_end');
|
|
537
816
|
startRecoveryTimeout();
|
|
538
817
|
}, [recordEvent, startRecoveryTimeout]);
|
|
818
|
+
const webViewUrlString = useMemo(() => webViewUrl.toString(), [webViewUrl]);
|
|
539
819
|
return /*#__PURE__*/jsx(WebView$1, {
|
|
540
820
|
ref: webViewRef,
|
|
541
821
|
source: {
|
|
542
|
-
uri:
|
|
822
|
+
uri: webViewUrlString
|
|
543
823
|
},
|
|
544
824
|
containerStyle: containerStyles,
|
|
545
825
|
style: styles['webview'],
|
|
@@ -557,7 +837,7 @@ const WebView = ({
|
|
|
557
837
|
isTopFrame: request.isTopFrame,
|
|
558
838
|
url: request.url
|
|
559
839
|
}, webViewUrl)
|
|
560
|
-
});
|
|
840
|
+
}, webViewUrlString);
|
|
561
841
|
};
|
|
562
842
|
const createWebView = internalProps => {
|
|
563
843
|
const WebViewWrapper = props => /*#__PURE__*/jsx(WebView, _extends({}, internalProps, props));
|
|
@@ -596,103 +876,51 @@ const getEmbeddedWebView = () => {
|
|
|
596
876
|
}
|
|
597
877
|
};
|
|
598
878
|
|
|
599
|
-
const MANIFEST_MESSAGE_TYPE = 'manifest';
|
|
600
|
-
const emptyPerAttemptState = () => ({
|
|
601
|
-
htmlLoadStartedAt: null,
|
|
602
|
-
htmlLoadedAt: null,
|
|
603
|
-
manifestReceivedAt: null,
|
|
604
|
-
onLoadEndAt: null,
|
|
605
|
-
sdkReadyAt: null
|
|
606
|
-
});
|
|
607
|
-
const diffMs = (later, earlier) => later !== null && earlier !== null ? later - earlier : null;
|
|
608
879
|
/**
|
|
609
880
|
* Non-React equivalent of `useWebViewPhaseTimers` for the embedded native
|
|
610
|
-
* webview path. The embedded path is a singleton wired by
|
|
611
|
-
* (not a React component), so we expose the same
|
|
612
|
-
* plain factory.
|
|
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.
|
|
613
884
|
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
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)
|
|
620
893
|
*/
|
|
621
894
|
const createEmbeddedWebViewPhaseTimers = ({
|
|
622
895
|
core,
|
|
623
|
-
webViewUrl,
|
|
624
896
|
loadingTimeoutMs,
|
|
625
|
-
recoveryTimeoutMs
|
|
897
|
+
recoveryTimeoutMs,
|
|
898
|
+
webViewUrl
|
|
626
899
|
}) => {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
};
|
|
639
|
-
core.messageTransport.on(handleMessage);
|
|
640
|
-
const recordEvent = event => {
|
|
641
|
-
const now = Date.now();
|
|
642
|
-
switch (event) {
|
|
643
|
-
case 'load_start':
|
|
644
|
-
perAttempt = emptyPerAttemptState();
|
|
645
|
-
perAttempt.htmlLoadStartedAt = now;
|
|
646
|
-
break;
|
|
647
|
-
case 'load':
|
|
648
|
-
perAttempt.htmlLoadedAt = now;
|
|
649
|
-
break;
|
|
650
|
-
case 'load_end':
|
|
651
|
-
perAttempt.onLoadEndAt = now;
|
|
652
|
-
break;
|
|
653
|
-
case 'native_error':
|
|
654
|
-
nativeErrorCount += 1;
|
|
655
|
-
break;
|
|
656
|
-
case 'os_kill':
|
|
657
|
-
osKillCount += 1;
|
|
658
|
-
break;
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
const getMeta = ({
|
|
662
|
-
phase
|
|
663
|
-
}) => {
|
|
664
|
-
const {
|
|
665
|
-
htmlLoadStartedAt,
|
|
666
|
-
htmlLoadedAt,
|
|
667
|
-
onLoadEndAt,
|
|
668
|
-
manifestReceivedAt,
|
|
669
|
-
sdkReadyAt
|
|
670
|
-
} = perAttempt;
|
|
671
|
-
return {
|
|
672
|
-
hadClearState: false,
|
|
673
|
-
htmlLoadMs: diffMs(htmlLoadedAt, htmlLoadStartedAt),
|
|
674
|
-
loadingTimeoutMs,
|
|
675
|
-
manifestReceivedMs: diffMs(manifestReceivedAt, onLoadEndAt),
|
|
676
|
-
nativeErrorCount,
|
|
677
|
-
onLoadToOnLoadEndMs: diffMs(onLoadEndAt, htmlLoadedAt),
|
|
678
|
-
osKillCount,
|
|
679
|
-
phase,
|
|
680
|
-
recoveryTimeoutMs,
|
|
681
|
-
retryCount: 0,
|
|
682
|
-
sdkReadyMs: diffMs(sdkReadyAt, onLoadEndAt),
|
|
683
|
-
webviewUrl: webViewUrl.toString()
|
|
684
|
-
};
|
|
685
|
-
};
|
|
686
|
-
const dispose = () => {
|
|
687
|
-
core.messageTransport.off(handleMessage);
|
|
688
|
-
};
|
|
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
|
+
});
|
|
689
911
|
return {
|
|
690
|
-
dispose,
|
|
691
|
-
getMeta
|
|
692
|
-
|
|
912
|
+
dispose: phaseCore.dispose,
|
|
913
|
+
getMeta: ({
|
|
914
|
+
phase
|
|
915
|
+
}) => Object.assign(Object.assign({}, getSuccessMeta()), {
|
|
916
|
+
phase
|
|
917
|
+
}),
|
|
918
|
+
getSuccessMeta,
|
|
919
|
+
recordEvent: phaseCore.recordEvent
|
|
693
920
|
};
|
|
694
921
|
};
|
|
695
922
|
|
|
923
|
+
const EMBEDDED_SUCCESS_LOG_COOLDOWN_STORAGE_KEY = 'dynamic.embeddedWebView.successLogLastEmittedAt';
|
|
696
924
|
const DEFAULT_LOADING_TIMEOUT_MS = 20000;
|
|
697
925
|
const DEFAULT_RECOVERY_TIMEOUT_MS = 20000;
|
|
698
926
|
// Wires the JS-side bridge to the native WKWebView singleton owned by Swift.
|
|
@@ -719,9 +947,14 @@ const setupEmbeddedWebView = ({
|
|
|
719
947
|
recoveryTimeoutMs: _recoveryTimeoutMs,
|
|
720
948
|
webViewUrl: builtUrl
|
|
721
949
|
});
|
|
950
|
+
const successLogCooldown = createSuccessLogCooldown({
|
|
951
|
+
name: 'embedded webview',
|
|
952
|
+
storageKey: EMBEDDED_SUCCESS_LOG_COOLDOWN_STORAGE_KEY
|
|
953
|
+
});
|
|
722
954
|
let loadingTimer = null;
|
|
723
955
|
let recoveryTimer = null;
|
|
724
956
|
let hasFailed = false;
|
|
957
|
+
let hasEmittedSuccessLog = false;
|
|
725
958
|
const clearLoadingTimer = () => {
|
|
726
959
|
if (loadingTimer !== null) {
|
|
727
960
|
clearTimeout(loadingTimer);
|
|
@@ -762,12 +995,27 @@ const setupEmbeddedWebView = ({
|
|
|
762
995
|
};
|
|
763
996
|
core.messageTransport.on(handleHostMessage);
|
|
764
997
|
// Watch for the webview-side SDK ready signal so we can clear the
|
|
765
|
-
// recovery timer. The phase-timer factory
|
|
766
|
-
// bookkeeping — both subscriptions are
|
|
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.
|
|
767
1005
|
const handleSdkReady = message => {
|
|
768
1006
|
if (message.origin !== 'webview') return;
|
|
769
1007
|
if (message.type !== sdkHasLoadedEventName) return;
|
|
770
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
|
+
});
|
|
771
1019
|
};
|
|
772
1020
|
core.messageTransport.on(handleSdkReady);
|
|
773
1021
|
const onMessageSub = native.addListener('onMessage', event => {
|
|
@@ -861,36 +1109,6 @@ const setupEmbeddedWebView = ({
|
|
|
861
1109
|
};
|
|
862
1110
|
};
|
|
863
1111
|
|
|
864
|
-
/******************************************************************************
|
|
865
|
-
Copyright (c) Microsoft Corporation.
|
|
866
|
-
|
|
867
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
868
|
-
purpose with or without fee is hereby granted.
|
|
869
|
-
|
|
870
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
871
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
872
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
873
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
874
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
875
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
876
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
877
|
-
***************************************************************************** */
|
|
878
|
-
|
|
879
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
880
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
881
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
882
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
883
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
884
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
885
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
890
|
-
var e = new Error(message);
|
|
891
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
892
|
-
};
|
|
893
|
-
|
|
894
1112
|
const setupPasskeyHandler = core => {
|
|
895
1113
|
const passkeyRequestChannel = createRequestChannel(core.messageTransport);
|
|
896
1114
|
passkeyRequestChannel.handle('passkey_register', _a => __awaiter(void 0, [_a], void 0, function* ({
|