@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.cjs
CHANGED
|
@@ -9,12 +9,12 @@ var reactNativeWebview = require('react-native-webview');
|
|
|
9
9
|
var logger$1 = require('@dynamic-labs/logger');
|
|
10
10
|
var messageTransport = require('@dynamic-labs/message-transport');
|
|
11
11
|
var webviewMessages = require('@dynamic-labs/webview-messages');
|
|
12
|
+
var expoSecureStore = require('expo-secure-store');
|
|
12
13
|
var jsxRuntime = require('react/jsx-runtime');
|
|
13
14
|
var expoModulesCore = require('expo-modules-core');
|
|
14
15
|
var reactNativePasskey = require('react-native-passkey');
|
|
15
16
|
var expoLinking = require('expo-linking');
|
|
16
17
|
var expoWebBrowser = require('expo-web-browser');
|
|
17
|
-
var expoSecureStore = require('expo-secure-store');
|
|
18
18
|
var reactNativePasskeyStamper = require('@turnkey/react-native-passkey-stamper');
|
|
19
19
|
|
|
20
20
|
function _interopNamespace(e) {
|
|
@@ -35,7 +35,7 @@ function _interopNamespace(e) {
|
|
|
35
35
|
return Object.freeze(n);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
var version = "4.
|
|
38
|
+
var version = "4.84.0";
|
|
39
39
|
|
|
40
40
|
function _extends() {
|
|
41
41
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -129,22 +129,157 @@ const WEBVIEW_START_TIME_PARAM = 'webviewStartTime';
|
|
|
129
129
|
|
|
130
130
|
const hasClearStateInUrl = webViewUrl => Boolean(webViewUrl.searchParams.get(CLEAR_STATE_PARAM));
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 = () => ({
|
|
135
142
|
htmlLoadStartedAt: null,
|
|
136
143
|
htmlLoadedAt: null,
|
|
137
144
|
manifestReceivedAt: null,
|
|
138
145
|
onLoadEndAt: null,
|
|
139
146
|
sdkReadyAt: null
|
|
140
147
|
});
|
|
141
|
-
const diffMs
|
|
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';
|
|
142
257
|
const readRetryCount = webViewUrl => {
|
|
143
258
|
const raw = webViewUrl.searchParams.get(RETRY_QUERY_PARAM);
|
|
144
259
|
if (raw === null) return 0;
|
|
145
260
|
const parsed = Number(raw);
|
|
146
261
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
147
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
|
+
};
|
|
148
283
|
/**
|
|
149
284
|
* Tracks how long each step of the WebView load took, so when we raise
|
|
150
285
|
* `WebViewFailedToLoadError` we can attach the timings to the error log.
|
|
@@ -153,92 +288,231 @@ const readRetryCount = webViewUrl => {
|
|
|
153
288
|
* timings (`webview.time_to_load_manifest`, `webview.time_to_sdk_ready`)
|
|
154
289
|
* still come from the webview itself once it boots, but those don't help
|
|
155
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`).
|
|
156
296
|
*/
|
|
157
297
|
const useWebViewPhaseTimers = ({
|
|
158
298
|
core,
|
|
159
|
-
webViewUrl,
|
|
160
299
|
loadingTimeout,
|
|
161
|
-
recoveryTimeout
|
|
300
|
+
recoveryTimeout,
|
|
301
|
+
webViewUrl
|
|
162
302
|
}) => {
|
|
163
|
-
const
|
|
164
|
-
const nativeErrorCountRef = react.useRef(0);
|
|
165
|
-
const osKillCountRef = react.useRef(0);
|
|
303
|
+
const phaseCoreRef = react.useRef(null);
|
|
166
304
|
const webViewUrlRef = react.useRef(webViewUrl);
|
|
167
305
|
webViewUrlRef.current = webViewUrl;
|
|
168
306
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* We only record the first occurrence per attempt so that a healthy
|
|
173
|
-
* reload doesn't overwrite the timing from the attempt that ultimately
|
|
174
|
-
* failed.
|
|
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.
|
|
175
310
|
*/
|
|
176
311
|
react.useEffect(() => {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
184
320
|
}
|
|
185
321
|
};
|
|
186
|
-
core.messageTransport.on(handler);
|
|
187
|
-
return () => core.messageTransport.off(handler);
|
|
188
322
|
}, [core]);
|
|
189
323
|
const recordEvent = react.useCallback(event => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
case 'load_start':
|
|
193
|
-
perAttemptRef.current = emptyPerAttemptState$1();
|
|
194
|
-
perAttemptRef.current.htmlLoadStartedAt = now;
|
|
195
|
-
break;
|
|
196
|
-
case 'load':
|
|
197
|
-
perAttemptRef.current.htmlLoadedAt = now;
|
|
198
|
-
break;
|
|
199
|
-
case 'load_end':
|
|
200
|
-
perAttemptRef.current.onLoadEndAt = now;
|
|
201
|
-
break;
|
|
202
|
-
case 'native_error':
|
|
203
|
-
nativeErrorCountRef.current += 1;
|
|
204
|
-
break;
|
|
205
|
-
case 'os_kill':
|
|
206
|
-
osKillCountRef.current += 1;
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
324
|
+
var _a;
|
|
325
|
+
(_a = phaseCoreRef.current) === null || _a === void 0 ? void 0 : _a.recordEvent(event);
|
|
209
326
|
}, []);
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
}) => {
|
|
327
|
+
const getSuccessMeta = react.useCallback(() => {
|
|
328
|
+
var _a;
|
|
213
329
|
const url = webViewUrlRef.current;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
htmlLoadedAt,
|
|
217
|
-
onLoadEndAt,
|
|
218
|
-
manifestReceivedAt,
|
|
219
|
-
sdkReadyAt
|
|
220
|
-
} = perAttemptRef.current;
|
|
221
|
-
return {
|
|
330
|
+
return buildSuccessMeta({
|
|
331
|
+
core: (_a = phaseCoreRef.current) !== null && _a !== void 0 ? _a : EMPTY_CORE_SNAPSHOT,
|
|
222
332
|
hadClearState: hasClearStateInUrl(url),
|
|
223
|
-
htmlLoadMs: diffMs$1(htmlLoadedAt, htmlLoadStartedAt),
|
|
224
333
|
loadingTimeoutMs: loadingTimeout,
|
|
225
|
-
manifestReceivedMs: diffMs$1(manifestReceivedAt, onLoadEndAt),
|
|
226
|
-
nativeErrorCount: nativeErrorCountRef.current,
|
|
227
|
-
onLoadToOnLoadEndMs: diffMs$1(onLoadEndAt, htmlLoadedAt),
|
|
228
|
-
osKillCount: osKillCountRef.current,
|
|
229
|
-
phase,
|
|
230
334
|
recoveryTimeoutMs: recoveryTimeout,
|
|
231
335
|
retryCount: readRetryCount(url),
|
|
232
|
-
sdkReadyMs: diffMs$1(sdkReadyAt, onLoadEndAt),
|
|
233
336
|
webviewUrl: url.toString()
|
|
234
|
-
};
|
|
337
|
+
});
|
|
235
338
|
}, [loadingTimeout, recoveryTimeout]);
|
|
339
|
+
const getMeta = react.useCallback(({
|
|
340
|
+
phase
|
|
341
|
+
}) => Object.assign(Object.assign({}, getSuccessMeta()), {
|
|
342
|
+
phase
|
|
343
|
+
}), [getSuccessMeta]);
|
|
236
344
|
return {
|
|
237
345
|
getMeta,
|
|
346
|
+
getSuccessMeta,
|
|
238
347
|
recordEvent
|
|
239
348
|
};
|
|
240
349
|
};
|
|
241
350
|
|
|
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();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const SUCCESS_LOG_COOLDOWN_MS = 60 * 60 * 1000;
|
|
407
|
+
const parseStoredTimestamp = raw => {
|
|
408
|
+
if (raw === null) return null;
|
|
409
|
+
const parsed = Number(raw);
|
|
410
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* Create a device-local cooldown gate for a `webview.load_succeeded`
|
|
414
|
+
* instrumentation log.
|
|
415
|
+
*
|
|
416
|
+
* Both `<WebView>` (RN bridge) and `<EmbeddedWebView>` (native bridge)
|
|
417
|
+
* emit the same success log; each instantiates its own cooldown with a
|
|
418
|
+
* dedicated storage key so emitting one log doesn't starve the other
|
|
419
|
+
* for an app that mounts both.
|
|
420
|
+
*
|
|
421
|
+
* - `shouldEmit` returns `true` when the cooldown has elapsed (or no
|
|
422
|
+
* timestamp has ever been stored) and `false` while inside the
|
|
423
|
+
* window. If secure-store throws on read we default to emitting —
|
|
424
|
+
* losing a diagnostic log is worse than the rare duplicate.
|
|
425
|
+
* - `recordEmitted` persists the current timestamp so subsequent calls
|
|
426
|
+
* within the window are suppressed. Write failures are swallowed
|
|
427
|
+
* (logged as a warning) so they don't break the host wrapper —
|
|
428
|
+
* worst case the next emit also goes through.
|
|
429
|
+
*/
|
|
430
|
+
const createSuccessLogCooldown = ({
|
|
431
|
+
name,
|
|
432
|
+
storageKey
|
|
433
|
+
}) => ({
|
|
434
|
+
recordEmitted: (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (now = Date.now()) {
|
|
435
|
+
try {
|
|
436
|
+
yield expoSecureStore.setItemAsync(storageKey, now.toString());
|
|
437
|
+
} catch (error) {
|
|
438
|
+
logger.warn(`Failed to persist ${name} success-log cooldown timestamp`, {
|
|
439
|
+
error: String(error)
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}),
|
|
443
|
+
shouldEmit: (...args_2) => __awaiter(void 0, [...args_2], void 0, function* (now = Date.now()) {
|
|
444
|
+
try {
|
|
445
|
+
const raw = yield expoSecureStore.getItemAsync(storageKey);
|
|
446
|
+
const lastEmittedAt = parseStoredTimestamp(raw);
|
|
447
|
+
if (lastEmittedAt === null) return true;
|
|
448
|
+
return now - lastEmittedAt >= SUCCESS_LOG_COOLDOWN_MS;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
logger.warn(`Failed to read ${name} success-log cooldown timestamp; emitting anyway`, {
|
|
451
|
+
error: String(error)
|
|
452
|
+
});
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
});
|
|
457
|
+
|
|
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
|
+
|
|
242
516
|
const useWebViewVisibility = core => {
|
|
243
517
|
const webViewVisibilityRequestChannelRef = react.useRef(messageTransport.createRequestChannel(core.messageTransport));
|
|
244
518
|
const [visible, setVisible] = react.useState(false);
|
|
@@ -454,13 +728,18 @@ const WebView = ({
|
|
|
454
728
|
} = useMessageTransportWebViewBridge(core, webViewRef);
|
|
455
729
|
const {
|
|
456
730
|
recordEvent,
|
|
457
|
-
getMeta
|
|
731
|
+
getMeta,
|
|
732
|
+
getSuccessMeta
|
|
458
733
|
} = useWebViewPhaseTimers({
|
|
459
734
|
core,
|
|
460
735
|
loadingTimeout: _loadingTimeout,
|
|
461
736
|
recoveryTimeout: _recoveryTimeout,
|
|
462
737
|
webViewUrl
|
|
463
738
|
});
|
|
739
|
+
useWebViewSuccessLog({
|
|
740
|
+
core,
|
|
741
|
+
getSuccessMeta
|
|
742
|
+
});
|
|
464
743
|
const containerStyles = [styles['container'], visible ? styles.show : styles.hide];
|
|
465
744
|
/**
|
|
466
745
|
* Block the message transport when the webview is unmounted
|
|
@@ -558,10 +837,11 @@ const WebView = ({
|
|
|
558
837
|
recordEvent('load_end');
|
|
559
838
|
startRecoveryTimeout();
|
|
560
839
|
}, [recordEvent, startRecoveryTimeout]);
|
|
840
|
+
const webViewUrlString = react.useMemo(() => webViewUrl.toString(), [webViewUrl]);
|
|
561
841
|
return /*#__PURE__*/jsxRuntime.jsx(reactNativeWebview.WebView, {
|
|
562
842
|
ref: webViewRef,
|
|
563
843
|
source: {
|
|
564
|
-
uri:
|
|
844
|
+
uri: webViewUrlString
|
|
565
845
|
},
|
|
566
846
|
containerStyle: containerStyles,
|
|
567
847
|
style: styles['webview'],
|
|
@@ -579,7 +859,7 @@ const WebView = ({
|
|
|
579
859
|
isTopFrame: request.isTopFrame,
|
|
580
860
|
url: request.url
|
|
581
861
|
}, webViewUrl)
|
|
582
|
-
});
|
|
862
|
+
}, webViewUrlString);
|
|
583
863
|
};
|
|
584
864
|
const createWebView = internalProps => {
|
|
585
865
|
const WebViewWrapper = props => /*#__PURE__*/jsxRuntime.jsx(WebView, _extends({}, internalProps, props));
|
|
@@ -618,103 +898,51 @@ const getEmbeddedWebView = () => {
|
|
|
618
898
|
}
|
|
619
899
|
};
|
|
620
900
|
|
|
621
|
-
const MANIFEST_MESSAGE_TYPE = 'manifest';
|
|
622
|
-
const emptyPerAttemptState = () => ({
|
|
623
|
-
htmlLoadStartedAt: null,
|
|
624
|
-
htmlLoadedAt: null,
|
|
625
|
-
manifestReceivedAt: null,
|
|
626
|
-
onLoadEndAt: null,
|
|
627
|
-
sdkReadyAt: null
|
|
628
|
-
});
|
|
629
|
-
const diffMs = (later, earlier) => later !== null && earlier !== null ? later - earlier : null;
|
|
630
901
|
/**
|
|
631
902
|
* Non-React equivalent of `useWebViewPhaseTimers` for the embedded native
|
|
632
|
-
* webview path. The embedded path is a singleton wired by
|
|
633
|
-
* (not a React component), so we expose the same
|
|
634
|
-
* plain factory.
|
|
903
|
+
* webview path. The embedded path is a singleton wired by
|
|
904
|
+
* `setupEmbeddedWebView` (not a React component), so we expose the same
|
|
905
|
+
* phase-timing surface as a plain factory.
|
|
635
906
|
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
907
|
+
* Both this factory and the hook delegate the actual state machine to
|
|
908
|
+
* {@link createWebViewPhaseTimerCore}; the only path-specific bits are:
|
|
909
|
+
*
|
|
910
|
+
* - `hadClearState` is always `false` (embedded path has no clear-state
|
|
911
|
+
* recovery model — that's exclusive to the React `<WebView>` path's
|
|
912
|
+
* loading-timeout recovery flow)
|
|
913
|
+
* - `retryCount` is always `0` (embedded path has no retry counter URL
|
|
914
|
+
* query param either)
|
|
642
915
|
*/
|
|
643
916
|
const createEmbeddedWebViewPhaseTimers = ({
|
|
644
917
|
core,
|
|
645
|
-
webViewUrl,
|
|
646
918
|
loadingTimeoutMs,
|
|
647
|
-
recoveryTimeoutMs
|
|
919
|
+
recoveryTimeoutMs,
|
|
920
|
+
webViewUrl
|
|
648
921
|
}) => {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
};
|
|
661
|
-
core.messageTransport.on(handleMessage);
|
|
662
|
-
const recordEvent = event => {
|
|
663
|
-
const now = Date.now();
|
|
664
|
-
switch (event) {
|
|
665
|
-
case 'load_start':
|
|
666
|
-
perAttempt = emptyPerAttemptState();
|
|
667
|
-
perAttempt.htmlLoadStartedAt = now;
|
|
668
|
-
break;
|
|
669
|
-
case 'load':
|
|
670
|
-
perAttempt.htmlLoadedAt = now;
|
|
671
|
-
break;
|
|
672
|
-
case 'load_end':
|
|
673
|
-
perAttempt.onLoadEndAt = now;
|
|
674
|
-
break;
|
|
675
|
-
case 'native_error':
|
|
676
|
-
nativeErrorCount += 1;
|
|
677
|
-
break;
|
|
678
|
-
case 'os_kill':
|
|
679
|
-
osKillCount += 1;
|
|
680
|
-
break;
|
|
681
|
-
}
|
|
682
|
-
};
|
|
683
|
-
const getMeta = ({
|
|
684
|
-
phase
|
|
685
|
-
}) => {
|
|
686
|
-
const {
|
|
687
|
-
htmlLoadStartedAt,
|
|
688
|
-
htmlLoadedAt,
|
|
689
|
-
onLoadEndAt,
|
|
690
|
-
manifestReceivedAt,
|
|
691
|
-
sdkReadyAt
|
|
692
|
-
} = perAttempt;
|
|
693
|
-
return {
|
|
694
|
-
hadClearState: false,
|
|
695
|
-
htmlLoadMs: diffMs(htmlLoadedAt, htmlLoadStartedAt),
|
|
696
|
-
loadingTimeoutMs,
|
|
697
|
-
manifestReceivedMs: diffMs(manifestReceivedAt, onLoadEndAt),
|
|
698
|
-
nativeErrorCount,
|
|
699
|
-
onLoadToOnLoadEndMs: diffMs(onLoadEndAt, htmlLoadedAt),
|
|
700
|
-
osKillCount,
|
|
701
|
-
phase,
|
|
702
|
-
recoveryTimeoutMs,
|
|
703
|
-
retryCount: 0,
|
|
704
|
-
sdkReadyMs: diffMs(sdkReadyAt, onLoadEndAt),
|
|
705
|
-
webviewUrl: webViewUrl.toString()
|
|
706
|
-
};
|
|
707
|
-
};
|
|
708
|
-
const dispose = () => {
|
|
709
|
-
core.messageTransport.off(handleMessage);
|
|
710
|
-
};
|
|
922
|
+
const phaseCore = createWebViewPhaseTimerCore({
|
|
923
|
+
core
|
|
924
|
+
});
|
|
925
|
+
const getSuccessMeta = () => buildSuccessMeta({
|
|
926
|
+
core: phaseCore,
|
|
927
|
+
hadClearState: false,
|
|
928
|
+
loadingTimeoutMs,
|
|
929
|
+
recoveryTimeoutMs,
|
|
930
|
+
retryCount: 0,
|
|
931
|
+
webviewUrl: webViewUrl.toString()
|
|
932
|
+
});
|
|
711
933
|
return {
|
|
712
|
-
dispose,
|
|
713
|
-
getMeta
|
|
714
|
-
|
|
934
|
+
dispose: phaseCore.dispose,
|
|
935
|
+
getMeta: ({
|
|
936
|
+
phase
|
|
937
|
+
}) => Object.assign(Object.assign({}, getSuccessMeta()), {
|
|
938
|
+
phase
|
|
939
|
+
}),
|
|
940
|
+
getSuccessMeta,
|
|
941
|
+
recordEvent: phaseCore.recordEvent
|
|
715
942
|
};
|
|
716
943
|
};
|
|
717
944
|
|
|
945
|
+
const EMBEDDED_SUCCESS_LOG_COOLDOWN_STORAGE_KEY = 'dynamic.embeddedWebView.successLogLastEmittedAt';
|
|
718
946
|
const DEFAULT_LOADING_TIMEOUT_MS = 20000;
|
|
719
947
|
const DEFAULT_RECOVERY_TIMEOUT_MS = 20000;
|
|
720
948
|
// Wires the JS-side bridge to the native WKWebView singleton owned by Swift.
|
|
@@ -741,9 +969,14 @@ const setupEmbeddedWebView = ({
|
|
|
741
969
|
recoveryTimeoutMs: _recoveryTimeoutMs,
|
|
742
970
|
webViewUrl: builtUrl
|
|
743
971
|
});
|
|
972
|
+
const successLogCooldown = createSuccessLogCooldown({
|
|
973
|
+
name: 'embedded webview',
|
|
974
|
+
storageKey: EMBEDDED_SUCCESS_LOG_COOLDOWN_STORAGE_KEY
|
|
975
|
+
});
|
|
744
976
|
let loadingTimer = null;
|
|
745
977
|
let recoveryTimer = null;
|
|
746
978
|
let hasFailed = false;
|
|
979
|
+
let hasEmittedSuccessLog = false;
|
|
747
980
|
const clearLoadingTimer = () => {
|
|
748
981
|
if (loadingTimer !== null) {
|
|
749
982
|
clearTimeout(loadingTimer);
|
|
@@ -784,12 +1017,27 @@ const setupEmbeddedWebView = ({
|
|
|
784
1017
|
};
|
|
785
1018
|
core.messageTransport.on(handleHostMessage);
|
|
786
1019
|
// Watch for the webview-side SDK ready signal so we can clear the
|
|
787
|
-
// recovery timer. The phase-timer factory
|
|
788
|
-
// bookkeeping — both subscriptions are
|
|
1020
|
+
// recovery timer and emit the success log. The phase-timer factory
|
|
1021
|
+
// also subscribes for its own bookkeeping — both subscriptions are
|
|
1022
|
+
// independent and idempotent. The factory subscribes first and so
|
|
1023
|
+
// sees `sdkReadyAt` before this handler runs, which means the meta
|
|
1024
|
+
// we emit below has the freshest `sdkReadyMs`. We only emit once
|
|
1025
|
+
// per setup; a second SDK-ready event for the same load wouldn't
|
|
1026
|
+
// represent a new successful boot.
|
|
789
1027
|
const handleSdkReady = message => {
|
|
790
1028
|
if (message.origin !== 'webview') return;
|
|
791
1029
|
if (message.type !== webviewMessages.sdkHasLoadedEventName) return;
|
|
792
1030
|
clearRecoveryTimer();
|
|
1031
|
+
if (hasEmittedSuccessLog || hasFailed) return;
|
|
1032
|
+
hasEmittedSuccessLog = true;
|
|
1033
|
+
emitSuccessLog({
|
|
1034
|
+
cooldown: successLogCooldown,
|
|
1035
|
+
core,
|
|
1036
|
+
getMeta: phaseTimers.getSuccessMeta,
|
|
1037
|
+
name: 'Embedded webview'
|
|
1038
|
+
}).catch(err => {
|
|
1039
|
+
logger.warn('EmbeddedWebView.emitSuccessLog failed', err);
|
|
1040
|
+
});
|
|
793
1041
|
};
|
|
794
1042
|
core.messageTransport.on(handleSdkReady);
|
|
795
1043
|
const onMessageSub = native.addListener('onMessage', event => {
|
|
@@ -883,36 +1131,6 @@ const setupEmbeddedWebView = ({
|
|
|
883
1131
|
};
|
|
884
1132
|
};
|
|
885
1133
|
|
|
886
|
-
/******************************************************************************
|
|
887
|
-
Copyright (c) Microsoft Corporation.
|
|
888
|
-
|
|
889
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
890
|
-
purpose with or without fee is hereby granted.
|
|
891
|
-
|
|
892
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
893
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
894
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
895
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
896
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
897
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
898
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
899
|
-
***************************************************************************** */
|
|
900
|
-
|
|
901
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
902
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
903
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
904
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
905
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
906
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
907
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
912
|
-
var e = new Error(message);
|
|
913
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
914
|
-
};
|
|
915
|
-
|
|
916
1134
|
const setupPasskeyHandler = core => {
|
|
917
1135
|
const passkeyRequestChannel = messageTransport.createRequestChannel(core.messageTransport);
|
|
918
1136
|
passkeyRequestChannel.handle('passkey_register', _a => __awaiter(void 0, [_a], void 0, function* ({
|