@dynamic-labs/react-native-extension 4.83.0 → 4.83.2-alpha.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 +26 -0
- package/android/EmbeddedWebViewModule.kt +8 -1
- package/android/KeyStoreKeyManager.kt +7 -1
- package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/android/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/android/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/android/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/android/src/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/android/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/index.cjs +352 -23
- package/index.js +353 -24
- package/ios/EmbeddedWebViewController.swift +22 -0
- package/ios/EmbeddedWebViewModule.swift +8 -1
- package/package.json +6 -6
- package/src/components/WebView/EmbeddedWebView/EmbeddedWebView.d.ts +20 -1
- package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/embeddedWebViewPhaseTimers.d.ts +52 -0
- package/src/components/WebView/EmbeddedWebView/embeddedWebViewPhaseTimers/index.d.ts +1 -0
- package/src/components/WebView/useWebViewPhaseTimers/index.d.ts +1 -0
- package/src/components/WebView/useWebViewPhaseTimers/useWebViewPhaseTimers.d.ts +45 -0
- package/src/errors/WebViewFailedToLoadError.d.ts +84 -1
- package/src/nativeModules/EmbeddedWebView.d.ts +27 -2
package/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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, useCallback, useState } from 'react';
|
|
4
4
|
import { WebView as WebView$1 } from 'react-native-webview';
|
|
5
5
|
import { Logger } from '@dynamic-labs/logger';
|
|
6
6
|
import { messageTransportDataJsonReviver, parseMessageTransportData, messageTransportDataJsonReplacer, createRequestChannel } from '@dynamic-labs/message-transport';
|
|
7
|
+
import { sdkHasLoadedEventName } from '@dynamic-labs/webview-messages';
|
|
7
8
|
import { jsx } from 'react/jsx-runtime';
|
|
8
9
|
import { requireNativeModule } from 'expo-modules-core';
|
|
9
10
|
import { Passkey } from 'react-native-passkey';
|
|
@@ -12,7 +13,7 @@ import { openAuthSessionAsync } from 'expo-web-browser';
|
|
|
12
13
|
import { getItemAsync, deleteItemAsync, setItemAsync } from 'expo-secure-store';
|
|
13
14
|
import { createPasskey, PasskeyStamper } from '@turnkey/react-native-passkey-stamper';
|
|
14
15
|
|
|
15
|
-
var version = "4.83.0";
|
|
16
|
+
var version = "4.83.2-alpha.0";
|
|
16
17
|
|
|
17
18
|
function _extends() {
|
|
18
19
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -25,8 +26,9 @@ function _extends() {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
class WebViewFailedToLoadError extends Error {
|
|
28
|
-
constructor() {
|
|
29
|
+
constructor(meta) {
|
|
29
30
|
super('Could not load Dynamic WebView');
|
|
31
|
+
this.meta = meta;
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -99,6 +101,122 @@ const useMessageTransportWebViewBridge = (core, webViewRef) => {
|
|
|
99
101
|
};
|
|
100
102
|
};
|
|
101
103
|
|
|
104
|
+
const CLEAR_STATE_PARAM = 'clear-state';
|
|
105
|
+
const ENVIRONMENT_ID_PARAM = 'environmentId';
|
|
106
|
+
const WEBVIEW_START_TIME_PARAM = 'webviewStartTime';
|
|
107
|
+
|
|
108
|
+
const hasClearStateInUrl = webViewUrl => Boolean(webViewUrl.searchParams.get(CLEAR_STATE_PARAM));
|
|
109
|
+
|
|
110
|
+
const MANIFEST_MESSAGE_TYPE$1 = 'manifest';
|
|
111
|
+
const RETRY_QUERY_PARAM = 'retry';
|
|
112
|
+
const emptyPerAttemptState$1 = () => ({
|
|
113
|
+
htmlLoadStartedAt: null,
|
|
114
|
+
htmlLoadedAt: null,
|
|
115
|
+
manifestReceivedAt: null,
|
|
116
|
+
onLoadEndAt: null,
|
|
117
|
+
sdkReadyAt: null
|
|
118
|
+
});
|
|
119
|
+
const diffMs$1 = (later, earlier) => later !== null && earlier !== null ? later - earlier : null;
|
|
120
|
+
const readRetryCount = webViewUrl => {
|
|
121
|
+
const raw = webViewUrl.searchParams.get(RETRY_QUERY_PARAM);
|
|
122
|
+
if (raw === null) return 0;
|
|
123
|
+
const parsed = Number(raw);
|
|
124
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Tracks how long each step of the WebView load took, so when we raise
|
|
128
|
+
* `WebViewFailedToLoadError` we can attach the timings to the error log.
|
|
129
|
+
*
|
|
130
|
+
* The timings cover the host-side phases of the load — the webview-side
|
|
131
|
+
* timings (`webview.time_to_load_manifest`, `webview.time_to_sdk_ready`)
|
|
132
|
+
* still come from the webview itself once it boots, but those don't help
|
|
133
|
+
* when the webview never boots in the first place.
|
|
134
|
+
*/
|
|
135
|
+
const useWebViewPhaseTimers = ({
|
|
136
|
+
core,
|
|
137
|
+
webViewUrl,
|
|
138
|
+
loadingTimeout,
|
|
139
|
+
recoveryTimeout
|
|
140
|
+
}) => {
|
|
141
|
+
const perAttemptRef = useRef(emptyPerAttemptState$1());
|
|
142
|
+
const nativeErrorCountRef = useRef(0);
|
|
143
|
+
const osKillCountRef = useRef(0);
|
|
144
|
+
const webViewUrlRef = useRef(webViewUrl);
|
|
145
|
+
webViewUrlRef.current = webViewUrl;
|
|
146
|
+
/**
|
|
147
|
+
* Listen on the message transport for the two webview-originated
|
|
148
|
+
* messages that mark "JS bundle alive" and "SDK ready".
|
|
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.
|
|
153
|
+
*/
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const handler = message => {
|
|
156
|
+
if (message.origin !== 'webview') return;
|
|
157
|
+
if (message.type === MANIFEST_MESSAGE_TYPE$1 && perAttemptRef.current.manifestReceivedAt === null) {
|
|
158
|
+
perAttemptRef.current.manifestReceivedAt = Date.now();
|
|
159
|
+
}
|
|
160
|
+
if (message.type === sdkHasLoadedEventName && perAttemptRef.current.sdkReadyAt === null) {
|
|
161
|
+
perAttemptRef.current.sdkReadyAt = Date.now();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
core.messageTransport.on(handler);
|
|
165
|
+
return () => core.messageTransport.off(handler);
|
|
166
|
+
}, [core]);
|
|
167
|
+
const recordEvent = useCallback(event => {
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
switch (event) {
|
|
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
|
+
}
|
|
187
|
+
}, []);
|
|
188
|
+
const getMeta = useCallback(({
|
|
189
|
+
phase
|
|
190
|
+
}) => {
|
|
191
|
+
const url = webViewUrlRef.current;
|
|
192
|
+
const {
|
|
193
|
+
htmlLoadStartedAt,
|
|
194
|
+
htmlLoadedAt,
|
|
195
|
+
onLoadEndAt,
|
|
196
|
+
manifestReceivedAt,
|
|
197
|
+
sdkReadyAt
|
|
198
|
+
} = perAttemptRef.current;
|
|
199
|
+
return {
|
|
200
|
+
hadClearState: hasClearStateInUrl(url),
|
|
201
|
+
htmlLoadMs: diffMs$1(htmlLoadedAt, htmlLoadStartedAt),
|
|
202
|
+
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
|
+
recoveryTimeoutMs: recoveryTimeout,
|
|
209
|
+
retryCount: readRetryCount(url),
|
|
210
|
+
sdkReadyMs: diffMs$1(sdkReadyAt, onLoadEndAt),
|
|
211
|
+
webviewUrl: url.toString()
|
|
212
|
+
};
|
|
213
|
+
}, [loadingTimeout, recoveryTimeout]);
|
|
214
|
+
return {
|
|
215
|
+
getMeta,
|
|
216
|
+
recordEvent
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
102
220
|
const useWebViewVisibility = core => {
|
|
103
221
|
const webViewVisibilityRequestChannelRef = useRef(createRequestChannel(core.messageTransport));
|
|
104
222
|
const [visible, setVisible] = useState(false);
|
|
@@ -135,10 +253,6 @@ const styles = StyleSheet.create({
|
|
|
135
253
|
}
|
|
136
254
|
});
|
|
137
255
|
|
|
138
|
-
const CLEAR_STATE_PARAM = 'clear-state';
|
|
139
|
-
const ENVIRONMENT_ID_PARAM = 'environmentId';
|
|
140
|
-
const WEBVIEW_START_TIME_PARAM = 'webviewStartTime';
|
|
141
|
-
|
|
142
256
|
const setClearStateToUrl = webViewUrl => {
|
|
143
257
|
const newWebViewUrl = new URL(webViewUrl);
|
|
144
258
|
newWebViewUrl.searchParams.set(CLEAR_STATE_PARAM, 'true');
|
|
@@ -146,8 +260,6 @@ const setClearStateToUrl = webViewUrl => {
|
|
|
146
260
|
return newWebViewUrl;
|
|
147
261
|
};
|
|
148
262
|
|
|
149
|
-
const hasClearStateInUrl = webViewUrl => Boolean(webViewUrl.searchParams.get(CLEAR_STATE_PARAM));
|
|
150
|
-
|
|
151
263
|
const useWebViewRecoveryTimeout = ({
|
|
152
264
|
core,
|
|
153
265
|
webViewUrl,
|
|
@@ -300,13 +412,15 @@ const shouldAllowNavigation = (request, webViewUrl) => {
|
|
|
300
412
|
return false;
|
|
301
413
|
};
|
|
302
414
|
|
|
415
|
+
const DEFAULT_RECOVERY_TIMEOUT_MS$1 = 20000;
|
|
416
|
+
const DEFAULT_LOADING_TIMEOUT_MS$1 = 20000;
|
|
303
417
|
const WebView = ({
|
|
304
418
|
webviewUrl: initialWebViewUrl,
|
|
305
419
|
core,
|
|
306
420
|
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
307
421
|
disableRecovery: _disableRecovery = false,
|
|
308
|
-
recoveryTimeout: _recoveryTimeout =
|
|
309
|
-
loadingTimeout: _loadingTimeout =
|
|
422
|
+
recoveryTimeout: _recoveryTimeout = DEFAULT_RECOVERY_TIMEOUT_MS$1,
|
|
423
|
+
loadingTimeout: _loadingTimeout = DEFAULT_LOADING_TIMEOUT_MS$1
|
|
310
424
|
}) => {
|
|
311
425
|
const webViewRef = useRef(null);
|
|
312
426
|
const {
|
|
@@ -316,6 +430,15 @@ const WebView = ({
|
|
|
316
430
|
const {
|
|
317
431
|
onMessageHandler
|
|
318
432
|
} = useMessageTransportWebViewBridge(core, webViewRef);
|
|
433
|
+
const {
|
|
434
|
+
recordEvent,
|
|
435
|
+
getMeta
|
|
436
|
+
} = useWebViewPhaseTimers({
|
|
437
|
+
core,
|
|
438
|
+
loadingTimeout: _loadingTimeout,
|
|
439
|
+
recoveryTimeout: _recoveryTimeout,
|
|
440
|
+
webViewUrl
|
|
441
|
+
});
|
|
319
442
|
const containerStyles = [styles['container'], visible ? styles.show : styles.hide];
|
|
320
443
|
/**
|
|
321
444
|
* Block the message transport when the webview is unmounted
|
|
@@ -357,13 +480,23 @@ const WebView = ({
|
|
|
357
480
|
return newWebViewUrl;
|
|
358
481
|
});
|
|
359
482
|
}, [core]);
|
|
360
|
-
const blockAndReloadWebViewOsKill = useCallback(() =>
|
|
483
|
+
const blockAndReloadWebViewOsKill = useCallback(() => {
|
|
484
|
+
recordEvent('os_kill');
|
|
485
|
+
blockAndReloadWebView('os_kill');
|
|
486
|
+
}, [blockAndReloadWebView, recordEvent]);
|
|
361
487
|
useEffect(() => core.messageTransport.recoveryManager.onRecoveryRequested(() => blockAndReloadWebView('recovery')), [core, blockAndReloadWebView]);
|
|
362
|
-
const setWebViewLoadError = useCallback(
|
|
488
|
+
const setWebViewLoadError = useCallback(phase => {
|
|
363
489
|
// Error was already thrown, do not throw again
|
|
364
490
|
if (core.initialization.error instanceof WebViewFailedToLoadError) return;
|
|
365
|
-
core.initialization.error = new WebViewFailedToLoadError(
|
|
366
|
-
|
|
491
|
+
core.initialization.error = new WebViewFailedToLoadError(getMeta({
|
|
492
|
+
phase
|
|
493
|
+
}));
|
|
494
|
+
}, [core, getMeta]);
|
|
495
|
+
/**
|
|
496
|
+
* Wraps `setWebViewLoadError` so the consumer doesn't need to know
|
|
497
|
+
* about the phase enum; each callsite below binds its own phase.
|
|
498
|
+
*/
|
|
499
|
+
const setWebViewLoadErrorWithPhase = useCallback(phase => () => setWebViewLoadError(phase), [setWebViewLoadError]);
|
|
367
500
|
/**
|
|
368
501
|
* Reload the webview with a clean state when a timeout is reached
|
|
369
502
|
* and the webview did not get to the loaded state yet
|
|
@@ -371,7 +504,7 @@ const WebView = ({
|
|
|
371
504
|
const startRecoveryTimeout = useWebViewRecoveryTimeout({
|
|
372
505
|
core,
|
|
373
506
|
disableRecovery: _disableRecovery,
|
|
374
|
-
onFailedToLoadAfterClearState:
|
|
507
|
+
onFailedToLoadAfterClearState: setWebViewLoadErrorWithPhase('after_clear_state'),
|
|
375
508
|
recoveryTimeout: _recoveryTimeout,
|
|
376
509
|
setWebViewUrl,
|
|
377
510
|
webViewUrl
|
|
@@ -379,17 +512,30 @@ const WebView = ({
|
|
|
379
512
|
const webViewLoadErrorCountRef = useRef(0);
|
|
380
513
|
const onWebViewLoadError = useCallback(() => {
|
|
381
514
|
webViewLoadErrorCountRef.current = webViewLoadErrorCountRef.current + 1;
|
|
515
|
+
recordEvent('native_error');
|
|
382
516
|
/**
|
|
383
517
|
* This is the first attempt to load the webview, do not throw an error
|
|
384
518
|
* because the recovery system will attempt to reload the webview
|
|
385
519
|
*/
|
|
386
520
|
if (webViewLoadErrorCountRef.current === 1) return;
|
|
387
|
-
setWebViewLoadError();
|
|
388
|
-
}, [setWebViewLoadError]);
|
|
521
|
+
setWebViewLoadError('native_error');
|
|
522
|
+
}, [recordEvent, setWebViewLoadError]);
|
|
389
523
|
const {
|
|
390
|
-
onLoad,
|
|
391
|
-
onLoadStart
|
|
392
|
-
} = useWebViewLoadingTimeout(_loadingTimeout,
|
|
524
|
+
onLoad: onLoadFromTimeout,
|
|
525
|
+
onLoadStart: onLoadStartFromTimeout
|
|
526
|
+
} = useWebViewLoadingTimeout(_loadingTimeout, setWebViewLoadErrorWithPhase('html_load'));
|
|
527
|
+
const onLoadStart = useCallback(() => {
|
|
528
|
+
recordEvent('load_start');
|
|
529
|
+
onLoadStartFromTimeout();
|
|
530
|
+
}, [onLoadStartFromTimeout, recordEvent]);
|
|
531
|
+
const onLoad = useCallback(() => {
|
|
532
|
+
recordEvent('load');
|
|
533
|
+
onLoadFromTimeout();
|
|
534
|
+
}, [onLoadFromTimeout, recordEvent]);
|
|
535
|
+
const onLoadEnd = useCallback(() => {
|
|
536
|
+
recordEvent('load_end');
|
|
537
|
+
startRecoveryTimeout();
|
|
538
|
+
}, [recordEvent, startRecoveryTimeout]);
|
|
393
539
|
return /*#__PURE__*/jsx(WebView$1, {
|
|
394
540
|
ref: webViewRef,
|
|
395
541
|
source: {
|
|
@@ -398,7 +544,7 @@ const WebView = ({
|
|
|
398
544
|
containerStyle: containerStyles,
|
|
399
545
|
style: styles['webview'],
|
|
400
546
|
onMessage: onMessageHandler,
|
|
401
|
-
onLoadEnd:
|
|
547
|
+
onLoadEnd: onLoadEnd,
|
|
402
548
|
onLoadStart: onLoadStart,
|
|
403
549
|
onLoad: onLoad,
|
|
404
550
|
hideKeyboardAccessoryView: true,
|
|
@@ -450,6 +596,105 @@ const getEmbeddedWebView = () => {
|
|
|
450
596
|
}
|
|
451
597
|
};
|
|
452
598
|
|
|
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
|
+
/**
|
|
609
|
+
* Non-React equivalent of `useWebViewPhaseTimers` for the embedded native
|
|
610
|
+
* webview path. The embedded path is a singleton wired by `setupEmbeddedWebView`
|
|
611
|
+
* (not a React component), so we expose the same phase-timing surface as a
|
|
612
|
+
* plain factory.
|
|
613
|
+
*
|
|
614
|
+
* The hook and the factory are intentionally separate: the embedded path does
|
|
615
|
+
* not have a retry or clear-state model, so `retryCount` is always `0` and
|
|
616
|
+
* `hadClearState` is always `false` in the meta produced here. Sharing one
|
|
617
|
+
* implementation would require either pulling React into a hot non-React path
|
|
618
|
+
* or threading a stateless adapter through both consumers \u2014 the duplication
|
|
619
|
+
* is small enough that keeping them separate stays cleaner.
|
|
620
|
+
*/
|
|
621
|
+
const createEmbeddedWebViewPhaseTimers = ({
|
|
622
|
+
core,
|
|
623
|
+
webViewUrl,
|
|
624
|
+
loadingTimeoutMs,
|
|
625
|
+
recoveryTimeoutMs
|
|
626
|
+
}) => {
|
|
627
|
+
let perAttempt = emptyPerAttemptState();
|
|
628
|
+
let nativeErrorCount = 0;
|
|
629
|
+
let osKillCount = 0;
|
|
630
|
+
const handleMessage = message => {
|
|
631
|
+
if (message.origin !== 'webview') return;
|
|
632
|
+
if (message.type === MANIFEST_MESSAGE_TYPE && perAttempt.manifestReceivedAt === null) {
|
|
633
|
+
perAttempt.manifestReceivedAt = Date.now();
|
|
634
|
+
}
|
|
635
|
+
if (message.type === sdkHasLoadedEventName && perAttempt.sdkReadyAt === null) {
|
|
636
|
+
perAttempt.sdkReadyAt = Date.now();
|
|
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
|
+
};
|
|
689
|
+
return {
|
|
690
|
+
dispose,
|
|
691
|
+
getMeta,
|
|
692
|
+
recordEvent
|
|
693
|
+
};
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const DEFAULT_LOADING_TIMEOUT_MS = 20000;
|
|
697
|
+
const DEFAULT_RECOVERY_TIMEOUT_MS = 20000;
|
|
453
698
|
// Wires the JS-side bridge to the native WKWebView singleton owned by Swift.
|
|
454
699
|
// This is intentionally not a React component: the embedded webview lives
|
|
455
700
|
// outside the React tree (in a dedicated UIWindow on iOS), so its setup runs
|
|
@@ -462,10 +707,47 @@ const getEmbeddedWebView = () => {
|
|
|
462
707
|
const setupEmbeddedWebView = ({
|
|
463
708
|
webviewUrl,
|
|
464
709
|
core,
|
|
465
|
-
webviewDebuggingEnabled: _webviewDebuggingEnabled = false
|
|
710
|
+
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
711
|
+
loadingTimeoutMs: _loadingTimeoutMs = DEFAULT_LOADING_TIMEOUT_MS,
|
|
712
|
+
recoveryTimeoutMs: _recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS
|
|
466
713
|
}) => {
|
|
467
714
|
const native = getEmbeddedWebView();
|
|
468
715
|
const builtUrl = assignStartTimeToUrl(assignEnvironmentIdToUrl(webviewUrl, core.environmentId));
|
|
716
|
+
const phaseTimers = createEmbeddedWebViewPhaseTimers({
|
|
717
|
+
core,
|
|
718
|
+
loadingTimeoutMs: _loadingTimeoutMs,
|
|
719
|
+
recoveryTimeoutMs: _recoveryTimeoutMs,
|
|
720
|
+
webViewUrl: builtUrl
|
|
721
|
+
});
|
|
722
|
+
let loadingTimer = null;
|
|
723
|
+
let recoveryTimer = null;
|
|
724
|
+
let hasFailed = false;
|
|
725
|
+
const clearLoadingTimer = () => {
|
|
726
|
+
if (loadingTimer !== null) {
|
|
727
|
+
clearTimeout(loadingTimer);
|
|
728
|
+
loadingTimer = null;
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
const clearRecoveryTimer = () => {
|
|
732
|
+
if (recoveryTimer !== null) {
|
|
733
|
+
clearTimeout(recoveryTimer);
|
|
734
|
+
recoveryTimer = null;
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
// Raise `WebViewFailedToLoadError` once per setup. Subsequent triggers
|
|
738
|
+
// (e.g. recovery timeout firing after html_load already failed) are
|
|
739
|
+
// suppressed so we don't spam the initialization-error setter with
|
|
740
|
+
// duplicate logs for the same load attempt.
|
|
741
|
+
const raiseFailure = (phase, extraMeta = {}) => {
|
|
742
|
+
if (hasFailed) return;
|
|
743
|
+
hasFailed = true;
|
|
744
|
+
clearLoadingTimer();
|
|
745
|
+
clearRecoveryTimer();
|
|
746
|
+
const meta = phaseTimers.getMeta({
|
|
747
|
+
phase
|
|
748
|
+
});
|
|
749
|
+
core.initialization.error = new WebViewFailedToLoadError(Object.assign(Object.assign({}, meta), extraMeta));
|
|
750
|
+
};
|
|
469
751
|
const visibilityChannel = createRequestChannel(core.messageTransport);
|
|
470
752
|
const removeVisibilityHandler = visibilityChannel.handle('setVisibility', visible => {
|
|
471
753
|
native.setVisible(visible).catch(err => {
|
|
@@ -479,6 +761,15 @@ const setupEmbeddedWebView = ({
|
|
|
479
761
|
});
|
|
480
762
|
};
|
|
481
763
|
core.messageTransport.on(handleHostMessage);
|
|
764
|
+
// Watch for the webview-side SDK ready signal so we can clear the
|
|
765
|
+
// recovery timer. The phase-timer factory also subscribes for its own
|
|
766
|
+
// bookkeeping — both subscriptions are independent and idempotent.
|
|
767
|
+
const handleSdkReady = message => {
|
|
768
|
+
if (message.origin !== 'webview') return;
|
|
769
|
+
if (message.type !== sdkHasLoadedEventName) return;
|
|
770
|
+
clearRecoveryTimer();
|
|
771
|
+
};
|
|
772
|
+
core.messageTransport.on(handleSdkReady);
|
|
482
773
|
const onMessageSub = native.addListener('onMessage', event => {
|
|
483
774
|
let parsed = null;
|
|
484
775
|
try {
|
|
@@ -501,7 +792,32 @@ const setupEmbeddedWebView = ({
|
|
|
501
792
|
logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
|
|
502
793
|
});
|
|
503
794
|
});
|
|
795
|
+
const onLoadStartSub = native.addListener('onLoadStart', () => {
|
|
796
|
+
phaseTimers.recordEvent('load_start');
|
|
797
|
+
// Re-arm both timers on every load_start. A retry from the native
|
|
798
|
+
// side (e.g. a redirect, or a reload after a transient failure)
|
|
799
|
+
// resets the html_load window cleanly without leaving a stale timer
|
|
800
|
+
// around from the previous attempt.
|
|
801
|
+
clearLoadingTimer();
|
|
802
|
+
clearRecoveryTimer();
|
|
803
|
+
loadingTimer = setTimeout(() => {
|
|
804
|
+
raiseFailure('html_load');
|
|
805
|
+
}, _loadingTimeoutMs);
|
|
806
|
+
});
|
|
807
|
+
const onLoadSub = native.addListener('onLoad', () => {
|
|
808
|
+
phaseTimers.recordEvent('load');
|
|
809
|
+
clearLoadingTimer();
|
|
810
|
+
});
|
|
811
|
+
const onLoadEndSub = native.addListener('onLoadEnd', () => {
|
|
812
|
+
phaseTimers.recordEvent('load_end');
|
|
813
|
+
clearLoadingTimer();
|
|
814
|
+
clearRecoveryTimer();
|
|
815
|
+
recoveryTimer = setTimeout(() => {
|
|
816
|
+
raiseFailure('sdk_bootstrap');
|
|
817
|
+
}, _recoveryTimeoutMs);
|
|
818
|
+
});
|
|
504
819
|
const onLoadErrorSub = native.addListener('onLoadError', event => {
|
|
820
|
+
phaseTimers.recordEvent('native_error');
|
|
505
821
|
logger.warn('EmbeddedWebView load error', event);
|
|
506
822
|
logger.instrument('Embedded webview load error', {
|
|
507
823
|
environmentId: core.environmentId,
|
|
@@ -515,7 +831,13 @@ const setupEmbeddedWebView = ({
|
|
|
515
831
|
time: 0,
|
|
516
832
|
webviewUrl: builtUrl.toString()
|
|
517
833
|
});
|
|
518
|
-
|
|
834
|
+
raiseFailure('embedded_native_error', {
|
|
835
|
+
nativeErrorCode: event.code,
|
|
836
|
+
nativeErrorDescription: event.description,
|
|
837
|
+
nativeErrorDomain: event.domain,
|
|
838
|
+
nativeErrorFailedUrl: event.url,
|
|
839
|
+
nativeErrorIsProvisional: event.isProvisional
|
|
840
|
+
});
|
|
519
841
|
});
|
|
520
842
|
native.setDebuggingEnabled(_webviewDebuggingEnabled).catch(err => {
|
|
521
843
|
logger.warn('EmbeddedWebView.setDebuggingEnabled failed', err);
|
|
@@ -524,11 +846,18 @@ const setupEmbeddedWebView = ({
|
|
|
524
846
|
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
525
847
|
});
|
|
526
848
|
return () => {
|
|
849
|
+
clearLoadingTimer();
|
|
850
|
+
clearRecoveryTimer();
|
|
527
851
|
removeVisibilityHandler();
|
|
528
852
|
core.messageTransport.off(handleHostMessage);
|
|
853
|
+
core.messageTransport.off(handleSdkReady);
|
|
529
854
|
onMessageSub.remove();
|
|
530
855
|
onShouldStartLoadSub.remove();
|
|
856
|
+
onLoadStartSub.remove();
|
|
857
|
+
onLoadSub.remove();
|
|
858
|
+
onLoadEndSub.remove();
|
|
531
859
|
onLoadErrorSub.remove();
|
|
860
|
+
phaseTimers.dispose();
|
|
532
861
|
};
|
|
533
862
|
};
|
|
534
863
|
|
|
@@ -380,12 +380,34 @@ extension EmbeddedWebViewController: WKNavigationDelegate {
|
|
|
380
380
|
emitLoadError(error: error as NSError, isProvisional: false, url: webView.url?.absoluteString)
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
public func webView(
|
|
384
|
+
_ webView: WKWebView,
|
|
385
|
+
didStartProvisionalNavigation _: WKNavigation!
|
|
386
|
+
) {
|
|
387
|
+
eventEmitter?("onLoadStart", [
|
|
388
|
+
"url": webView.url?.absoluteString ?? "",
|
|
389
|
+
])
|
|
390
|
+
}
|
|
391
|
+
|
|
383
392
|
public func webView(_ webView: WKWebView, didCommit _: WKNavigation!) {
|
|
384
393
|
// WKContentView is created lazily once content starts being committed —
|
|
385
394
|
// suppress its input accessory bar here so focusing inputs in the page
|
|
386
395
|
// doesn't show the system "< > Done" toolbar. Matches the
|
|
387
396
|
// `hideKeyboardAccessoryView` setting used by `react-native-webview`.
|
|
388
397
|
hideInputAccessoryView(on: webView)
|
|
398
|
+
|
|
399
|
+
// didCommit fires when the first byte of the response is rendered —
|
|
400
|
+
// analogous to RN's `onLoad` callback (page content has actually
|
|
401
|
+
// started arriving, not just the navigation request).
|
|
402
|
+
eventEmitter?("onLoad", [
|
|
403
|
+
"url": webView.url?.absoluteString ?? "",
|
|
404
|
+
])
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
public func webView(_ webView: WKWebView, didFinish _: WKNavigation!) {
|
|
408
|
+
eventEmitter?("onLoadEnd", [
|
|
409
|
+
"url": webView.url?.absoluteString ?? "",
|
|
410
|
+
])
|
|
389
411
|
}
|
|
390
412
|
|
|
391
413
|
public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
|
@@ -6,7 +6,14 @@ public class EmbeddedWebViewModule: Module {
|
|
|
6
6
|
public func definition() -> ModuleDefinition {
|
|
7
7
|
Name("EmbeddedWebView")
|
|
8
8
|
|
|
9
|
-
Events(
|
|
9
|
+
Events(
|
|
10
|
+
"onMessage",
|
|
11
|
+
"onShouldStartLoad",
|
|
12
|
+
"onLoadError",
|
|
13
|
+
"onLoadStart",
|
|
14
|
+
"onLoad",
|
|
15
|
+
"onLoadEnd"
|
|
16
|
+
)
|
|
10
17
|
|
|
11
18
|
OnCreate {
|
|
12
19
|
self.emitterToken = EmbeddedWebViewController.shared.setEmitter { [weak self] name, payload in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamic-labs/react-native-extension",
|
|
3
|
-
"version": "4.83.0",
|
|
3
|
+
"version": "4.83.2-alpha.0",
|
|
4
4
|
"main": "./index.cjs",
|
|
5
5
|
"module": "./index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"@turnkey/react-native-passkey-stamper": "1.2.7",
|
|
19
19
|
"@react-native-documents/picker": "^11.0.0",
|
|
20
20
|
"react-native-fs": ">=2.20.0",
|
|
21
|
-
"@dynamic-labs/assert-package-version": "4.83.0",
|
|
22
|
-
"@dynamic-labs/client": "4.83.0",
|
|
23
|
-
"@dynamic-labs/logger": "4.83.0",
|
|
24
|
-
"@dynamic-labs/message-transport": "4.83.0",
|
|
25
|
-
"@dynamic-labs/webview-messages": "4.83.0"
|
|
21
|
+
"@dynamic-labs/assert-package-version": "4.83.2-alpha.0",
|
|
22
|
+
"@dynamic-labs/client": "4.83.2-alpha.0",
|
|
23
|
+
"@dynamic-labs/logger": "4.83.2-alpha.0",
|
|
24
|
+
"@dynamic-labs/message-transport": "4.83.2-alpha.0",
|
|
25
|
+
"@dynamic-labs/webview-messages": "4.83.2-alpha.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": ">=18.0.0 <20.0.0",
|
|
@@ -3,5 +3,24 @@ export type SetupEmbeddedWebViewArgs = {
|
|
|
3
3
|
webviewUrl: string;
|
|
4
4
|
core: Core;
|
|
5
5
|
webviewDebuggingEnabled?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Max time (ms) between the native webview reporting it has started a
|
|
8
|
+
* navigation (`onLoadStart`) and reporting that the response committed
|
|
9
|
+
* (`onLoad`). If this elapses, the SDK raises
|
|
10
|
+
* {@link WebViewFailedToLoadError} with `phase: 'html_load'` so the page
|
|
11
|
+
* can no longer hang silently waiting for bytes that never arrive.
|
|
12
|
+
*
|
|
13
|
+
* Defaults to 20s, matching the RN `<WebView>` path.
|
|
14
|
+
*/
|
|
15
|
+
loadingTimeoutMs?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Max time (ms) between the native webview reporting it has finished
|
|
18
|
+
* loading (`onLoadEnd`) and the SDK signalling it has finished booting
|
|
19
|
+
* (`sdkHasLoadedEventName`). If this elapses, the SDK raises
|
|
20
|
+
* {@link WebViewFailedToLoadError} with `phase: 'sdk_bootstrap'`.
|
|
21
|
+
*
|
|
22
|
+
* Defaults to 20s, matching the RN `<WebView>` path.
|
|
23
|
+
*/
|
|
24
|
+
recoveryTimeoutMs?: number;
|
|
6
25
|
};
|
|
7
|
-
export declare const setupEmbeddedWebView: ({ webviewUrl, core, webviewDebuggingEnabled, }: SetupEmbeddedWebViewArgs) => (() => void);
|
|
26
|
+
export declare const setupEmbeddedWebView: ({ webviewUrl, core, webviewDebuggingEnabled, loadingTimeoutMs, recoveryTimeoutMs, }: SetupEmbeddedWebViewArgs) => (() => void);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Core } from '@dynamic-labs/client';
|
|
2
|
+
import { WebViewFailedToLoadErrorMeta, WebViewFailedToLoadErrorPhase } from '../../../../errors/WebViewFailedToLoadError';
|
|
3
|
+
/**
|
|
4
|
+
* Per-attempt lifecycle event recorded by
|
|
5
|
+
* {@link createEmbeddedWebViewPhaseTimers}. `load_start` resets the per-attempt
|
|
6
|
+
* state (so the timings reflect the current load); `native_error` and
|
|
7
|
+
* `os_kill` increment cumulative counters that persist across reloads.
|
|
8
|
+
*/
|
|
9
|
+
export type EmbeddedWebViewPhaseEvent = 'load_start' | 'load' | 'load_end' | 'native_error' | 'os_kill';
|
|
10
|
+
type EmbeddedWebViewPhaseTimersProps = {
|
|
11
|
+
core: Core;
|
|
12
|
+
webViewUrl: URL;
|
|
13
|
+
loadingTimeoutMs: number;
|
|
14
|
+
recoveryTimeoutMs: number;
|
|
15
|
+
};
|
|
16
|
+
export type EmbeddedWebViewPhaseTimers = {
|
|
17
|
+
/**
|
|
18
|
+
* Record a per-attempt lifecycle event. See {@link EmbeddedWebViewPhaseEvent}
|
|
19
|
+
* for the semantics of each event.
|
|
20
|
+
*/
|
|
21
|
+
recordEvent: (event: EmbeddedWebViewPhaseEvent) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Build the structured meta to attach to
|
|
24
|
+
* {@link WebViewFailedToLoadError}. The `phase` argument reflects the
|
|
25
|
+
* source of the failure; the duration fields come from the recorded
|
|
26
|
+
* lifecycle events and observed message-transport activity.
|
|
27
|
+
*/
|
|
28
|
+
getMeta: (args: {
|
|
29
|
+
phase: WebViewFailedToLoadErrorPhase;
|
|
30
|
+
}) => WebViewFailedToLoadErrorMeta;
|
|
31
|
+
/**
|
|
32
|
+
* Detach from the message transport. Call this on teardown so the
|
|
33
|
+
* embedded webview controller can be re-created without leaking
|
|
34
|
+
* subscriptions.
|
|
35
|
+
*/
|
|
36
|
+
dispose: () => void;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Non-React equivalent of `useWebViewPhaseTimers` for the embedded native
|
|
40
|
+
* webview path. The embedded path is a singleton wired by `setupEmbeddedWebView`
|
|
41
|
+
* (not a React component), so we expose the same phase-timing surface as a
|
|
42
|
+
* plain factory.
|
|
43
|
+
*
|
|
44
|
+
* The hook and the factory are intentionally separate: the embedded path does
|
|
45
|
+
* not have a retry or clear-state model, so `retryCount` is always `0` and
|
|
46
|
+
* `hadClearState` is always `false` in the meta produced here. Sharing one
|
|
47
|
+
* implementation would require either pulling React into a hot non-React path
|
|
48
|
+
* or threading a stateless adapter through both consumers \u2014 the duplication
|
|
49
|
+
* is small enough that keeping them separate stays cleaner.
|
|
50
|
+
*/
|
|
51
|
+
export declare const createEmbeddedWebViewPhaseTimers: ({ core, webViewUrl, loadingTimeoutMs, recoveryTimeoutMs, }: EmbeddedWebViewPhaseTimersProps) => EmbeddedWebViewPhaseTimers;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createEmbeddedWebViewPhaseTimers, type EmbeddedWebViewPhaseEvent, type EmbeddedWebViewPhaseTimers, } from './embeddedWebViewPhaseTimers';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useWebViewPhaseTimers, type WebViewPhaseTimers, } from './useWebViewPhaseTimers';
|