@dynamic-labs/react-native-extension 4.81.0 → 4.83.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 +476 -0
- package/android/EmbeddedWebViewModule.kt +55 -0
- package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- 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/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- 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/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- package/expo-module.config.json +5 -2
- package/index.cjs +172 -39
- package/index.js +172 -39
- 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/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.0";
|
|
38
38
|
|
|
39
39
|
function _extends() {
|
|
40
40
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -289,20 +289,39 @@ const assignStartTimeToUrl = url => {
|
|
|
289
289
|
};
|
|
290
290
|
|
|
291
291
|
const waasOrigins = ['https://app.dynamic-preprod.xyz', 'https://app.dynamicauth.com'];
|
|
292
|
+
const turnkeyOrigins = ['https://recovery.turnkey.com', 'https://export.turnkey.com'];
|
|
292
293
|
const isNonProductionBuild = () => process.env['NODE_ENV'] !== 'production';
|
|
293
294
|
const isAllowedWaasOrigin = origin => {
|
|
294
295
|
if (waasOrigins.includes(origin)) {
|
|
295
296
|
return true;
|
|
296
297
|
}
|
|
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
298
|
if (isNonProductionBuild() && /^http:\/\/localhost:\d+$/.test(origin)) {
|
|
301
299
|
return true;
|
|
302
300
|
}
|
|
303
301
|
return false;
|
|
304
302
|
};
|
|
305
|
-
const
|
|
303
|
+
const parseUrl = url => {
|
|
304
|
+
try {
|
|
305
|
+
return new URL(url);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
logger.error('Failed to get origin from url', error);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
const shouldAllowNavigation = (request, webViewUrl) => {
|
|
312
|
+
const requestUrl = parseUrl(request.url);
|
|
313
|
+
if (!requestUrl) return false;
|
|
314
|
+
// Sub-frame (iframe) requests are controlled by the trusted web app
|
|
315
|
+
// content — allow them through without restriction.
|
|
316
|
+
if (!request.isTopFrame) return true;
|
|
317
|
+
if (webViewUrl.origin === requestUrl.origin) return true;
|
|
318
|
+
if (requestUrl.pathname.startsWith('/waas-v1') && isAllowedWaasOrigin(requestUrl.origin)) {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
if (turnkeyOrigins.includes(requestUrl.origin)) return true;
|
|
322
|
+
return false;
|
|
323
|
+
};
|
|
324
|
+
|
|
306
325
|
const WebView = ({
|
|
307
326
|
webviewUrl: initialWebViewUrl,
|
|
308
327
|
core,
|
|
@@ -410,40 +429,12 @@ const WebView = ({
|
|
|
410
429
|
onRenderProcessGone: blockAndReloadWebViewOsKill,
|
|
411
430
|
onError: onWebViewLoadError,
|
|
412
431
|
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
|
-
}
|
|
432
|
+
onShouldStartLoadWithRequest: request => shouldAllowNavigation({
|
|
433
|
+
isTopFrame: request.isTopFrame,
|
|
434
|
+
url: request.url
|
|
435
|
+
}, webViewUrl)
|
|
437
436
|
});
|
|
438
437
|
};
|
|
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
438
|
const createWebView = internalProps => {
|
|
448
439
|
const WebViewWrapper = props => /*#__PURE__*/jsxRuntime.jsx(WebView, _extends({}, internalProps, props));
|
|
449
440
|
return WebViewWrapper;
|
|
@@ -458,6 +449,111 @@ For more information, please refer to the documentation:
|
|
|
458
449
|
https://docs.dynamic.xyz/react-native/react-native-extension
|
|
459
450
|
`;
|
|
460
451
|
|
|
452
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
453
|
+
const NATIVE_MODULE_NAME = 'EmbeddedWebView';
|
|
454
|
+
const getEmbeddedWebView = () => {
|
|
455
|
+
try {
|
|
456
|
+
return expoModulesCore.requireNativeModule(NATIVE_MODULE_NAME);
|
|
457
|
+
} catch (_a) {
|
|
458
|
+
logger.warn('Could not get EmbeddedWebView native module, using unavailable fallback');
|
|
459
|
+
const unavailableError = new Error('EmbeddedWebView is not available');
|
|
460
|
+
const unavailable = {
|
|
461
|
+
addListener: () => ({
|
|
462
|
+
remove: () => undefined
|
|
463
|
+
}),
|
|
464
|
+
destroy: () => Promise.reject(unavailableError),
|
|
465
|
+
postMessage: () => Promise.reject(unavailableError),
|
|
466
|
+
respondToShouldStartLoad: () => Promise.reject(unavailableError),
|
|
467
|
+
setDebuggingEnabled: () => Promise.reject(unavailableError),
|
|
468
|
+
setUrl: () => Promise.reject(unavailableError),
|
|
469
|
+
setVisible: () => Promise.reject(unavailableError)
|
|
470
|
+
};
|
|
471
|
+
return unavailable;
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// Wires the JS-side bridge to the native WKWebView singleton owned by Swift.
|
|
476
|
+
// This is intentionally not a React component: the embedded webview lives
|
|
477
|
+
// outside the React tree (in a dedicated UIWindow on iOS), so its setup runs
|
|
478
|
+
// once when the SDK is initialised — there's nothing to mount or unmount.
|
|
479
|
+
//
|
|
480
|
+
// Returns a teardown that removes every JS-side subscription. The native
|
|
481
|
+
// singleton is intentionally NOT destroyed — re-running setup with a fresh
|
|
482
|
+
// `core` (e.g. on client recreation with a new environmentId) just reloads
|
|
483
|
+
// the existing webview via setUrl.
|
|
484
|
+
const setupEmbeddedWebView = ({
|
|
485
|
+
webviewUrl,
|
|
486
|
+
core,
|
|
487
|
+
webviewDebuggingEnabled: _webviewDebuggingEnabled = false
|
|
488
|
+
}) => {
|
|
489
|
+
const native = getEmbeddedWebView();
|
|
490
|
+
const builtUrl = assignStartTimeToUrl(assignEnvironmentIdToUrl(webviewUrl, core.environmentId));
|
|
491
|
+
const visibilityChannel = messageTransport.createRequestChannel(core.messageTransport);
|
|
492
|
+
const removeVisibilityHandler = visibilityChannel.handle('setVisibility', visible => {
|
|
493
|
+
native.setVisible(visible).catch(err => {
|
|
494
|
+
logger.warn('EmbeddedWebView.setVisible failed', err);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
const handleHostMessage = message => {
|
|
498
|
+
if (message.origin !== 'host') return;
|
|
499
|
+
native.postMessage(JSON.stringify(message, messageTransport.messageTransportDataJsonReplacer)).catch(err => {
|
|
500
|
+
logger.warn('EmbeddedWebView.postMessage failed', err);
|
|
501
|
+
});
|
|
502
|
+
};
|
|
503
|
+
core.messageTransport.on(handleHostMessage);
|
|
504
|
+
const onMessageSub = native.addListener('onMessage', event => {
|
|
505
|
+
let parsed = null;
|
|
506
|
+
try {
|
|
507
|
+
parsed = JSON.parse(event.message, messageTransport.messageTransportDataJsonReviver);
|
|
508
|
+
} catch (_a) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const message = messageTransport.parseMessageTransportData(parsed);
|
|
512
|
+
if (!message) return;
|
|
513
|
+
if (message.origin === 'webview') {
|
|
514
|
+
core.messageTransport.emit(message);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
const onShouldStartLoadSub = native.addListener('onShouldStartLoad', event => {
|
|
518
|
+
const allow = shouldAllowNavigation({
|
|
519
|
+
isTopFrame: event.isTopFrame,
|
|
520
|
+
url: event.url
|
|
521
|
+
}, builtUrl);
|
|
522
|
+
native.respondToShouldStartLoad(event.id, allow).catch(err => {
|
|
523
|
+
logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
const onLoadErrorSub = native.addListener('onLoadError', event => {
|
|
527
|
+
logger.warn('EmbeddedWebView load error', event);
|
|
528
|
+
logger.instrument('Embedded webview load error', {
|
|
529
|
+
environmentId: core.environmentId,
|
|
530
|
+
errorCode: event.code,
|
|
531
|
+
errorDescription: event.description,
|
|
532
|
+
errorDomain: event.domain,
|
|
533
|
+
failedUrl: event.url,
|
|
534
|
+
hostSdkSessionId: core.hostSdkSessionId,
|
|
535
|
+
isProvisional: event.isProvisional,
|
|
536
|
+
key: 'webview.embedded.load_error',
|
|
537
|
+
time: 0,
|
|
538
|
+
webviewUrl: builtUrl.toString()
|
|
539
|
+
});
|
|
540
|
+
core.initialization.error = new WebViewFailedToLoadError();
|
|
541
|
+
});
|
|
542
|
+
native.setDebuggingEnabled(_webviewDebuggingEnabled).catch(err => {
|
|
543
|
+
logger.warn('EmbeddedWebView.setDebuggingEnabled failed', err);
|
|
544
|
+
});
|
|
545
|
+
native.setUrl(builtUrl.toString()).catch(err => {
|
|
546
|
+
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
547
|
+
});
|
|
548
|
+
return () => {
|
|
549
|
+
removeVisibilityHandler();
|
|
550
|
+
core.messageTransport.off(handleHostMessage);
|
|
551
|
+
onMessageSub.remove();
|
|
552
|
+
onShouldStartLoadSub.remove();
|
|
553
|
+
onLoadErrorSub.remove();
|
|
554
|
+
};
|
|
555
|
+
};
|
|
556
|
+
|
|
461
557
|
/******************************************************************************
|
|
462
558
|
Copyright (c) Microsoft Corporation.
|
|
463
559
|
|
|
@@ -759,10 +855,17 @@ const setupKeychainHandler = core => {
|
|
|
759
855
|
};
|
|
760
856
|
|
|
761
857
|
const defaultWebviewUrl = `https://webview.dynamicauth.com/${version}`;
|
|
858
|
+
// The native webview singleton outlives the JS bridge, so a re-invocation
|
|
859
|
+
// of the extension factory (e.g. when the consumer recreates the client
|
|
860
|
+
// with a new environmentId) would otherwise stack JS-side listeners on top
|
|
861
|
+
// of the prior ones. We retain the previous teardown at module scope and
|
|
862
|
+
// run it before wiring the next bridge.
|
|
863
|
+
let previousEmbeddedWebViewTeardown = null;
|
|
762
864
|
const ReactNativeExtension = ({
|
|
763
865
|
webviewUrl: _webviewUrl = defaultWebviewUrl,
|
|
764
866
|
webviewDebuggingEnabled,
|
|
765
|
-
appOrigin
|
|
867
|
+
appOrigin,
|
|
868
|
+
embeddedWebView: _embeddedWebView = false
|
|
766
869
|
} = {}) => (_, core) => {
|
|
767
870
|
const isPlatformSupportedByWebView = reactNative.Platform.OS === 'android' || reactNative.Platform.OS === 'ios';
|
|
768
871
|
/**
|
|
@@ -782,6 +885,36 @@ const ReactNativeExtension = ({
|
|
|
782
885
|
setupPlatformHandler(core);
|
|
783
886
|
setupStorageHandler(core);
|
|
784
887
|
setupKeychainHandler(core);
|
|
888
|
+
// Native overlay-webview path supported on iOS (WKWebView) and Android
|
|
889
|
+
// (android.webkit.WebView). Other platforms fall back to react-native-webview.
|
|
890
|
+
const useNativeEmbeddedWebView = _embeddedWebView && (reactNative.Platform.OS === 'ios' || reactNative.Platform.OS === 'android');
|
|
891
|
+
if (useNativeEmbeddedWebView) {
|
|
892
|
+
// The native WKWebView lives in its own UIWindow, outside the React
|
|
893
|
+
// tree. Wire the JS bridge once at extension construction and hand the
|
|
894
|
+
// consumer a no-op component so its render never touches the webview.
|
|
895
|
+
if (previousEmbeddedWebViewTeardown) {
|
|
896
|
+
previousEmbeddedWebViewTeardown();
|
|
897
|
+
previousEmbeddedWebViewTeardown = null;
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
previousEmbeddedWebViewTeardown = setupEmbeddedWebView({
|
|
901
|
+
core,
|
|
902
|
+
webviewDebuggingEnabled,
|
|
903
|
+
webviewUrl: _webviewUrl
|
|
904
|
+
});
|
|
905
|
+
} catch (err) {
|
|
906
|
+
// Setup failed — leave the slot empty so the next invocation isn't
|
|
907
|
+
// wedged with a stale teardown reference. The error itself is left
|
|
908
|
+
// to bubble: the consumer needs to see that the bridge isn't wired.
|
|
909
|
+
previousEmbeddedWebViewTeardown = null;
|
|
910
|
+
throw err;
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
reactNative: {
|
|
914
|
+
WebView: () => null
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
785
918
|
return {
|
|
786
919
|
reactNative: {
|
|
787
920
|
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.0";
|
|
16
16
|
|
|
17
17
|
function _extends() {
|
|
18
18
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -267,20 +267,39 @@ const assignStartTimeToUrl = url => {
|
|
|
267
267
|
};
|
|
268
268
|
|
|
269
269
|
const waasOrigins = ['https://app.dynamic-preprod.xyz', 'https://app.dynamicauth.com'];
|
|
270
|
+
const turnkeyOrigins = ['https://recovery.turnkey.com', 'https://export.turnkey.com'];
|
|
270
271
|
const isNonProductionBuild = () => process.env['NODE_ENV'] !== 'production';
|
|
271
272
|
const isAllowedWaasOrigin = origin => {
|
|
272
273
|
if (waasOrigins.includes(origin)) {
|
|
273
274
|
return true;
|
|
274
275
|
}
|
|
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
276
|
if (isNonProductionBuild() && /^http:\/\/localhost:\d+$/.test(origin)) {
|
|
279
277
|
return true;
|
|
280
278
|
}
|
|
281
279
|
return false;
|
|
282
280
|
};
|
|
283
|
-
const
|
|
281
|
+
const parseUrl = url => {
|
|
282
|
+
try {
|
|
283
|
+
return new URL(url);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
logger.error('Failed to get origin from url', error);
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const shouldAllowNavigation = (request, webViewUrl) => {
|
|
290
|
+
const requestUrl = parseUrl(request.url);
|
|
291
|
+
if (!requestUrl) return false;
|
|
292
|
+
// Sub-frame (iframe) requests are controlled by the trusted web app
|
|
293
|
+
// content — allow them through without restriction.
|
|
294
|
+
if (!request.isTopFrame) return true;
|
|
295
|
+
if (webViewUrl.origin === requestUrl.origin) return true;
|
|
296
|
+
if (requestUrl.pathname.startsWith('/waas-v1') && isAllowedWaasOrigin(requestUrl.origin)) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
if (turnkeyOrigins.includes(requestUrl.origin)) return true;
|
|
300
|
+
return false;
|
|
301
|
+
};
|
|
302
|
+
|
|
284
303
|
const WebView = ({
|
|
285
304
|
webviewUrl: initialWebViewUrl,
|
|
286
305
|
core,
|
|
@@ -388,40 +407,12 @@ const WebView = ({
|
|
|
388
407
|
onRenderProcessGone: blockAndReloadWebViewOsKill,
|
|
389
408
|
onError: onWebViewLoadError,
|
|
390
409
|
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
|
-
}
|
|
410
|
+
onShouldStartLoadWithRequest: request => shouldAllowNavigation({
|
|
411
|
+
isTopFrame: request.isTopFrame,
|
|
412
|
+
url: request.url
|
|
413
|
+
}, webViewUrl)
|
|
415
414
|
});
|
|
416
415
|
};
|
|
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
416
|
const createWebView = internalProps => {
|
|
426
417
|
const WebViewWrapper = props => /*#__PURE__*/jsx(WebView, _extends({}, internalProps, props));
|
|
427
418
|
return WebViewWrapper;
|
|
@@ -436,6 +427,111 @@ For more information, please refer to the documentation:
|
|
|
436
427
|
https://docs.dynamic.xyz/react-native/react-native-extension
|
|
437
428
|
`;
|
|
438
429
|
|
|
430
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
431
|
+
const NATIVE_MODULE_NAME = 'EmbeddedWebView';
|
|
432
|
+
const getEmbeddedWebView = () => {
|
|
433
|
+
try {
|
|
434
|
+
return requireNativeModule(NATIVE_MODULE_NAME);
|
|
435
|
+
} catch (_a) {
|
|
436
|
+
logger.warn('Could not get EmbeddedWebView native module, using unavailable fallback');
|
|
437
|
+
const unavailableError = new Error('EmbeddedWebView is not available');
|
|
438
|
+
const unavailable = {
|
|
439
|
+
addListener: () => ({
|
|
440
|
+
remove: () => undefined
|
|
441
|
+
}),
|
|
442
|
+
destroy: () => Promise.reject(unavailableError),
|
|
443
|
+
postMessage: () => Promise.reject(unavailableError),
|
|
444
|
+
respondToShouldStartLoad: () => Promise.reject(unavailableError),
|
|
445
|
+
setDebuggingEnabled: () => Promise.reject(unavailableError),
|
|
446
|
+
setUrl: () => Promise.reject(unavailableError),
|
|
447
|
+
setVisible: () => Promise.reject(unavailableError)
|
|
448
|
+
};
|
|
449
|
+
return unavailable;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Wires the JS-side bridge to the native WKWebView singleton owned by Swift.
|
|
454
|
+
// This is intentionally not a React component: the embedded webview lives
|
|
455
|
+
// outside the React tree (in a dedicated UIWindow on iOS), so its setup runs
|
|
456
|
+
// once when the SDK is initialised — there's nothing to mount or unmount.
|
|
457
|
+
//
|
|
458
|
+
// Returns a teardown that removes every JS-side subscription. The native
|
|
459
|
+
// singleton is intentionally NOT destroyed — re-running setup with a fresh
|
|
460
|
+
// `core` (e.g. on client recreation with a new environmentId) just reloads
|
|
461
|
+
// the existing webview via setUrl.
|
|
462
|
+
const setupEmbeddedWebView = ({
|
|
463
|
+
webviewUrl,
|
|
464
|
+
core,
|
|
465
|
+
webviewDebuggingEnabled: _webviewDebuggingEnabled = false
|
|
466
|
+
}) => {
|
|
467
|
+
const native = getEmbeddedWebView();
|
|
468
|
+
const builtUrl = assignStartTimeToUrl(assignEnvironmentIdToUrl(webviewUrl, core.environmentId));
|
|
469
|
+
const visibilityChannel = createRequestChannel(core.messageTransport);
|
|
470
|
+
const removeVisibilityHandler = visibilityChannel.handle('setVisibility', visible => {
|
|
471
|
+
native.setVisible(visible).catch(err => {
|
|
472
|
+
logger.warn('EmbeddedWebView.setVisible failed', err);
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
const handleHostMessage = message => {
|
|
476
|
+
if (message.origin !== 'host') return;
|
|
477
|
+
native.postMessage(JSON.stringify(message, messageTransportDataJsonReplacer)).catch(err => {
|
|
478
|
+
logger.warn('EmbeddedWebView.postMessage failed', err);
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
core.messageTransport.on(handleHostMessage);
|
|
482
|
+
const onMessageSub = native.addListener('onMessage', event => {
|
|
483
|
+
let parsed = null;
|
|
484
|
+
try {
|
|
485
|
+
parsed = JSON.parse(event.message, messageTransportDataJsonReviver);
|
|
486
|
+
} catch (_a) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const message = parseMessageTransportData(parsed);
|
|
490
|
+
if (!message) return;
|
|
491
|
+
if (message.origin === 'webview') {
|
|
492
|
+
core.messageTransport.emit(message);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
const onShouldStartLoadSub = native.addListener('onShouldStartLoad', event => {
|
|
496
|
+
const allow = shouldAllowNavigation({
|
|
497
|
+
isTopFrame: event.isTopFrame,
|
|
498
|
+
url: event.url
|
|
499
|
+
}, builtUrl);
|
|
500
|
+
native.respondToShouldStartLoad(event.id, allow).catch(err => {
|
|
501
|
+
logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
const onLoadErrorSub = native.addListener('onLoadError', event => {
|
|
505
|
+
logger.warn('EmbeddedWebView load error', event);
|
|
506
|
+
logger.instrument('Embedded webview load error', {
|
|
507
|
+
environmentId: core.environmentId,
|
|
508
|
+
errorCode: event.code,
|
|
509
|
+
errorDescription: event.description,
|
|
510
|
+
errorDomain: event.domain,
|
|
511
|
+
failedUrl: event.url,
|
|
512
|
+
hostSdkSessionId: core.hostSdkSessionId,
|
|
513
|
+
isProvisional: event.isProvisional,
|
|
514
|
+
key: 'webview.embedded.load_error',
|
|
515
|
+
time: 0,
|
|
516
|
+
webviewUrl: builtUrl.toString()
|
|
517
|
+
});
|
|
518
|
+
core.initialization.error = new WebViewFailedToLoadError();
|
|
519
|
+
});
|
|
520
|
+
native.setDebuggingEnabled(_webviewDebuggingEnabled).catch(err => {
|
|
521
|
+
logger.warn('EmbeddedWebView.setDebuggingEnabled failed', err);
|
|
522
|
+
});
|
|
523
|
+
native.setUrl(builtUrl.toString()).catch(err => {
|
|
524
|
+
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
525
|
+
});
|
|
526
|
+
return () => {
|
|
527
|
+
removeVisibilityHandler();
|
|
528
|
+
core.messageTransport.off(handleHostMessage);
|
|
529
|
+
onMessageSub.remove();
|
|
530
|
+
onShouldStartLoadSub.remove();
|
|
531
|
+
onLoadErrorSub.remove();
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
|
|
439
535
|
/******************************************************************************
|
|
440
536
|
Copyright (c) Microsoft Corporation.
|
|
441
537
|
|
|
@@ -737,10 +833,17 @@ const setupKeychainHandler = core => {
|
|
|
737
833
|
};
|
|
738
834
|
|
|
739
835
|
const defaultWebviewUrl = `https://webview.dynamicauth.com/${version}`;
|
|
836
|
+
// The native webview singleton outlives the JS bridge, so a re-invocation
|
|
837
|
+
// of the extension factory (e.g. when the consumer recreates the client
|
|
838
|
+
// with a new environmentId) would otherwise stack JS-side listeners on top
|
|
839
|
+
// of the prior ones. We retain the previous teardown at module scope and
|
|
840
|
+
// run it before wiring the next bridge.
|
|
841
|
+
let previousEmbeddedWebViewTeardown = null;
|
|
740
842
|
const ReactNativeExtension = ({
|
|
741
843
|
webviewUrl: _webviewUrl = defaultWebviewUrl,
|
|
742
844
|
webviewDebuggingEnabled,
|
|
743
|
-
appOrigin
|
|
845
|
+
appOrigin,
|
|
846
|
+
embeddedWebView: _embeddedWebView = false
|
|
744
847
|
} = {}) => (_, core) => {
|
|
745
848
|
const isPlatformSupportedByWebView = Platform.OS === 'android' || Platform.OS === 'ios';
|
|
746
849
|
/**
|
|
@@ -760,6 +863,36 @@ const ReactNativeExtension = ({
|
|
|
760
863
|
setupPlatformHandler(core);
|
|
761
864
|
setupStorageHandler(core);
|
|
762
865
|
setupKeychainHandler(core);
|
|
866
|
+
// Native overlay-webview path supported on iOS (WKWebView) and Android
|
|
867
|
+
// (android.webkit.WebView). Other platforms fall back to react-native-webview.
|
|
868
|
+
const useNativeEmbeddedWebView = _embeddedWebView && (Platform.OS === 'ios' || Platform.OS === 'android');
|
|
869
|
+
if (useNativeEmbeddedWebView) {
|
|
870
|
+
// The native WKWebView lives in its own UIWindow, outside the React
|
|
871
|
+
// tree. Wire the JS bridge once at extension construction and hand the
|
|
872
|
+
// consumer a no-op component so its render never touches the webview.
|
|
873
|
+
if (previousEmbeddedWebViewTeardown) {
|
|
874
|
+
previousEmbeddedWebViewTeardown();
|
|
875
|
+
previousEmbeddedWebViewTeardown = null;
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
previousEmbeddedWebViewTeardown = setupEmbeddedWebView({
|
|
879
|
+
core,
|
|
880
|
+
webviewDebuggingEnabled,
|
|
881
|
+
webviewUrl: _webviewUrl
|
|
882
|
+
});
|
|
883
|
+
} catch (err) {
|
|
884
|
+
// Setup failed — leave the slot empty so the next invocation isn't
|
|
885
|
+
// wedged with a stale teardown reference. The error itself is left
|
|
886
|
+
// to bubble: the consumer needs to see that the bridge isn't wired.
|
|
887
|
+
previousEmbeddedWebViewTeardown = null;
|
|
888
|
+
throw err;
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
reactNative: {
|
|
892
|
+
WebView: () => null
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
}
|
|
763
896
|
return {
|
|
764
897
|
reactNative: {
|
|
765
898
|
WebView: createWebView({
|