@dynamic-labs/react-native-extension 4.81.0 → 4.83.1
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 +476 -0
- package/android/EmbeddedWebViewModule.kt +55 -0
- package/android/KeyStoreKeyManager.kt +7 -1
- package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- package/android/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- 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 +476 -0
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- package/android/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- package/android/src/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- package/android/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
- package/expo-module.config.json +5 -2
- package/index.cjs +178 -42
- package/index.js +178 -42
- package/ios/EmbeddedWebViewController.swift +426 -0
- package/ios/EmbeddedWebViewModule.swift +62 -0
- package/ios/Keychain.podspec +2 -2
- package/package.json +6 -6
- package/src/ReactNativeExtension/ReactNativeExtension.d.ts +24 -1
- package/src/components/WebView/EmbeddedWebView/EmbeddedWebView.d.ts +7 -0
- package/src/components/WebView/EmbeddedWebView/index.d.ts +1 -0
- package/src/components/WebView/utils/shouldAllowNavigation/index.d.ts +1 -0
- package/src/components/WebView/utils/shouldAllowNavigation/shouldAllowNavigation.d.ts +5 -0
- package/src/errors/WebViewFailedToLoadError.d.ts +84 -1
- package/src/nativeModules/EmbeddedWebView.d.ts +29 -0
package/index.cjs
CHANGED
|
@@ -9,12 +9,12 @@ var reactNativeWebview = require('react-native-webview');
|
|
|
9
9
|
var logger$1 = require('@dynamic-labs/logger');
|
|
10
10
|
var messageTransport = require('@dynamic-labs/message-transport');
|
|
11
11
|
var jsxRuntime = require('react/jsx-runtime');
|
|
12
|
+
var expoModulesCore = require('expo-modules-core');
|
|
12
13
|
var reactNativePasskey = require('react-native-passkey');
|
|
13
14
|
var expoLinking = require('expo-linking');
|
|
14
15
|
var expoWebBrowser = require('expo-web-browser');
|
|
15
16
|
var expoSecureStore = require('expo-secure-store');
|
|
16
17
|
var reactNativePasskeyStamper = require('@turnkey/react-native-passkey-stamper');
|
|
17
|
-
var expoModulesCore = require('expo-modules-core');
|
|
18
18
|
|
|
19
19
|
function _interopNamespace(e) {
|
|
20
20
|
if (e && e.__esModule) return e;
|
|
@@ -34,7 +34,7 @@ function _interopNamespace(e) {
|
|
|
34
34
|
return Object.freeze(n);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
var version = "4.
|
|
37
|
+
var version = "4.83.1";
|
|
38
38
|
|
|
39
39
|
function _extends() {
|
|
40
40
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -47,8 +47,9 @@ function _extends() {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
class WebViewFailedToLoadError extends Error {
|
|
50
|
-
constructor() {
|
|
50
|
+
constructor(meta) {
|
|
51
51
|
super('Could not load Dynamic WebView');
|
|
52
|
+
this.meta = meta;
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -289,27 +290,48 @@ const assignStartTimeToUrl = url => {
|
|
|
289
290
|
};
|
|
290
291
|
|
|
291
292
|
const waasOrigins = ['https://app.dynamic-preprod.xyz', 'https://app.dynamicauth.com'];
|
|
293
|
+
const turnkeyOrigins = ['https://recovery.turnkey.com', 'https://export.turnkey.com'];
|
|
292
294
|
const isNonProductionBuild = () => process.env['NODE_ENV'] !== 'production';
|
|
293
295
|
const isAllowedWaasOrigin = origin => {
|
|
294
296
|
if (waasOrigins.includes(origin)) {
|
|
295
297
|
return true;
|
|
296
298
|
}
|
|
297
|
-
// Dev-only fallback: allow localhost so mobile-demo can load the iframe
|
|
298
|
-
// against a local redcoast stack. Never allowed in production bundles to
|
|
299
|
-
// prevent a malicious app on the device from impersonating the iframe.
|
|
300
299
|
if (isNonProductionBuild() && /^http:\/\/localhost:\d+$/.test(origin)) {
|
|
301
300
|
return true;
|
|
302
301
|
}
|
|
303
302
|
return false;
|
|
304
303
|
};
|
|
305
|
-
const
|
|
304
|
+
const parseUrl = url => {
|
|
305
|
+
try {
|
|
306
|
+
return new URL(url);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
logger.error('Failed to get origin from url', error);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const shouldAllowNavigation = (request, webViewUrl) => {
|
|
313
|
+
const requestUrl = parseUrl(request.url);
|
|
314
|
+
if (!requestUrl) return false;
|
|
315
|
+
// Sub-frame (iframe) requests are controlled by the trusted web app
|
|
316
|
+
// content — allow them through without restriction.
|
|
317
|
+
if (!request.isTopFrame) return true;
|
|
318
|
+
if (webViewUrl.origin === requestUrl.origin) return true;
|
|
319
|
+
if (requestUrl.pathname.startsWith('/waas-v1') && isAllowedWaasOrigin(requestUrl.origin)) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
if (turnkeyOrigins.includes(requestUrl.origin)) return true;
|
|
323
|
+
return false;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const DEFAULT_RECOVERY_TIMEOUT_MS = 20000;
|
|
327
|
+
const DEFAULT_LOADING_TIMEOUT_MS = 20000;
|
|
306
328
|
const WebView = ({
|
|
307
329
|
webviewUrl: initialWebViewUrl,
|
|
308
330
|
core,
|
|
309
331
|
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
310
332
|
disableRecovery: _disableRecovery = false,
|
|
311
|
-
recoveryTimeout: _recoveryTimeout =
|
|
312
|
-
loadingTimeout: _loadingTimeout =
|
|
333
|
+
recoveryTimeout: _recoveryTimeout = DEFAULT_RECOVERY_TIMEOUT_MS,
|
|
334
|
+
loadingTimeout: _loadingTimeout = DEFAULT_LOADING_TIMEOUT_MS
|
|
313
335
|
}) => {
|
|
314
336
|
const webViewRef = react.useRef(null);
|
|
315
337
|
const {
|
|
@@ -410,40 +432,12 @@ const WebView = ({
|
|
|
410
432
|
onRenderProcessGone: blockAndReloadWebViewOsKill,
|
|
411
433
|
onError: onWebViewLoadError,
|
|
412
434
|
setSupportMultipleWindows: false,
|
|
413
|
-
onShouldStartLoadWithRequest: request => {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
// Sub-frame (iframe) requests are controlled by the trusted web app
|
|
418
|
-
// content — allow them through without restriction. On iOS,
|
|
419
|
-
// onShouldStartLoadWithRequest fires for iframe navigations too,
|
|
420
|
-
// unlike Android, which is why third-party iframes (e.g. Banxa
|
|
421
|
-
// checkout) would otherwise be blocked only on iOS.
|
|
422
|
-
if (!request.isTopFrame) return true;
|
|
423
|
-
// Same origin as the webview, allow top-level navigation
|
|
424
|
-
if (webViewUrl.origin === requestUrl.origin) {
|
|
425
|
-
return true;
|
|
426
|
-
}
|
|
427
|
-
// Allow WAAS top-level navigation
|
|
428
|
-
if (requestUrl.pathname.startsWith('/waas-v1') && isAllowedWaasOrigin(requestUrl.origin)) {
|
|
429
|
-
return true;
|
|
430
|
-
}
|
|
431
|
-
// Allow TurnkeyV1 top-level navigation
|
|
432
|
-
if (turnkeyOrigins.includes(requestUrl.origin)) {
|
|
433
|
-
return true;
|
|
434
|
-
}
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
435
|
+
onShouldStartLoadWithRequest: request => shouldAllowNavigation({
|
|
436
|
+
isTopFrame: request.isTopFrame,
|
|
437
|
+
url: request.url
|
|
438
|
+
}, webViewUrl)
|
|
437
439
|
});
|
|
438
440
|
};
|
|
439
|
-
const getUrl = url => {
|
|
440
|
-
try {
|
|
441
|
-
return new URL(url);
|
|
442
|
-
} catch (error) {
|
|
443
|
-
logger.error('Failed to get origin from url', error);
|
|
444
|
-
return null;
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
441
|
const createWebView = internalProps => {
|
|
448
442
|
const WebViewWrapper = props => /*#__PURE__*/jsxRuntime.jsx(WebView, _extends({}, internalProps, props));
|
|
449
443
|
return WebViewWrapper;
|
|
@@ -458,6 +452,111 @@ For more information, please refer to the documentation:
|
|
|
458
452
|
https://docs.dynamic.xyz/react-native/react-native-extension
|
|
459
453
|
`;
|
|
460
454
|
|
|
455
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
456
|
+
const NATIVE_MODULE_NAME = 'EmbeddedWebView';
|
|
457
|
+
const getEmbeddedWebView = () => {
|
|
458
|
+
try {
|
|
459
|
+
return expoModulesCore.requireNativeModule(NATIVE_MODULE_NAME);
|
|
460
|
+
} catch (_a) {
|
|
461
|
+
logger.warn('Could not get EmbeddedWebView native module, using unavailable fallback');
|
|
462
|
+
const unavailableError = new Error('EmbeddedWebView is not available');
|
|
463
|
+
const unavailable = {
|
|
464
|
+
addListener: () => ({
|
|
465
|
+
remove: () => undefined
|
|
466
|
+
}),
|
|
467
|
+
destroy: () => Promise.reject(unavailableError),
|
|
468
|
+
postMessage: () => Promise.reject(unavailableError),
|
|
469
|
+
respondToShouldStartLoad: () => Promise.reject(unavailableError),
|
|
470
|
+
setDebuggingEnabled: () => Promise.reject(unavailableError),
|
|
471
|
+
setUrl: () => Promise.reject(unavailableError),
|
|
472
|
+
setVisible: () => Promise.reject(unavailableError)
|
|
473
|
+
};
|
|
474
|
+
return unavailable;
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// Wires the JS-side bridge to the native WKWebView singleton owned by Swift.
|
|
479
|
+
// This is intentionally not a React component: the embedded webview lives
|
|
480
|
+
// outside the React tree (in a dedicated UIWindow on iOS), so its setup runs
|
|
481
|
+
// once when the SDK is initialised — there's nothing to mount or unmount.
|
|
482
|
+
//
|
|
483
|
+
// Returns a teardown that removes every JS-side subscription. The native
|
|
484
|
+
// singleton is intentionally NOT destroyed — re-running setup with a fresh
|
|
485
|
+
// `core` (e.g. on client recreation with a new environmentId) just reloads
|
|
486
|
+
// the existing webview via setUrl.
|
|
487
|
+
const setupEmbeddedWebView = ({
|
|
488
|
+
webviewUrl,
|
|
489
|
+
core,
|
|
490
|
+
webviewDebuggingEnabled: _webviewDebuggingEnabled = false
|
|
491
|
+
}) => {
|
|
492
|
+
const native = getEmbeddedWebView();
|
|
493
|
+
const builtUrl = assignStartTimeToUrl(assignEnvironmentIdToUrl(webviewUrl, core.environmentId));
|
|
494
|
+
const visibilityChannel = messageTransport.createRequestChannel(core.messageTransport);
|
|
495
|
+
const removeVisibilityHandler = visibilityChannel.handle('setVisibility', visible => {
|
|
496
|
+
native.setVisible(visible).catch(err => {
|
|
497
|
+
logger.warn('EmbeddedWebView.setVisible failed', err);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
const handleHostMessage = message => {
|
|
501
|
+
if (message.origin !== 'host') return;
|
|
502
|
+
native.postMessage(JSON.stringify(message, messageTransport.messageTransportDataJsonReplacer)).catch(err => {
|
|
503
|
+
logger.warn('EmbeddedWebView.postMessage failed', err);
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
core.messageTransport.on(handleHostMessage);
|
|
507
|
+
const onMessageSub = native.addListener('onMessage', event => {
|
|
508
|
+
let parsed = null;
|
|
509
|
+
try {
|
|
510
|
+
parsed = JSON.parse(event.message, messageTransport.messageTransportDataJsonReviver);
|
|
511
|
+
} catch (_a) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const message = messageTransport.parseMessageTransportData(parsed);
|
|
515
|
+
if (!message) return;
|
|
516
|
+
if (message.origin === 'webview') {
|
|
517
|
+
core.messageTransport.emit(message);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
const onShouldStartLoadSub = native.addListener('onShouldStartLoad', event => {
|
|
521
|
+
const allow = shouldAllowNavigation({
|
|
522
|
+
isTopFrame: event.isTopFrame,
|
|
523
|
+
url: event.url
|
|
524
|
+
}, builtUrl);
|
|
525
|
+
native.respondToShouldStartLoad(event.id, allow).catch(err => {
|
|
526
|
+
logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
const onLoadErrorSub = native.addListener('onLoadError', event => {
|
|
530
|
+
logger.warn('EmbeddedWebView load error', event);
|
|
531
|
+
logger.instrument('Embedded webview load error', {
|
|
532
|
+
environmentId: core.environmentId,
|
|
533
|
+
errorCode: event.code,
|
|
534
|
+
errorDescription: event.description,
|
|
535
|
+
errorDomain: event.domain,
|
|
536
|
+
failedUrl: event.url,
|
|
537
|
+
hostSdkSessionId: core.hostSdkSessionId,
|
|
538
|
+
isProvisional: event.isProvisional,
|
|
539
|
+
key: 'webview.embedded.load_error',
|
|
540
|
+
time: 0,
|
|
541
|
+
webviewUrl: builtUrl.toString()
|
|
542
|
+
});
|
|
543
|
+
core.initialization.error = new WebViewFailedToLoadError();
|
|
544
|
+
});
|
|
545
|
+
native.setDebuggingEnabled(_webviewDebuggingEnabled).catch(err => {
|
|
546
|
+
logger.warn('EmbeddedWebView.setDebuggingEnabled failed', err);
|
|
547
|
+
});
|
|
548
|
+
native.setUrl(builtUrl.toString()).catch(err => {
|
|
549
|
+
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
550
|
+
});
|
|
551
|
+
return () => {
|
|
552
|
+
removeVisibilityHandler();
|
|
553
|
+
core.messageTransport.off(handleHostMessage);
|
|
554
|
+
onMessageSub.remove();
|
|
555
|
+
onShouldStartLoadSub.remove();
|
|
556
|
+
onLoadErrorSub.remove();
|
|
557
|
+
};
|
|
558
|
+
};
|
|
559
|
+
|
|
461
560
|
/******************************************************************************
|
|
462
561
|
Copyright (c) Microsoft Corporation.
|
|
463
562
|
|
|
@@ -759,10 +858,17 @@ const setupKeychainHandler = core => {
|
|
|
759
858
|
};
|
|
760
859
|
|
|
761
860
|
const defaultWebviewUrl = `https://webview.dynamicauth.com/${version}`;
|
|
861
|
+
// The native webview singleton outlives the JS bridge, so a re-invocation
|
|
862
|
+
// of the extension factory (e.g. when the consumer recreates the client
|
|
863
|
+
// with a new environmentId) would otherwise stack JS-side listeners on top
|
|
864
|
+
// of the prior ones. We retain the previous teardown at module scope and
|
|
865
|
+
// run it before wiring the next bridge.
|
|
866
|
+
let previousEmbeddedWebViewTeardown = null;
|
|
762
867
|
const ReactNativeExtension = ({
|
|
763
868
|
webviewUrl: _webviewUrl = defaultWebviewUrl,
|
|
764
869
|
webviewDebuggingEnabled,
|
|
765
|
-
appOrigin
|
|
870
|
+
appOrigin,
|
|
871
|
+
embeddedWebView: _embeddedWebView = false
|
|
766
872
|
} = {}) => (_, core) => {
|
|
767
873
|
const isPlatformSupportedByWebView = reactNative.Platform.OS === 'android' || reactNative.Platform.OS === 'ios';
|
|
768
874
|
/**
|
|
@@ -782,6 +888,36 @@ const ReactNativeExtension = ({
|
|
|
782
888
|
setupPlatformHandler(core);
|
|
783
889
|
setupStorageHandler(core);
|
|
784
890
|
setupKeychainHandler(core);
|
|
891
|
+
// Native overlay-webview path supported on iOS (WKWebView) and Android
|
|
892
|
+
// (android.webkit.WebView). Other platforms fall back to react-native-webview.
|
|
893
|
+
const useNativeEmbeddedWebView = _embeddedWebView && (reactNative.Platform.OS === 'ios' || reactNative.Platform.OS === 'android');
|
|
894
|
+
if (useNativeEmbeddedWebView) {
|
|
895
|
+
// The native WKWebView lives in its own UIWindow, outside the React
|
|
896
|
+
// tree. Wire the JS bridge once at extension construction and hand the
|
|
897
|
+
// consumer a no-op component so its render never touches the webview.
|
|
898
|
+
if (previousEmbeddedWebViewTeardown) {
|
|
899
|
+
previousEmbeddedWebViewTeardown();
|
|
900
|
+
previousEmbeddedWebViewTeardown = null;
|
|
901
|
+
}
|
|
902
|
+
try {
|
|
903
|
+
previousEmbeddedWebViewTeardown = setupEmbeddedWebView({
|
|
904
|
+
core,
|
|
905
|
+
webviewDebuggingEnabled,
|
|
906
|
+
webviewUrl: _webviewUrl
|
|
907
|
+
});
|
|
908
|
+
} catch (err) {
|
|
909
|
+
// Setup failed — leave the slot empty so the next invocation isn't
|
|
910
|
+
// wedged with a stale teardown reference. The error itself is left
|
|
911
|
+
// to bubble: the consumer needs to see that the bridge isn't wired.
|
|
912
|
+
previousEmbeddedWebViewTeardown = null;
|
|
913
|
+
throw err;
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
reactNative: {
|
|
917
|
+
WebView: () => null
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
}
|
|
785
921
|
return {
|
|
786
922
|
reactNative: {
|
|
787
923
|
WebView: createWebView({
|
package/index.js
CHANGED
|
@@ -5,14 +5,14 @@ import { WebView as WebView$1 } from 'react-native-webview';
|
|
|
5
5
|
import { Logger } from '@dynamic-labs/logger';
|
|
6
6
|
import { messageTransportDataJsonReviver, parseMessageTransportData, messageTransportDataJsonReplacer, createRequestChannel } from '@dynamic-labs/message-transport';
|
|
7
7
|
import { jsx } from 'react/jsx-runtime';
|
|
8
|
+
import { requireNativeModule } from 'expo-modules-core';
|
|
8
9
|
import { Passkey } from 'react-native-passkey';
|
|
9
10
|
import { createURL, getInitialURL, addEventListener, openURL } from 'expo-linking';
|
|
10
11
|
import { openAuthSessionAsync } from 'expo-web-browser';
|
|
11
12
|
import { getItemAsync, deleteItemAsync, setItemAsync } from 'expo-secure-store';
|
|
12
13
|
import { createPasskey, PasskeyStamper } from '@turnkey/react-native-passkey-stamper';
|
|
13
|
-
import { requireNativeModule } from 'expo-modules-core';
|
|
14
14
|
|
|
15
|
-
var version = "4.
|
|
15
|
+
var version = "4.83.1";
|
|
16
16
|
|
|
17
17
|
function _extends() {
|
|
18
18
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -25,8 +25,9 @@ function _extends() {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
class WebViewFailedToLoadError extends Error {
|
|
28
|
-
constructor() {
|
|
28
|
+
constructor(meta) {
|
|
29
29
|
super('Could not load Dynamic WebView');
|
|
30
|
+
this.meta = meta;
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -267,27 +268,48 @@ const assignStartTimeToUrl = url => {
|
|
|
267
268
|
};
|
|
268
269
|
|
|
269
270
|
const waasOrigins = ['https://app.dynamic-preprod.xyz', 'https://app.dynamicauth.com'];
|
|
271
|
+
const turnkeyOrigins = ['https://recovery.turnkey.com', 'https://export.turnkey.com'];
|
|
270
272
|
const isNonProductionBuild = () => process.env['NODE_ENV'] !== 'production';
|
|
271
273
|
const isAllowedWaasOrigin = origin => {
|
|
272
274
|
if (waasOrigins.includes(origin)) {
|
|
273
275
|
return true;
|
|
274
276
|
}
|
|
275
|
-
// Dev-only fallback: allow localhost so mobile-demo can load the iframe
|
|
276
|
-
// against a local redcoast stack. Never allowed in production bundles to
|
|
277
|
-
// prevent a malicious app on the device from impersonating the iframe.
|
|
278
277
|
if (isNonProductionBuild() && /^http:\/\/localhost:\d+$/.test(origin)) {
|
|
279
278
|
return true;
|
|
280
279
|
}
|
|
281
280
|
return false;
|
|
282
281
|
};
|
|
283
|
-
const
|
|
282
|
+
const parseUrl = url => {
|
|
283
|
+
try {
|
|
284
|
+
return new URL(url);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
logger.error('Failed to get origin from url', error);
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const shouldAllowNavigation = (request, webViewUrl) => {
|
|
291
|
+
const requestUrl = parseUrl(request.url);
|
|
292
|
+
if (!requestUrl) return false;
|
|
293
|
+
// Sub-frame (iframe) requests are controlled by the trusted web app
|
|
294
|
+
// content — allow them through without restriction.
|
|
295
|
+
if (!request.isTopFrame) return true;
|
|
296
|
+
if (webViewUrl.origin === requestUrl.origin) return true;
|
|
297
|
+
if (requestUrl.pathname.startsWith('/waas-v1') && isAllowedWaasOrigin(requestUrl.origin)) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
if (turnkeyOrigins.includes(requestUrl.origin)) return true;
|
|
301
|
+
return false;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const DEFAULT_RECOVERY_TIMEOUT_MS = 20000;
|
|
305
|
+
const DEFAULT_LOADING_TIMEOUT_MS = 20000;
|
|
284
306
|
const WebView = ({
|
|
285
307
|
webviewUrl: initialWebViewUrl,
|
|
286
308
|
core,
|
|
287
309
|
webviewDebuggingEnabled: _webviewDebuggingEnabled = false,
|
|
288
310
|
disableRecovery: _disableRecovery = false,
|
|
289
|
-
recoveryTimeout: _recoveryTimeout =
|
|
290
|
-
loadingTimeout: _loadingTimeout =
|
|
311
|
+
recoveryTimeout: _recoveryTimeout = DEFAULT_RECOVERY_TIMEOUT_MS,
|
|
312
|
+
loadingTimeout: _loadingTimeout = DEFAULT_LOADING_TIMEOUT_MS
|
|
291
313
|
}) => {
|
|
292
314
|
const webViewRef = useRef(null);
|
|
293
315
|
const {
|
|
@@ -388,40 +410,12 @@ const WebView = ({
|
|
|
388
410
|
onRenderProcessGone: blockAndReloadWebViewOsKill,
|
|
389
411
|
onError: onWebViewLoadError,
|
|
390
412
|
setSupportMultipleWindows: false,
|
|
391
|
-
onShouldStartLoadWithRequest: request => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// Sub-frame (iframe) requests are controlled by the trusted web app
|
|
396
|
-
// content — allow them through without restriction. On iOS,
|
|
397
|
-
// onShouldStartLoadWithRequest fires for iframe navigations too,
|
|
398
|
-
// unlike Android, which is why third-party iframes (e.g. Banxa
|
|
399
|
-
// checkout) would otherwise be blocked only on iOS.
|
|
400
|
-
if (!request.isTopFrame) return true;
|
|
401
|
-
// Same origin as the webview, allow top-level navigation
|
|
402
|
-
if (webViewUrl.origin === requestUrl.origin) {
|
|
403
|
-
return true;
|
|
404
|
-
}
|
|
405
|
-
// Allow WAAS top-level navigation
|
|
406
|
-
if (requestUrl.pathname.startsWith('/waas-v1') && isAllowedWaasOrigin(requestUrl.origin)) {
|
|
407
|
-
return true;
|
|
408
|
-
}
|
|
409
|
-
// Allow TurnkeyV1 top-level navigation
|
|
410
|
-
if (turnkeyOrigins.includes(requestUrl.origin)) {
|
|
411
|
-
return true;
|
|
412
|
-
}
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
413
|
+
onShouldStartLoadWithRequest: request => shouldAllowNavigation({
|
|
414
|
+
isTopFrame: request.isTopFrame,
|
|
415
|
+
url: request.url
|
|
416
|
+
}, webViewUrl)
|
|
415
417
|
});
|
|
416
418
|
};
|
|
417
|
-
const getUrl = url => {
|
|
418
|
-
try {
|
|
419
|
-
return new URL(url);
|
|
420
|
-
} catch (error) {
|
|
421
|
-
logger.error('Failed to get origin from url', error);
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
419
|
const createWebView = internalProps => {
|
|
426
420
|
const WebViewWrapper = props => /*#__PURE__*/jsx(WebView, _extends({}, internalProps, props));
|
|
427
421
|
return WebViewWrapper;
|
|
@@ -436,6 +430,111 @@ For more information, please refer to the documentation:
|
|
|
436
430
|
https://docs.dynamic.xyz/react-native/react-native-extension
|
|
437
431
|
`;
|
|
438
432
|
|
|
433
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
434
|
+
const NATIVE_MODULE_NAME = 'EmbeddedWebView';
|
|
435
|
+
const getEmbeddedWebView = () => {
|
|
436
|
+
try {
|
|
437
|
+
return requireNativeModule(NATIVE_MODULE_NAME);
|
|
438
|
+
} catch (_a) {
|
|
439
|
+
logger.warn('Could not get EmbeddedWebView native module, using unavailable fallback');
|
|
440
|
+
const unavailableError = new Error('EmbeddedWebView is not available');
|
|
441
|
+
const unavailable = {
|
|
442
|
+
addListener: () => ({
|
|
443
|
+
remove: () => undefined
|
|
444
|
+
}),
|
|
445
|
+
destroy: () => Promise.reject(unavailableError),
|
|
446
|
+
postMessage: () => Promise.reject(unavailableError),
|
|
447
|
+
respondToShouldStartLoad: () => Promise.reject(unavailableError),
|
|
448
|
+
setDebuggingEnabled: () => Promise.reject(unavailableError),
|
|
449
|
+
setUrl: () => Promise.reject(unavailableError),
|
|
450
|
+
setVisible: () => Promise.reject(unavailableError)
|
|
451
|
+
};
|
|
452
|
+
return unavailable;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// Wires the JS-side bridge to the native WKWebView singleton owned by Swift.
|
|
457
|
+
// This is intentionally not a React component: the embedded webview lives
|
|
458
|
+
// outside the React tree (in a dedicated UIWindow on iOS), so its setup runs
|
|
459
|
+
// once when the SDK is initialised — there's nothing to mount or unmount.
|
|
460
|
+
//
|
|
461
|
+
// Returns a teardown that removes every JS-side subscription. The native
|
|
462
|
+
// singleton is intentionally NOT destroyed — re-running setup with a fresh
|
|
463
|
+
// `core` (e.g. on client recreation with a new environmentId) just reloads
|
|
464
|
+
// the existing webview via setUrl.
|
|
465
|
+
const setupEmbeddedWebView = ({
|
|
466
|
+
webviewUrl,
|
|
467
|
+
core,
|
|
468
|
+
webviewDebuggingEnabled: _webviewDebuggingEnabled = false
|
|
469
|
+
}) => {
|
|
470
|
+
const native = getEmbeddedWebView();
|
|
471
|
+
const builtUrl = assignStartTimeToUrl(assignEnvironmentIdToUrl(webviewUrl, core.environmentId));
|
|
472
|
+
const visibilityChannel = createRequestChannel(core.messageTransport);
|
|
473
|
+
const removeVisibilityHandler = visibilityChannel.handle('setVisibility', visible => {
|
|
474
|
+
native.setVisible(visible).catch(err => {
|
|
475
|
+
logger.warn('EmbeddedWebView.setVisible failed', err);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
const handleHostMessage = message => {
|
|
479
|
+
if (message.origin !== 'host') return;
|
|
480
|
+
native.postMessage(JSON.stringify(message, messageTransportDataJsonReplacer)).catch(err => {
|
|
481
|
+
logger.warn('EmbeddedWebView.postMessage failed', err);
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
core.messageTransport.on(handleHostMessage);
|
|
485
|
+
const onMessageSub = native.addListener('onMessage', event => {
|
|
486
|
+
let parsed = null;
|
|
487
|
+
try {
|
|
488
|
+
parsed = JSON.parse(event.message, messageTransportDataJsonReviver);
|
|
489
|
+
} catch (_a) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const message = parseMessageTransportData(parsed);
|
|
493
|
+
if (!message) return;
|
|
494
|
+
if (message.origin === 'webview') {
|
|
495
|
+
core.messageTransport.emit(message);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
const onShouldStartLoadSub = native.addListener('onShouldStartLoad', event => {
|
|
499
|
+
const allow = shouldAllowNavigation({
|
|
500
|
+
isTopFrame: event.isTopFrame,
|
|
501
|
+
url: event.url
|
|
502
|
+
}, builtUrl);
|
|
503
|
+
native.respondToShouldStartLoad(event.id, allow).catch(err => {
|
|
504
|
+
logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
const onLoadErrorSub = native.addListener('onLoadError', event => {
|
|
508
|
+
logger.warn('EmbeddedWebView load error', event);
|
|
509
|
+
logger.instrument('Embedded webview load error', {
|
|
510
|
+
environmentId: core.environmentId,
|
|
511
|
+
errorCode: event.code,
|
|
512
|
+
errorDescription: event.description,
|
|
513
|
+
errorDomain: event.domain,
|
|
514
|
+
failedUrl: event.url,
|
|
515
|
+
hostSdkSessionId: core.hostSdkSessionId,
|
|
516
|
+
isProvisional: event.isProvisional,
|
|
517
|
+
key: 'webview.embedded.load_error',
|
|
518
|
+
time: 0,
|
|
519
|
+
webviewUrl: builtUrl.toString()
|
|
520
|
+
});
|
|
521
|
+
core.initialization.error = new WebViewFailedToLoadError();
|
|
522
|
+
});
|
|
523
|
+
native.setDebuggingEnabled(_webviewDebuggingEnabled).catch(err => {
|
|
524
|
+
logger.warn('EmbeddedWebView.setDebuggingEnabled failed', err);
|
|
525
|
+
});
|
|
526
|
+
native.setUrl(builtUrl.toString()).catch(err => {
|
|
527
|
+
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
528
|
+
});
|
|
529
|
+
return () => {
|
|
530
|
+
removeVisibilityHandler();
|
|
531
|
+
core.messageTransport.off(handleHostMessage);
|
|
532
|
+
onMessageSub.remove();
|
|
533
|
+
onShouldStartLoadSub.remove();
|
|
534
|
+
onLoadErrorSub.remove();
|
|
535
|
+
};
|
|
536
|
+
};
|
|
537
|
+
|
|
439
538
|
/******************************************************************************
|
|
440
539
|
Copyright (c) Microsoft Corporation.
|
|
441
540
|
|
|
@@ -737,10 +836,17 @@ const setupKeychainHandler = core => {
|
|
|
737
836
|
};
|
|
738
837
|
|
|
739
838
|
const defaultWebviewUrl = `https://webview.dynamicauth.com/${version}`;
|
|
839
|
+
// The native webview singleton outlives the JS bridge, so a re-invocation
|
|
840
|
+
// of the extension factory (e.g. when the consumer recreates the client
|
|
841
|
+
// with a new environmentId) would otherwise stack JS-side listeners on top
|
|
842
|
+
// of the prior ones. We retain the previous teardown at module scope and
|
|
843
|
+
// run it before wiring the next bridge.
|
|
844
|
+
let previousEmbeddedWebViewTeardown = null;
|
|
740
845
|
const ReactNativeExtension = ({
|
|
741
846
|
webviewUrl: _webviewUrl = defaultWebviewUrl,
|
|
742
847
|
webviewDebuggingEnabled,
|
|
743
|
-
appOrigin
|
|
848
|
+
appOrigin,
|
|
849
|
+
embeddedWebView: _embeddedWebView = false
|
|
744
850
|
} = {}) => (_, core) => {
|
|
745
851
|
const isPlatformSupportedByWebView = Platform.OS === 'android' || Platform.OS === 'ios';
|
|
746
852
|
/**
|
|
@@ -760,6 +866,36 @@ const ReactNativeExtension = ({
|
|
|
760
866
|
setupPlatformHandler(core);
|
|
761
867
|
setupStorageHandler(core);
|
|
762
868
|
setupKeychainHandler(core);
|
|
869
|
+
// Native overlay-webview path supported on iOS (WKWebView) and Android
|
|
870
|
+
// (android.webkit.WebView). Other platforms fall back to react-native-webview.
|
|
871
|
+
const useNativeEmbeddedWebView = _embeddedWebView && (Platform.OS === 'ios' || Platform.OS === 'android');
|
|
872
|
+
if (useNativeEmbeddedWebView) {
|
|
873
|
+
// The native WKWebView lives in its own UIWindow, outside the React
|
|
874
|
+
// tree. Wire the JS bridge once at extension construction and hand the
|
|
875
|
+
// consumer a no-op component so its render never touches the webview.
|
|
876
|
+
if (previousEmbeddedWebViewTeardown) {
|
|
877
|
+
previousEmbeddedWebViewTeardown();
|
|
878
|
+
previousEmbeddedWebViewTeardown = null;
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
previousEmbeddedWebViewTeardown = setupEmbeddedWebView({
|
|
882
|
+
core,
|
|
883
|
+
webviewDebuggingEnabled,
|
|
884
|
+
webviewUrl: _webviewUrl
|
|
885
|
+
});
|
|
886
|
+
} catch (err) {
|
|
887
|
+
// Setup failed — leave the slot empty so the next invocation isn't
|
|
888
|
+
// wedged with a stale teardown reference. The error itself is left
|
|
889
|
+
// to bubble: the consumer needs to see that the bridge isn't wired.
|
|
890
|
+
previousEmbeddedWebViewTeardown = null;
|
|
891
|
+
throw err;
|
|
892
|
+
}
|
|
893
|
+
return {
|
|
894
|
+
reactNative: {
|
|
895
|
+
WebView: () => null
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
}
|
|
763
899
|
return {
|
|
764
900
|
reactNative: {
|
|
765
901
|
WebView: createWebView({
|