@dynamic-labs/react-native-extension 4.83.1 → 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/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -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/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -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/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +26 -0
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +8 -1
- package/index.cjs +350 -24
- package/index.js +351 -25
- 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/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.
|
|
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) {
|
|
@@ -100,6 +101,122 @@ const useMessageTransportWebViewBridge = (core, webViewRef) => {
|
|
|
100
101
|
};
|
|
101
102
|
};
|
|
102
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
|
+
|
|
103
220
|
const useWebViewVisibility = core => {
|
|
104
221
|
const webViewVisibilityRequestChannelRef = useRef(createRequestChannel(core.messageTransport));
|
|
105
222
|
const [visible, setVisible] = useState(false);
|
|
@@ -136,10 +253,6 @@ const styles = StyleSheet.create({
|
|
|
136
253
|
}
|
|
137
254
|
});
|
|
138
255
|
|
|
139
|
-
const CLEAR_STATE_PARAM = 'clear-state';
|
|
140
|
-
const ENVIRONMENT_ID_PARAM = 'environmentId';
|
|
141
|
-
const WEBVIEW_START_TIME_PARAM = 'webviewStartTime';
|
|
142
|
-
|
|
143
256
|
const setClearStateToUrl = webViewUrl => {
|
|
144
257
|
const newWebViewUrl = new URL(webViewUrl);
|
|
145
258
|
newWebViewUrl.searchParams.set(CLEAR_STATE_PARAM, 'true');
|
|
@@ -147,8 +260,6 @@ const setClearStateToUrl = webViewUrl => {
|
|
|
147
260
|
return newWebViewUrl;
|
|
148
261
|
};
|
|
149
262
|
|
|
150
|
-
const hasClearStateInUrl = webViewUrl => Boolean(webViewUrl.searchParams.get(CLEAR_STATE_PARAM));
|
|
151
|
-
|
|
152
263
|
const useWebViewRecoveryTimeout = ({
|
|
153
264
|
core,
|
|
154
265
|
webViewUrl,
|
|
@@ -301,15 +412,15 @@ const shouldAllowNavigation = (request, webViewUrl) => {
|
|
|
301
412
|
return false;
|
|
302
413
|
};
|
|
303
414
|
|
|
304
|
-
const DEFAULT_RECOVERY_TIMEOUT_MS = 20000;
|
|
305
|
-
const DEFAULT_LOADING_TIMEOUT_MS = 20000;
|
|
415
|
+
const DEFAULT_RECOVERY_TIMEOUT_MS$1 = 20000;
|
|
416
|
+
const DEFAULT_LOADING_TIMEOUT_MS$1 = 20000;
|
|
306
417
|
const WebView = ({
|
|
307
418
|
webviewUrl: initialWebViewUrl,
|
|
308
419
|
core,
|
|
309
420
|
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
310
421
|
disableRecovery: _disableRecovery = false,
|
|
311
|
-
recoveryTimeout: _recoveryTimeout = DEFAULT_RECOVERY_TIMEOUT_MS,
|
|
312
|
-
loadingTimeout: _loadingTimeout = DEFAULT_LOADING_TIMEOUT_MS
|
|
422
|
+
recoveryTimeout: _recoveryTimeout = DEFAULT_RECOVERY_TIMEOUT_MS$1,
|
|
423
|
+
loadingTimeout: _loadingTimeout = DEFAULT_LOADING_TIMEOUT_MS$1
|
|
313
424
|
}) => {
|
|
314
425
|
const webViewRef = useRef(null);
|
|
315
426
|
const {
|
|
@@ -319,6 +430,15 @@ const WebView = ({
|
|
|
319
430
|
const {
|
|
320
431
|
onMessageHandler
|
|
321
432
|
} = useMessageTransportWebViewBridge(core, webViewRef);
|
|
433
|
+
const {
|
|
434
|
+
recordEvent,
|
|
435
|
+
getMeta
|
|
436
|
+
} = useWebViewPhaseTimers({
|
|
437
|
+
core,
|
|
438
|
+
loadingTimeout: _loadingTimeout,
|
|
439
|
+
recoveryTimeout: _recoveryTimeout,
|
|
440
|
+
webViewUrl
|
|
441
|
+
});
|
|
322
442
|
const containerStyles = [styles['container'], visible ? styles.show : styles.hide];
|
|
323
443
|
/**
|
|
324
444
|
* Block the message transport when the webview is unmounted
|
|
@@ -360,13 +480,23 @@ const WebView = ({
|
|
|
360
480
|
return newWebViewUrl;
|
|
361
481
|
});
|
|
362
482
|
}, [core]);
|
|
363
|
-
const blockAndReloadWebViewOsKill = useCallback(() =>
|
|
483
|
+
const blockAndReloadWebViewOsKill = useCallback(() => {
|
|
484
|
+
recordEvent('os_kill');
|
|
485
|
+
blockAndReloadWebView('os_kill');
|
|
486
|
+
}, [blockAndReloadWebView, recordEvent]);
|
|
364
487
|
useEffect(() => core.messageTransport.recoveryManager.onRecoveryRequested(() => blockAndReloadWebView('recovery')), [core, blockAndReloadWebView]);
|
|
365
|
-
const setWebViewLoadError = useCallback(
|
|
488
|
+
const setWebViewLoadError = useCallback(phase => {
|
|
366
489
|
// Error was already thrown, do not throw again
|
|
367
490
|
if (core.initialization.error instanceof WebViewFailedToLoadError) return;
|
|
368
|
-
core.initialization.error = new WebViewFailedToLoadError(
|
|
369
|
-
|
|
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]);
|
|
370
500
|
/**
|
|
371
501
|
* Reload the webview with a clean state when a timeout is reached
|
|
372
502
|
* and the webview did not get to the loaded state yet
|
|
@@ -374,7 +504,7 @@ const WebView = ({
|
|
|
374
504
|
const startRecoveryTimeout = useWebViewRecoveryTimeout({
|
|
375
505
|
core,
|
|
376
506
|
disableRecovery: _disableRecovery,
|
|
377
|
-
onFailedToLoadAfterClearState:
|
|
507
|
+
onFailedToLoadAfterClearState: setWebViewLoadErrorWithPhase('after_clear_state'),
|
|
378
508
|
recoveryTimeout: _recoveryTimeout,
|
|
379
509
|
setWebViewUrl,
|
|
380
510
|
webViewUrl
|
|
@@ -382,17 +512,30 @@ const WebView = ({
|
|
|
382
512
|
const webViewLoadErrorCountRef = useRef(0);
|
|
383
513
|
const onWebViewLoadError = useCallback(() => {
|
|
384
514
|
webViewLoadErrorCountRef.current = webViewLoadErrorCountRef.current + 1;
|
|
515
|
+
recordEvent('native_error');
|
|
385
516
|
/**
|
|
386
517
|
* This is the first attempt to load the webview, do not throw an error
|
|
387
518
|
* because the recovery system will attempt to reload the webview
|
|
388
519
|
*/
|
|
389
520
|
if (webViewLoadErrorCountRef.current === 1) return;
|
|
390
|
-
setWebViewLoadError();
|
|
391
|
-
}, [setWebViewLoadError]);
|
|
521
|
+
setWebViewLoadError('native_error');
|
|
522
|
+
}, [recordEvent, setWebViewLoadError]);
|
|
392
523
|
const {
|
|
393
|
-
onLoad,
|
|
394
|
-
onLoadStart
|
|
395
|
-
} = 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]);
|
|
396
539
|
return /*#__PURE__*/jsx(WebView$1, {
|
|
397
540
|
ref: webViewRef,
|
|
398
541
|
source: {
|
|
@@ -401,7 +544,7 @@ const WebView = ({
|
|
|
401
544
|
containerStyle: containerStyles,
|
|
402
545
|
style: styles['webview'],
|
|
403
546
|
onMessage: onMessageHandler,
|
|
404
|
-
onLoadEnd:
|
|
547
|
+
onLoadEnd: onLoadEnd,
|
|
405
548
|
onLoadStart: onLoadStart,
|
|
406
549
|
onLoad: onLoad,
|
|
407
550
|
hideKeyboardAccessoryView: true,
|
|
@@ -453,6 +596,105 @@ const getEmbeddedWebView = () => {
|
|
|
453
596
|
}
|
|
454
597
|
};
|
|
455
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;
|
|
456
698
|
// Wires the JS-side bridge to the native WKWebView singleton owned by Swift.
|
|
457
699
|
// This is intentionally not a React component: the embedded webview lives
|
|
458
700
|
// outside the React tree (in a dedicated UIWindow on iOS), so its setup runs
|
|
@@ -465,10 +707,47 @@ const getEmbeddedWebView = () => {
|
|
|
465
707
|
const setupEmbeddedWebView = ({
|
|
466
708
|
webviewUrl,
|
|
467
709
|
core,
|
|
468
|
-
webviewDebuggingEnabled: _webviewDebuggingEnabled = false
|
|
710
|
+
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
711
|
+
loadingTimeoutMs: _loadingTimeoutMs = DEFAULT_LOADING_TIMEOUT_MS,
|
|
712
|
+
recoveryTimeoutMs: _recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS
|
|
469
713
|
}) => {
|
|
470
714
|
const native = getEmbeddedWebView();
|
|
471
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
|
+
};
|
|
472
751
|
const visibilityChannel = createRequestChannel(core.messageTransport);
|
|
473
752
|
const removeVisibilityHandler = visibilityChannel.handle('setVisibility', visible => {
|
|
474
753
|
native.setVisible(visible).catch(err => {
|
|
@@ -482,6 +761,15 @@ const setupEmbeddedWebView = ({
|
|
|
482
761
|
});
|
|
483
762
|
};
|
|
484
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);
|
|
485
773
|
const onMessageSub = native.addListener('onMessage', event => {
|
|
486
774
|
let parsed = null;
|
|
487
775
|
try {
|
|
@@ -504,7 +792,32 @@ const setupEmbeddedWebView = ({
|
|
|
504
792
|
logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
|
|
505
793
|
});
|
|
506
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
|
+
});
|
|
507
819
|
const onLoadErrorSub = native.addListener('onLoadError', event => {
|
|
820
|
+
phaseTimers.recordEvent('native_error');
|
|
508
821
|
logger.warn('EmbeddedWebView load error', event);
|
|
509
822
|
logger.instrument('Embedded webview load error', {
|
|
510
823
|
environmentId: core.environmentId,
|
|
@@ -518,7 +831,13 @@ const setupEmbeddedWebView = ({
|
|
|
518
831
|
time: 0,
|
|
519
832
|
webviewUrl: builtUrl.toString()
|
|
520
833
|
});
|
|
521
|
-
|
|
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
|
+
});
|
|
522
841
|
});
|
|
523
842
|
native.setDebuggingEnabled(_webviewDebuggingEnabled).catch(err => {
|
|
524
843
|
logger.warn('EmbeddedWebView.setDebuggingEnabled failed', err);
|
|
@@ -527,11 +846,18 @@ const setupEmbeddedWebView = ({
|
|
|
527
846
|
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
528
847
|
});
|
|
529
848
|
return () => {
|
|
849
|
+
clearLoadingTimer();
|
|
850
|
+
clearRecoveryTimer();
|
|
530
851
|
removeVisibilityHandler();
|
|
531
852
|
core.messageTransport.off(handleHostMessage);
|
|
853
|
+
core.messageTransport.off(handleSdkReady);
|
|
532
854
|
onMessageSub.remove();
|
|
533
855
|
onShouldStartLoadSub.remove();
|
|
856
|
+
onLoadStartSub.remove();
|
|
857
|
+
onLoadSub.remove();
|
|
858
|
+
onLoadEndSub.remove();
|
|
534
859
|
onLoadErrorSub.remove();
|
|
860
|
+
phaseTimers.dispose();
|
|
535
861
|
};
|
|
536
862
|
};
|
|
537
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.
|
|
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.
|
|
22
|
-
"@dynamic-labs/client": "4.83.
|
|
23
|
-
"@dynamic-labs/logger": "4.83.
|
|
24
|
-
"@dynamic-labs/message-transport": "4.83.
|
|
25
|
-
"@dynamic-labs/webview-messages": "4.83.
|
|
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';
|