@dynamic-labs/react-native-extension 4.92.3 → 4.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/EmbeddedWebViewController.kt +19 -2
- package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
- package/android/embeddedwebview/EmbeddedWebViewController.kt +19 -2
- package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
- package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +19 -2
- package/index.cjs +252 -796
- package/index.js +251 -797
- package/ios/EmbeddedWebViewController.swift +7 -6
- package/package.json +6 -6
- package/src/ReactNativeExtension/ReactNativeExtension.d.ts +11 -2
- package/src/components/WebView/EmbeddedWebView/EmbeddedWebView.d.ts +6 -19
- package/src/components/WebView/WebView.d.ts +17 -6
- package/src/errors/WebViewFailedToLoadError.d.ts +1 -86
- package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/embeddedWebViewPhaseTimers.d.ts +0 -66
- package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/index.d.ts +0 -1
- package/src/components/WebView/constants.d.ts +0 -3
- package/src/components/WebView/successLog/emitSuccessLog.d.ts +0 -35
- package/src/components/WebView/successLog/index.d.ts +0 -3
- package/src/components/WebView/useWebViewLoadingTimeout/index.d.ts +0 -1
- package/src/components/WebView/useWebViewLoadingTimeout/useWebViewLoadingTimeout.d.ts +0 -4
- package/src/components/WebView/useWebViewPhaseTimers/index.d.ts +0 -1
- package/src/components/WebView/useWebViewPhaseTimers/useWebViewPhaseTimers.d.ts +0 -46
- package/src/components/WebView/useWebViewRecoveryTimeout/index.d.ts +0 -1
- package/src/components/WebView/useWebViewRecoveryTimeout/useWebViewRecoveryTimeout.d.ts +0 -12
- package/src/components/WebView/useWebViewSuccessLog/index.d.ts +0 -1
- package/src/components/WebView/useWebViewSuccessLog/useWebViewSuccessLog.d.ts +0 -28
- package/src/components/WebView/utils/assignEnvironmentIdToUrl/assignEnvironmentIdToUrl.d.ts +0 -1
- package/src/components/WebView/utils/assignEnvironmentIdToUrl/index.d.ts +0 -1
- package/src/components/WebView/utils/assignStartTimeToUrl/assignStartTimeToUrl.d.ts +0 -1
- package/src/components/WebView/utils/assignStartTimeToUrl/index.d.ts +0 -1
- package/src/components/WebView/utils/hasClearStateInUrl/hasClearStateInUrl.d.ts +0 -1
- package/src/components/WebView/utils/hasClearStateInUrl/index.d.ts +0 -1
- package/src/components/WebView/utils/increaseRetryToUrl/increaseRetryToUrl.d.ts +0 -1
- package/src/components/WebView/utils/increaseRetryToUrl/index.d.ts +0 -1
- package/src/components/WebView/utils/setClearStateToUrl/index.d.ts +0 -1
- package/src/components/WebView/utils/setClearStateToUrl/setClearStateToUrl.d.ts +0 -1
- package/src/components/WebView/webViewPhaseTimerCore/index.d.ts +0 -2
- package/src/components/WebView/webViewPhaseTimerCore/webViewPhaseTimerCore.d.ts +0 -123
package/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { assertPackageVersion } from '@dynamic-labs/assert-package-version';
|
|
2
2
|
import { StyleSheet, Platform } from 'react-native';
|
|
3
|
-
import { useEffect, useRef,
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { WebView as WebView$1 } from 'react-native-webview';
|
|
5
|
+
import { createWebViewLoadController, EMBEDDED_WEBVIEW_HTTP_ERROR_DOMAIN } from '@dynamic-labs/client';
|
|
6
|
+
export { WebViewFailedToLoadError } from '@dynamic-labs/client';
|
|
5
7
|
import { Logger } from '@dynamic-labs/logger';
|
|
6
8
|
import { messageTransportDataJsonReviver, parseMessageTransportData, messageTransportDataJsonReplacer, createRequestChannel } from '@dynamic-labs/message-transport';
|
|
7
|
-
import { sdkHasLoadedEventName } from '@dynamic-labs/webview-messages';
|
|
8
9
|
import { setItemAsync, getItemAsync, deleteItemAsync } from 'expo-secure-store';
|
|
9
10
|
import { jsx } from 'react/jsx-runtime';
|
|
10
11
|
import { requireOptionalNativeModule, requireNativeModule } from 'expo-modules-core';
|
|
@@ -13,7 +14,7 @@ import { createURL, getInitialURL, addEventListener, openURL } from 'expo-linkin
|
|
|
13
14
|
import { openAuthSessionAsync } from 'expo-web-browser';
|
|
14
15
|
import { createPasskey, PasskeyStamper } from '@turnkey/react-native-passkey-stamper';
|
|
15
16
|
|
|
16
|
-
var version = "4.
|
|
17
|
+
var version = "4.93.0";
|
|
17
18
|
|
|
18
19
|
function _extends() {
|
|
19
20
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -25,12 +26,35 @@ function _extends() {
|
|
|
25
26
|
}, _extends.apply(null, arguments);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
/******************************************************************************
|
|
30
|
+
Copyright (c) Microsoft Corporation.
|
|
31
|
+
|
|
32
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
33
|
+
purpose with or without fee is hereby granted.
|
|
34
|
+
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
36
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
37
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
38
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
39
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
40
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
41
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
42
|
+
***************************************************************************** */
|
|
43
|
+
|
|
44
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
45
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
46
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
47
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
48
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
49
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
50
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
55
|
+
var e = new Error(message);
|
|
56
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
57
|
+
};
|
|
34
58
|
|
|
35
59
|
const logger = new Logger('react-native-extension');
|
|
36
60
|
|
|
@@ -101,284 +125,40 @@ const useMessageTransportWebViewBridge = (core, webViewRef) => {
|
|
|
101
125
|
};
|
|
102
126
|
};
|
|
103
127
|
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
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]);
|
|
128
|
+
const useWebViewVisibility = core => {
|
|
129
|
+
const webViewVisibilityRequestChannelRef = useRef(createRequestChannel(core.messageTransport));
|
|
130
|
+
const [visible, setVisible] = useState(false);
|
|
131
|
+
useEffect(() => webViewVisibilityRequestChannelRef.current.handle('setVisibility', setVisible), [setVisible]);
|
|
322
132
|
return {
|
|
323
|
-
|
|
324
|
-
getSuccessMeta,
|
|
325
|
-
recordEvent
|
|
133
|
+
visible
|
|
326
134
|
};
|
|
327
135
|
};
|
|
328
136
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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();
|
|
137
|
+
const styles = StyleSheet.create({
|
|
138
|
+
container: {
|
|
139
|
+
backgroundColor: 'transparent',
|
|
140
|
+
bottom: 0,
|
|
141
|
+
flex: 1,
|
|
142
|
+
left: 0,
|
|
143
|
+
position: 'absolute',
|
|
144
|
+
right: 0,
|
|
145
|
+
top: 0,
|
|
146
|
+
width: '100%'
|
|
147
|
+
},
|
|
148
|
+
hide: {
|
|
149
|
+
elevation: 0,
|
|
150
|
+
opacity: 0,
|
|
151
|
+
zIndex: -10000
|
|
152
|
+
},
|
|
153
|
+
show: {
|
|
154
|
+
elevation: 10000,
|
|
155
|
+
opacity: 1,
|
|
156
|
+
zIndex: 10000
|
|
157
|
+
},
|
|
158
|
+
webview: {
|
|
159
|
+
backgroundColor: 'transparent',
|
|
160
|
+
flex: 1
|
|
161
|
+
}
|
|
382
162
|
});
|
|
383
163
|
|
|
384
164
|
const SUCCESS_LOG_COOLDOWN_MS = 60 * 60 * 1000;
|
|
@@ -433,225 +213,6 @@ const createSuccessLogCooldown = ({
|
|
|
433
213
|
})
|
|
434
214
|
});
|
|
435
215
|
|
|
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
|
-
|
|
494
|
-
const useWebViewVisibility = core => {
|
|
495
|
-
const webViewVisibilityRequestChannelRef = useRef(createRequestChannel(core.messageTransport));
|
|
496
|
-
const [visible, setVisible] = useState(false);
|
|
497
|
-
useEffect(() => webViewVisibilityRequestChannelRef.current.handle('setVisibility', setVisible), [setVisible]);
|
|
498
|
-
return {
|
|
499
|
-
visible
|
|
500
|
-
};
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
const styles = StyleSheet.create({
|
|
504
|
-
container: {
|
|
505
|
-
backgroundColor: 'transparent',
|
|
506
|
-
bottom: 0,
|
|
507
|
-
flex: 1,
|
|
508
|
-
left: 0,
|
|
509
|
-
position: 'absolute',
|
|
510
|
-
right: 0,
|
|
511
|
-
top: 0,
|
|
512
|
-
width: '100%'
|
|
513
|
-
},
|
|
514
|
-
hide: {
|
|
515
|
-
elevation: 0,
|
|
516
|
-
opacity: 0,
|
|
517
|
-
zIndex: -10000
|
|
518
|
-
},
|
|
519
|
-
show: {
|
|
520
|
-
elevation: 10000,
|
|
521
|
-
opacity: 1,
|
|
522
|
-
zIndex: 10000
|
|
523
|
-
},
|
|
524
|
-
webview: {
|
|
525
|
-
backgroundColor: 'transparent',
|
|
526
|
-
flex: 1
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
const setClearStateToUrl = webViewUrl => {
|
|
531
|
-
const newWebViewUrl = new URL(webViewUrl);
|
|
532
|
-
newWebViewUrl.searchParams.set(CLEAR_STATE_PARAM, 'true');
|
|
533
|
-
newWebViewUrl.searchParams.delete('retry');
|
|
534
|
-
return newWebViewUrl;
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
const useWebViewRecoveryTimeout = ({
|
|
538
|
-
core,
|
|
539
|
-
webViewUrl,
|
|
540
|
-
disableRecovery,
|
|
541
|
-
recoveryTimeout,
|
|
542
|
-
setWebViewUrl,
|
|
543
|
-
onFailedToLoadAfterClearState: _onFailedToLoadAfterClearState = () => {}
|
|
544
|
-
}) => {
|
|
545
|
-
const timeoutRef = useRef(null);
|
|
546
|
-
const onFailedToLoadAfterClearStateRef = useRef(_onFailedToLoadAfterClearState);
|
|
547
|
-
onFailedToLoadAfterClearStateRef.current = _onFailedToLoadAfterClearState;
|
|
548
|
-
/**
|
|
549
|
-
* Clear timeout on unmount
|
|
550
|
-
*/
|
|
551
|
-
useEffect(() => () => {
|
|
552
|
-
if (timeoutRef.current) {
|
|
553
|
-
clearTimeout(timeoutRef.current);
|
|
554
|
-
timeoutRef.current = null;
|
|
555
|
-
}
|
|
556
|
-
}, []);
|
|
557
|
-
return useCallback(() => {
|
|
558
|
-
if (disableRecovery) {
|
|
559
|
-
logger.debug('timeout recovery: is disabled');
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
if (hasClearStateInUrl(webViewUrl)) {
|
|
563
|
-
logger.instrument('Webview failed to load after clearing its state', {
|
|
564
|
-
environmentId: core.environmentId,
|
|
565
|
-
hostSdkSessionId: core.hostSdkSessionId,
|
|
566
|
-
key: 'webview.failed_to_load_after_clear_state',
|
|
567
|
-
time: 0,
|
|
568
|
-
webviewUrl: webViewUrl.toString()
|
|
569
|
-
});
|
|
570
|
-
onFailedToLoadAfterClearStateRef.current();
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
logger.debug('timeout recovery: registering timeout', webViewUrl.toString());
|
|
574
|
-
if (timeoutRef.current) {
|
|
575
|
-
clearTimeout(timeoutRef.current);
|
|
576
|
-
}
|
|
577
|
-
timeoutRef.current = setTimeout(() => {
|
|
578
|
-
if (!core.messageTransport.isBlocked()) {
|
|
579
|
-
logger.instrument('Webview recovery skipped because the webview did load', {
|
|
580
|
-
environmentId: core.environmentId,
|
|
581
|
-
hostSdkSessionId: core.hostSdkSessionId,
|
|
582
|
-
key: 'webview.recovery_skipped',
|
|
583
|
-
time: 0,
|
|
584
|
-
webviewUrl: webViewUrl.toString()
|
|
585
|
-
});
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
core.messageTransport.block();
|
|
589
|
-
// Increase the retry
|
|
590
|
-
setWebViewUrl(webViewUrl => {
|
|
591
|
-
const newWebViewUrl = setClearStateToUrl(webViewUrl);
|
|
592
|
-
logger.instrument('timeout recovery: reloading webview to clear its state', {
|
|
593
|
-
environmentId: core.environmentId,
|
|
594
|
-
hostSdkSessionId: core.hostSdkSessionId,
|
|
595
|
-
key: 'webview.recovery_by_clear_state',
|
|
596
|
-
time: 0,
|
|
597
|
-
webviewUrl: webViewUrl.toString()
|
|
598
|
-
});
|
|
599
|
-
return newWebViewUrl;
|
|
600
|
-
});
|
|
601
|
-
}, recoveryTimeout);
|
|
602
|
-
}, [core, disableRecovery, recoveryTimeout, setWebViewUrl, webViewUrl]);
|
|
603
|
-
};
|
|
604
|
-
|
|
605
|
-
const increaseRetryToUrl = webViewUrl => {
|
|
606
|
-
var _a;
|
|
607
|
-
const currentRetry = Number((_a = webViewUrl.searchParams.get('retry')) !== null && _a !== void 0 ? _a : '0');
|
|
608
|
-
const newWebViewUrl = new URL(webViewUrl);
|
|
609
|
-
newWebViewUrl.searchParams.delete(CLEAR_STATE_PARAM);
|
|
610
|
-
newWebViewUrl.searchParams.set('retry', String(currentRetry + 1));
|
|
611
|
-
return newWebViewUrl;
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
const useWebViewLoadingTimeout = (timeout, onTimeout) => {
|
|
615
|
-
const timeoutRef = useRef(null);
|
|
616
|
-
const onTimeoutRef = useRef(onTimeout);
|
|
617
|
-
onTimeoutRef.current = onTimeout;
|
|
618
|
-
const onLoadStart = useCallback(() => {
|
|
619
|
-
if (timeoutRef.current) {
|
|
620
|
-
clearTimeout(timeoutRef.current);
|
|
621
|
-
}
|
|
622
|
-
timeoutRef.current = setTimeout(() => {
|
|
623
|
-
onTimeoutRef.current();
|
|
624
|
-
}, timeout);
|
|
625
|
-
}, [timeout]);
|
|
626
|
-
const onLoad = useCallback(() => {
|
|
627
|
-
if (timeoutRef.current) {
|
|
628
|
-
clearTimeout(timeoutRef.current);
|
|
629
|
-
timeoutRef.current = null;
|
|
630
|
-
}
|
|
631
|
-
}, []);
|
|
632
|
-
useEffect(() => () => {
|
|
633
|
-
if (timeoutRef.current) {
|
|
634
|
-
clearTimeout(timeoutRef.current);
|
|
635
|
-
timeoutRef.current = null;
|
|
636
|
-
}
|
|
637
|
-
}, []);
|
|
638
|
-
return {
|
|
639
|
-
onLoad,
|
|
640
|
-
onLoadStart
|
|
641
|
-
};
|
|
642
|
-
};
|
|
643
|
-
|
|
644
|
-
const assignEnvironmentIdToUrl = (url, environmentId) => {
|
|
645
|
-
const _url = new URL(url);
|
|
646
|
-
_url.searchParams.set(ENVIRONMENT_ID_PARAM, environmentId);
|
|
647
|
-
return _url;
|
|
648
|
-
};
|
|
649
|
-
|
|
650
|
-
const assignStartTimeToUrl = url => {
|
|
651
|
-
url.searchParams.set(WEBVIEW_START_TIME_PARAM, Date.now().toString());
|
|
652
|
-
return url;
|
|
653
|
-
};
|
|
654
|
-
|
|
655
216
|
const waasOrigins = ['https://app.dynamic-preprod.xyz', 'https://app.dynamicauth.com'];
|
|
656
217
|
const turnkeyOrigins = ['https://recovery.turnkey.com', 'https://export.turnkey.com'];
|
|
657
218
|
const isNonProductionBuild = () => process.env['NODE_ENV'] !== 'production';
|
|
@@ -686,158 +247,153 @@ const shouldAllowNavigation = (request, webViewUrl) => {
|
|
|
686
247
|
return false;
|
|
687
248
|
};
|
|
688
249
|
|
|
689
|
-
const
|
|
690
|
-
|
|
250
|
+
const REACT_NATIVE_WEBVIEW_TYPE = 'react_native_webview';
|
|
251
|
+
// react-native-webview's `onError` only carries a `domain` on iOS/macOS.
|
|
252
|
+
// When it is absent (Android) this stable literal stands in so
|
|
253
|
+
// `classifyWebViewLoadError` still receives a domain and classifies the
|
|
254
|
+
// failure by its code alone (unknown domains default to transient).
|
|
255
|
+
const REACT_NATIVE_WEBVIEW_LOAD_ERROR_DOMAIN = 'ReactNativeWebViewLoadError';
|
|
256
|
+
const SUCCESS_LOG_COOLDOWN_STORAGE_KEY = 'dynamic.webview.successLogLastEmittedAt';
|
|
257
|
+
// All load, timeout, retry, telemetry, and terminal-error logic is owned by
|
|
258
|
+
// the webview load controller (`createWebViewLoadController`). This
|
|
259
|
+
// component only owns the platform bindings: the react-native-webview
|
|
260
|
+
// element, its lifecycle-event mapping, the message bridge, visibility
|
|
261
|
+
// handling, and the navigation allowlist.
|
|
691
262
|
const WebView = ({
|
|
692
|
-
webviewUrl
|
|
263
|
+
webviewUrl,
|
|
693
264
|
core,
|
|
694
265
|
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
266
|
+
recoveryTimeout,
|
|
267
|
+
loadingTimeout,
|
|
268
|
+
webviewLoad
|
|
698
269
|
}) => {
|
|
699
270
|
const webViewRef = useRef(null);
|
|
700
271
|
const {
|
|
701
272
|
visible
|
|
702
273
|
} = useWebViewVisibility(core);
|
|
703
|
-
|
|
274
|
+
// The URL of the load attempt currently in flight. The controller owns
|
|
275
|
+
// URL construction (environmentId, start time, retry counter); loading a
|
|
276
|
+
// new URL remounts the webview via `key`, which is how reloads work on
|
|
277
|
+
// this path. `null` until the controller issues the first load.
|
|
278
|
+
const [currentAttemptUrl, setCurrentAttemptUrl] = useState(null);
|
|
279
|
+
const controllerRef = useRef(null);
|
|
704
280
|
const {
|
|
705
281
|
onMessageHandler
|
|
706
282
|
} = 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
|
-
});
|
|
721
|
-
const containerStyles = [styles['container'], visible ? styles.show : styles.hide];
|
|
722
283
|
/**
|
|
723
|
-
*
|
|
284
|
+
* Owns the load-controller lifecycle: created once per mount, torn down
|
|
285
|
+
* on unmount (keeping the unmount warning + transport block so a stray
|
|
286
|
+
* unmount doesn't leave host messages hitting a dead page).
|
|
724
287
|
*/
|
|
725
|
-
useEffect(() =>
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
* Should not reload the webview if it is already blocked
|
|
734
|
-
* and loading a new page
|
|
735
|
-
*/
|
|
736
|
-
if (core.messageTransport.isBlocked()) {
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
/**
|
|
740
|
-
* iOS and Android both may trigger this event when the webview
|
|
741
|
-
* is removed due to memory pressure.
|
|
742
|
-
*
|
|
743
|
-
* When the webview is killed by the OS, the message transport must
|
|
744
|
-
* be blocked to prevent the "No handlers registered" error while
|
|
745
|
-
* the webview is reloading
|
|
746
|
-
*/
|
|
747
|
-
core.messageTransport.block();
|
|
748
|
-
setWebViewUrl(webViewUrl => {
|
|
749
|
-
const newWebViewUrl = increaseRetryToUrl(webViewUrl);
|
|
750
|
-
logger.instrument('Reloading webview', {
|
|
751
|
-
environmentId: core.environmentId,
|
|
752
|
-
hostSdkSessionId: core.hostSdkSessionId,
|
|
753
|
-
key: 'webview.reloading',
|
|
754
|
-
newWebViewUrl,
|
|
755
|
-
reason,
|
|
756
|
-
time: 0,
|
|
757
|
-
webviewUrl: webViewUrl.toString()
|
|
758
|
-
});
|
|
759
|
-
return newWebViewUrl;
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
const adapter = {
|
|
290
|
+
load: url => setCurrentAttemptUrl(url),
|
|
291
|
+
webviewType: REACT_NATIVE_WEBVIEW_TYPE
|
|
292
|
+
};
|
|
293
|
+
const successLogCooldown = createSuccessLogCooldown({
|
|
294
|
+
name: 'webview',
|
|
295
|
+
storageKey: SUCCESS_LOG_COOLDOWN_STORAGE_KEY
|
|
760
296
|
});
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
297
|
+
const controller = createWebViewLoadController(Object.assign(Object.assign(Object.assign(Object.assign({
|
|
298
|
+
adapter,
|
|
299
|
+
core
|
|
300
|
+
}, webviewLoad), loadingTimeout !== undefined && {
|
|
301
|
+
pageLoadTimeoutMs: loadingTimeout
|
|
302
|
+
}), recoveryTimeout !== undefined && {
|
|
303
|
+
sdkReadyTimeoutMs: recoveryTimeout
|
|
304
|
+
}), {
|
|
305
|
+
// The controller exposes no post-emit hook, so the cooldown is
|
|
306
|
+
// recorded as soon as the gate passes — before the log leaves the
|
|
307
|
+
// device. Worst case (a teardown between gate and emit) one log is
|
|
308
|
+
// suppressed, which beats replaying it after a crash.
|
|
309
|
+
shouldEmitSuccessLog: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
310
|
+
if (!(yield successLogCooldown.shouldEmit())) return false;
|
|
311
|
+
yield successLogCooldown.recordEmitted();
|
|
312
|
+
return true;
|
|
313
|
+
}),
|
|
314
|
+
webviewUrl
|
|
772
315
|
}));
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
316
|
+
controllerRef.current = controller;
|
|
317
|
+
// Hands the controller to the client core, which wires the
|
|
318
|
+
// customer-facing `client.sdk.loadState` / `client.sdk.retry()`
|
|
319
|
+
// surface to it.
|
|
320
|
+
core.registerWebViewLoadController(controller);
|
|
321
|
+
controller.start();
|
|
322
|
+
return () => {
|
|
323
|
+
logger.warn(unmountWebViewWarningMessage);
|
|
324
|
+
controller.teardown();
|
|
325
|
+
controllerRef.current = null;
|
|
326
|
+
core.messageTransport.block();
|
|
327
|
+
};
|
|
328
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- the controller lifecycle is strictly mount/unmount
|
|
329
|
+
}, []);
|
|
330
|
+
const dispatch = event => {
|
|
331
|
+
var _a;
|
|
332
|
+
return (_a = controllerRef.current) === null || _a === void 0 ? void 0 : _a.dispatch(event);
|
|
333
|
+
};
|
|
334
|
+
const onError = ({
|
|
335
|
+
nativeEvent
|
|
336
|
+
}) => {
|
|
337
|
+
var _a;
|
|
338
|
+
return dispatch({
|
|
339
|
+
code: nativeEvent.code,
|
|
340
|
+
description: nativeEvent.description,
|
|
341
|
+
domain: (_a = nativeEvent.domain) !== null && _a !== void 0 ? _a : REACT_NATIVE_WEBVIEW_LOAD_ERROR_DOMAIN,
|
|
342
|
+
failedUrl: nativeEvent.url,
|
|
343
|
+
type: 'native_error'
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
const onHttpError = ({
|
|
347
|
+
nativeEvent
|
|
348
|
+
}) => dispatch({
|
|
349
|
+
code: nativeEvent.statusCode,
|
|
350
|
+
description: nativeEvent.description,
|
|
351
|
+
domain: EMBEDDED_WEBVIEW_HTTP_ERROR_DOMAIN,
|
|
352
|
+
failedUrl: nativeEvent.url,
|
|
353
|
+
type: 'native_error'
|
|
354
|
+
});
|
|
779
355
|
/**
|
|
780
|
-
*
|
|
781
|
-
*
|
|
356
|
+
* iOS and Android both may kill the webview content process under memory
|
|
357
|
+
* pressure. The controller routes the signal through the bounded
|
|
358
|
+
* backed-off reload cycle, blocking the message transport while the
|
|
359
|
+
* reload is in flight (the machine handles it both mid-load and
|
|
360
|
+
* post-ready).
|
|
782
361
|
*/
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
disableRecovery: _disableRecovery,
|
|
786
|
-
onFailedToLoadAfterClearState: setWebViewLoadErrorWithPhase('after_clear_state'),
|
|
787
|
-
recoveryTimeout: _recoveryTimeout,
|
|
788
|
-
setWebViewUrl,
|
|
789
|
-
webViewUrl
|
|
362
|
+
const onProcessTerminated = () => dispatch({
|
|
363
|
+
type: 'recovery_requested'
|
|
790
364
|
});
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
recordEvent('native_error');
|
|
795
|
-
/**
|
|
796
|
-
* This is the first attempt to load the webview, do not throw an error
|
|
797
|
-
* because the recovery system will attempt to reload the webview
|
|
798
|
-
*/
|
|
799
|
-
if (webViewLoadErrorCountRef.current === 1) return;
|
|
800
|
-
setWebViewLoadError('native_error');
|
|
801
|
-
}, [recordEvent, setWebViewLoadError]);
|
|
802
|
-
const {
|
|
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]);
|
|
365
|
+
const containerStyles = [styles['container'], visible ? styles.show : styles.hide];
|
|
366
|
+
if (currentAttemptUrl === null) return null;
|
|
367
|
+
const currentAttemptUrlString = currentAttemptUrl.toString();
|
|
819
368
|
return /*#__PURE__*/jsx(WebView$1, {
|
|
820
369
|
ref: webViewRef,
|
|
821
370
|
source: {
|
|
822
|
-
uri:
|
|
371
|
+
uri: currentAttemptUrlString
|
|
823
372
|
},
|
|
824
373
|
containerStyle: containerStyles,
|
|
825
374
|
style: styles['webview'],
|
|
826
375
|
onMessage: onMessageHandler,
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
376
|
+
onLoadStart: () => dispatch({
|
|
377
|
+
type: 'load_started'
|
|
378
|
+
}),
|
|
379
|
+
onLoad: () => dispatch({
|
|
380
|
+
type: 'response_committed'
|
|
381
|
+
}),
|
|
382
|
+
onLoadEnd: () => dispatch({
|
|
383
|
+
type: 'page_finished'
|
|
384
|
+
}),
|
|
830
385
|
hideKeyboardAccessoryView: true,
|
|
831
386
|
webviewDebuggingEnabled: _webviewDebuggingEnabled,
|
|
832
|
-
onContentProcessDidTerminate:
|
|
833
|
-
onRenderProcessGone:
|
|
834
|
-
onError:
|
|
387
|
+
onContentProcessDidTerminate: onProcessTerminated,
|
|
388
|
+
onRenderProcessGone: onProcessTerminated,
|
|
389
|
+
onError: onError,
|
|
390
|
+
onHttpError: onHttpError,
|
|
835
391
|
setSupportMultipleWindows: false,
|
|
836
392
|
onShouldStartLoadWithRequest: request => shouldAllowNavigation({
|
|
837
393
|
isTopFrame: request.isTopFrame,
|
|
838
394
|
url: request.url
|
|
839
|
-
},
|
|
840
|
-
},
|
|
395
|
+
}, currentAttemptUrl)
|
|
396
|
+
}, currentAttemptUrlString);
|
|
841
397
|
};
|
|
842
398
|
const createWebView = internalProps => {
|
|
843
399
|
const WebViewWrapper = props => /*#__PURE__*/jsx(WebView, _extends({}, internalProps, props));
|
|
@@ -896,53 +452,10 @@ const getEmbeddedWebView = () => {
|
|
|
896
452
|
}
|
|
897
453
|
};
|
|
898
454
|
|
|
899
|
-
/**
|
|
900
|
-
* Non-React equivalent of `useWebViewPhaseTimers` for the embedded native
|
|
901
|
-
* webview path. The embedded path is a singleton wired by
|
|
902
|
-
* `setupEmbeddedWebView` (not a React component), so we expose the same
|
|
903
|
-
* phase-timing surface as a plain factory.
|
|
904
|
-
*
|
|
905
|
-
* Both this factory and the hook delegate the actual state machine to
|
|
906
|
-
* {@link createWebViewPhaseTimerCore}; the only path-specific bits are:
|
|
907
|
-
*
|
|
908
|
-
* - `hadClearState` is always `false` (embedded path has no clear-state
|
|
909
|
-
* recovery model — that's exclusive to the React `<WebView>` path's
|
|
910
|
-
* loading-timeout recovery flow)
|
|
911
|
-
* - `retryCount` is always `0` (embedded path has no retry counter URL
|
|
912
|
-
* query param either)
|
|
913
|
-
*/
|
|
914
|
-
const createEmbeddedWebViewPhaseTimers = ({
|
|
915
|
-
core,
|
|
916
|
-
loadingTimeoutMs,
|
|
917
|
-
recoveryTimeoutMs,
|
|
918
|
-
webViewUrl
|
|
919
|
-
}) => {
|
|
920
|
-
const phaseCore = createWebViewPhaseTimerCore({
|
|
921
|
-
core
|
|
922
|
-
});
|
|
923
|
-
const getSuccessMeta = () => buildSuccessMeta({
|
|
924
|
-
core: phaseCore,
|
|
925
|
-
hadClearState: false,
|
|
926
|
-
loadingTimeoutMs,
|
|
927
|
-
recoveryTimeoutMs,
|
|
928
|
-
retryCount: 0,
|
|
929
|
-
webviewUrl: webViewUrl.toString()
|
|
930
|
-
});
|
|
931
|
-
return {
|
|
932
|
-
dispose: phaseCore.dispose,
|
|
933
|
-
getMeta: ({
|
|
934
|
-
phase
|
|
935
|
-
}) => Object.assign(Object.assign({}, getSuccessMeta()), {
|
|
936
|
-
phase
|
|
937
|
-
}),
|
|
938
|
-
getSuccessMeta,
|
|
939
|
-
recordEvent: phaseCore.recordEvent
|
|
940
|
-
};
|
|
941
|
-
};
|
|
942
|
-
|
|
943
455
|
const EMBEDDED_SUCCESS_LOG_COOLDOWN_STORAGE_KEY = 'dynamic.embeddedWebView.successLogLastEmittedAt';
|
|
944
|
-
|
|
945
|
-
|
|
456
|
+
// The platform discriminator the load controller attaches to every
|
|
457
|
+
// consolidated `webview.load_*` telemetry event emitted for this path.
|
|
458
|
+
const EMBEDDED_WEBVIEW_TYPE = 'embedded_native';
|
|
946
459
|
// Debug log labels matching the react-native-webview bridge
|
|
947
460
|
// (`useMessageTransportWebViewBridge`) so both paths surface the same
|
|
948
461
|
// HOST/WEBVIEW message trace under `core.debug.messageTransport`.
|
|
@@ -953,6 +466,11 @@ const WEBVIEW_TO_HOST_LOG = 'WEBVIEW >>>> HOST';
|
|
|
953
466
|
// outside the React tree (in a dedicated UIWindow on iOS), so its setup runs
|
|
954
467
|
// once when the SDK is initialised — there's nothing to mount or unmount.
|
|
955
468
|
//
|
|
469
|
+
// All load, timeout, retry, telemetry, and terminal-error logic is owned by
|
|
470
|
+
// the webview load controller (`createWebViewLoadController`). This module
|
|
471
|
+
// only owns the platform bindings: the native-module wiring, the message
|
|
472
|
+
// bridge, visibility handling, and the navigation allowlist.
|
|
473
|
+
//
|
|
956
474
|
// Returns a teardown that removes every JS-side subscription. The native
|
|
957
475
|
// singleton is intentionally NOT destroyed — re-running setup with a fresh
|
|
958
476
|
// `core` (e.g. on client recreation with a new environmentId) just reloads
|
|
@@ -961,61 +479,48 @@ const setupEmbeddedWebView = ({
|
|
|
961
479
|
webviewUrl,
|
|
962
480
|
core,
|
|
963
481
|
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
964
|
-
|
|
965
|
-
recoveryTimeoutMs: _recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS
|
|
482
|
+
webviewLoad
|
|
966
483
|
}) => {
|
|
967
484
|
const native = getEmbeddedWebView();
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
485
|
+
// The URL of the load attempt currently in flight. The controller owns
|
|
486
|
+
// URL construction (environmentId, start time, retry counter), so the
|
|
487
|
+
// navigation allowlist below must compare against the URL the native
|
|
488
|
+
// webview was actually pointed at. Until the first `adapter.load` the
|
|
489
|
+
// base URL stands in — only its origin is ever compared.
|
|
490
|
+
let currentAttemptUrl = new URL(webviewUrl);
|
|
491
|
+
const adapter = {
|
|
492
|
+
load: url => {
|
|
493
|
+
var _a;
|
|
494
|
+
currentAttemptUrl = url;
|
|
495
|
+
logger.debug(`[EmbeddedWebView] calling setUrl: ${url.toString()}`);
|
|
496
|
+
(_a = native.setUrl(url.toString())) === null || _a === void 0 ? void 0 : _a.catch(err => {
|
|
497
|
+
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
webviewType: EMBEDDED_WEBVIEW_TYPE
|
|
501
|
+
};
|
|
975
502
|
const successLogCooldown = createSuccessLogCooldown({
|
|
976
503
|
name: 'embedded webview',
|
|
977
504
|
storageKey: EMBEDDED_SUCCESS_LOG_COOLDOWN_STORAGE_KEY
|
|
978
505
|
});
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
// (e.g. recovery timeout firing after html_load already failed) are
|
|
998
|
-
// suppressed so we don't spam the initialization-error setter with
|
|
999
|
-
// duplicate logs for the same load attempt.
|
|
1000
|
-
const raiseFailure = (phase, extraMeta = {}) => {
|
|
1001
|
-
if (hasFailed) return;
|
|
1002
|
-
hasFailed = true;
|
|
1003
|
-
clearLoadingTimer();
|
|
1004
|
-
clearRecoveryTimer();
|
|
1005
|
-
logger.debug(`[EmbeddedWebView] raiseFailure phase=${phase}`);
|
|
1006
|
-
const meta = phaseTimers.getMeta({
|
|
1007
|
-
phase
|
|
1008
|
-
});
|
|
1009
|
-
logger.instrument('Embedded webview load error', Object.assign({
|
|
1010
|
-
environmentId: core.environmentId,
|
|
1011
|
-
hostSdkSessionId: core.hostSdkSessionId,
|
|
1012
|
-
key: 'webview.embedded.load_error',
|
|
1013
|
-
phase,
|
|
1014
|
-
time: 0,
|
|
1015
|
-
webviewUrl: builtUrl.toString()
|
|
1016
|
-
}, extraMeta));
|
|
1017
|
-
core.initialization.error = new WebViewFailedToLoadError(Object.assign(Object.assign({}, meta), extraMeta));
|
|
1018
|
-
};
|
|
506
|
+
const controller = createWebViewLoadController(Object.assign(Object.assign({
|
|
507
|
+
adapter,
|
|
508
|
+
core
|
|
509
|
+
}, webviewLoad), {
|
|
510
|
+
// The controller exposes no post-emit hook, so the cooldown is recorded
|
|
511
|
+
// as soon as the gate passes — before the log leaves the device. Worst
|
|
512
|
+
// case (a teardown between gate and emit) one log is suppressed, which
|
|
513
|
+
// beats replaying it after a crash.
|
|
514
|
+
shouldEmitSuccessLog: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
515
|
+
if (!(yield successLogCooldown.shouldEmit())) return false;
|
|
516
|
+
yield successLogCooldown.recordEmitted();
|
|
517
|
+
return true;
|
|
518
|
+
}),
|
|
519
|
+
webviewUrl
|
|
520
|
+
}));
|
|
521
|
+
// Hands the controller to the client core, which wires the customer-facing
|
|
522
|
+
// `client.sdk.loadState` / `client.sdk.retry()` surface to it.
|
|
523
|
+
core.registerWebViewLoadController(controller);
|
|
1019
524
|
const visibilityChannel = createRequestChannel(core.messageTransport);
|
|
1020
525
|
const removeVisibilityHandler = visibilityChannel.handle('setVisibility', visible => {
|
|
1021
526
|
native.setVisible(visible).catch(err => {
|
|
@@ -1033,31 +538,6 @@ const setupEmbeddedWebView = ({
|
|
|
1033
538
|
});
|
|
1034
539
|
};
|
|
1035
540
|
core.messageTransport.on(handleHostMessage);
|
|
1036
|
-
// Watch for the webview-side SDK ready signal so we can clear the
|
|
1037
|
-
// recovery timer and emit the success log. The phase-timer factory
|
|
1038
|
-
// also subscribes for its own bookkeeping — both subscriptions are
|
|
1039
|
-
// independent and idempotent. The factory subscribes first and so
|
|
1040
|
-
// sees `sdkReadyAt` before this handler runs, which means the meta
|
|
1041
|
-
// we emit below has the freshest `sdkReadyMs`. We only emit once
|
|
1042
|
-
// per setup; a second SDK-ready event for the same load wouldn't
|
|
1043
|
-
// represent a new successful boot.
|
|
1044
|
-
const handleSdkReady = message => {
|
|
1045
|
-
if (message.origin !== 'webview') return;
|
|
1046
|
-
if (message.type !== sdkHasLoadedEventName) return;
|
|
1047
|
-
logger.debug('[EmbeddedWebView] SDK ready signal received');
|
|
1048
|
-
clearRecoveryTimer();
|
|
1049
|
-
if (hasEmittedSuccessLog || hasFailed) return;
|
|
1050
|
-
hasEmittedSuccessLog = true;
|
|
1051
|
-
emitSuccessLog({
|
|
1052
|
-
cooldown: successLogCooldown,
|
|
1053
|
-
core,
|
|
1054
|
-
getMeta: phaseTimers.getSuccessMeta,
|
|
1055
|
-
name: 'Embedded webview'
|
|
1056
|
-
}).catch(err => {
|
|
1057
|
-
logger.warn('EmbeddedWebView.emitSuccessLog failed', err);
|
|
1058
|
-
});
|
|
1059
|
-
};
|
|
1060
|
-
core.messageTransport.on(handleSdkReady);
|
|
1061
541
|
const onMessageSub = native.addListener('onMessage', event => {
|
|
1062
542
|
var _a;
|
|
1063
543
|
let parsed = null;
|
|
@@ -1079,7 +559,7 @@ const setupEmbeddedWebView = ({
|
|
|
1079
559
|
const allow = shouldAllowNavigation({
|
|
1080
560
|
isTopFrame: event.isTopFrame,
|
|
1081
561
|
url: event.url
|
|
1082
|
-
},
|
|
562
|
+
}, currentAttemptUrl);
|
|
1083
563
|
logger.debug(`[EmbeddedWebView] onShouldStartLoad id=${event.id} url=${event.url} isTopFrame=${event.isTopFrame} allow=${allow}`);
|
|
1084
564
|
native.respondToShouldStartLoad(event.id, allow).catch(err => {
|
|
1085
565
|
logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
|
|
@@ -1087,86 +567,57 @@ const setupEmbeddedWebView = ({
|
|
|
1087
567
|
});
|
|
1088
568
|
const onLoadStartSub = native.addListener('onLoadStart', () => {
|
|
1089
569
|
logger.debug('[EmbeddedWebView] onLoadStart');
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
// resets the html_load window cleanly without leaving a stale timer
|
|
1094
|
-
// around from the previous attempt.
|
|
1095
|
-
clearLoadingTimer();
|
|
1096
|
-
clearRecoveryTimer();
|
|
1097
|
-
loadingTimer = setTimeout(() => {
|
|
1098
|
-
raiseFailure('html_load');
|
|
1099
|
-
}, _loadingTimeoutMs);
|
|
570
|
+
controller.dispatch({
|
|
571
|
+
type: 'load_started'
|
|
572
|
+
});
|
|
1100
573
|
});
|
|
1101
574
|
const onLoadSub = native.addListener('onLoad', () => {
|
|
1102
575
|
logger.debug('[EmbeddedWebView] onLoad (response committed)');
|
|
1103
|
-
|
|
1104
|
-
|
|
576
|
+
controller.dispatch({
|
|
577
|
+
type: 'response_committed'
|
|
578
|
+
});
|
|
1105
579
|
});
|
|
1106
580
|
const onLoadEndSub = native.addListener('onLoadEnd', () => {
|
|
1107
581
|
logger.debug('[EmbeddedWebView] onLoadEnd (page finished)');
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
recoveryTimer = setTimeout(() => {
|
|
1112
|
-
raiseFailure('sdk_bootstrap');
|
|
1113
|
-
}, _recoveryTimeoutMs);
|
|
582
|
+
controller.dispatch({
|
|
583
|
+
type: 'page_finished'
|
|
584
|
+
});
|
|
1114
585
|
});
|
|
1115
586
|
const onLoadErrorSub = native.addListener('onLoadError', event => {
|
|
1116
587
|
logger.debug(`[EmbeddedWebView] onLoadError domain=${event.domain} code=${event.code} url=${event.url}`);
|
|
1117
|
-
phaseTimers.recordEvent('native_error');
|
|
1118
588
|
logger.warn('EmbeddedWebView load error', event);
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
589
|
+
controller.dispatch({
|
|
590
|
+
code: event.code,
|
|
591
|
+
description: event.description,
|
|
592
|
+
domain: event.domain,
|
|
593
|
+
failedUrl: event.url,
|
|
594
|
+
isProvisional: event.isProvisional,
|
|
595
|
+
type: 'native_error'
|
|
1125
596
|
});
|
|
1126
597
|
});
|
|
1127
598
|
native.setDebuggingEnabled(_webviewDebuggingEnabled).catch(err => {
|
|
1128
599
|
logger.warn('EmbeddedWebView.setDebuggingEnabled failed', err);
|
|
1129
600
|
});
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
// timer) never triggered. The onLoadStart handler clears and re-arms this
|
|
1135
|
-
// timer when it fires, so behaviour after onLoadStart is unchanged.
|
|
1136
|
-
logger.debug(`[EmbeddedWebView] arming loading timer (${_loadingTimeoutMs}ms) before setUrl`);
|
|
1137
|
-
loadingTimer = setTimeout(() => {
|
|
1138
|
-
logger.debug('[EmbeddedWebView] loading timer fired — raising html_load failure');
|
|
1139
|
-
raiseFailure('html_load');
|
|
1140
|
-
}, _loadingTimeoutMs);
|
|
1141
|
-
// Defer setUrl by one event-loop tick so the JS thread has fully settled
|
|
1142
|
-
// after listener registration. On cold boot the main thread is congested
|
|
1143
|
-
// with bundle evaluation — yielding here gives pending microtasks and
|
|
1144
|
-
// native bridge batches a chance to flush before the native side starts
|
|
601
|
+
// Defer the initial load by one event-loop tick so the JS thread has fully
|
|
602
|
+
// settled after listener registration. On cold boot the main thread is
|
|
603
|
+
// congested with bundle evaluation — yielding here gives pending microtasks
|
|
604
|
+
// and native bridge batches a chance to flush before the native side starts
|
|
1145
605
|
// firing navigation events that require a JS round-trip response.
|
|
1146
606
|
const warmupTimer = setTimeout(() => {
|
|
1147
|
-
|
|
1148
|
-
if (tornDown) return;
|
|
1149
|
-
logger.debug(`[EmbeddedWebView] calling setUrl: ${builtUrl.toString()}`);
|
|
1150
|
-
(_a = native.setUrl(builtUrl.toString())) === null || _a === void 0 ? void 0 : _a.catch(err => {
|
|
1151
|
-
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
1152
|
-
});
|
|
607
|
+
controller.start();
|
|
1153
608
|
}, 0);
|
|
1154
609
|
return () => {
|
|
1155
610
|
logger.debug('[EmbeddedWebView] teardown — removing listeners');
|
|
1156
|
-
tornDown = true;
|
|
1157
611
|
clearTimeout(warmupTimer);
|
|
1158
|
-
|
|
1159
|
-
clearRecoveryTimer();
|
|
612
|
+
controller.teardown();
|
|
1160
613
|
removeVisibilityHandler();
|
|
1161
614
|
core.messageTransport.off(handleHostMessage);
|
|
1162
|
-
core.messageTransport.off(handleSdkReady);
|
|
1163
615
|
onMessageSub.remove();
|
|
1164
616
|
onShouldStartLoadSub.remove();
|
|
1165
617
|
onLoadStartSub.remove();
|
|
1166
618
|
onLoadSub.remove();
|
|
1167
619
|
onLoadEndSub.remove();
|
|
1168
620
|
onLoadErrorSub.remove();
|
|
1169
|
-
phaseTimers.dispose();
|
|
1170
621
|
};
|
|
1171
622
|
};
|
|
1172
623
|
|
|
@@ -1510,7 +961,8 @@ const ReactNativeExtension = ({
|
|
|
1510
961
|
webviewUrl: _webviewUrl = defaultWebviewUrl,
|
|
1511
962
|
webviewDebuggingEnabled,
|
|
1512
963
|
appOrigin,
|
|
1513
|
-
embeddedWebView: _embeddedWebView = false
|
|
964
|
+
embeddedWebView: _embeddedWebView = false,
|
|
965
|
+
webviewLoad
|
|
1514
966
|
} = {}) => (_, core) => {
|
|
1515
967
|
const isPlatformSupportedByWebView = Platform.OS === 'android' || Platform.OS === 'ios';
|
|
1516
968
|
/**
|
|
@@ -1561,6 +1013,7 @@ const ReactNativeExtension = ({
|
|
|
1561
1013
|
previousEmbeddedWebViewTeardown = setupEmbeddedWebView({
|
|
1562
1014
|
core,
|
|
1563
1015
|
webviewDebuggingEnabled,
|
|
1016
|
+
webviewLoad,
|
|
1564
1017
|
webviewUrl: _webviewUrl
|
|
1565
1018
|
});
|
|
1566
1019
|
} catch (err) {
|
|
@@ -1581,6 +1034,7 @@ const ReactNativeExtension = ({
|
|
|
1581
1034
|
WebView: createWebView({
|
|
1582
1035
|
core,
|
|
1583
1036
|
webviewDebuggingEnabled,
|
|
1037
|
+
webviewLoad,
|
|
1584
1038
|
webviewUrl: _webviewUrl
|
|
1585
1039
|
})
|
|
1586
1040
|
}
|
|
@@ -1589,4 +1043,4 @@ const ReactNativeExtension = ({
|
|
|
1589
1043
|
|
|
1590
1044
|
assertPackageVersion('@dynamic-labs/react-native-extension', version);
|
|
1591
1045
|
|
|
1592
|
-
export { ReactNativeExtension
|
|
1046
|
+
export { ReactNativeExtension };
|