@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.
Files changed (35) hide show
  1. package/android/EmbeddedWebViewController.kt +476 -0
  2. package/android/EmbeddedWebViewModule.kt +55 -0
  3. package/android/KeyStoreKeyManager.kt +7 -1
  4. package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  5. package/android/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  6. package/android/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  7. package/android/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  8. package/android/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  9. package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  10. package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  11. package/android/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  12. package/android/keychain/KeyStoreKeyManager.kt +7 -1
  13. package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  14. package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  15. package/android/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  16. package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  17. package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  18. package/android/src/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  19. package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  20. package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  21. package/android/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  22. package/expo-module.config.json +5 -2
  23. package/index.cjs +178 -42
  24. package/index.js +178 -42
  25. package/ios/EmbeddedWebViewController.swift +426 -0
  26. package/ios/EmbeddedWebViewModule.swift +62 -0
  27. package/ios/Keychain.podspec +2 -2
  28. package/package.json +6 -6
  29. package/src/ReactNativeExtension/ReactNativeExtension.d.ts +24 -1
  30. package/src/components/WebView/EmbeddedWebView/EmbeddedWebView.d.ts +7 -0
  31. package/src/components/WebView/EmbeddedWebView/index.d.ts +1 -0
  32. package/src/components/WebView/utils/shouldAllowNavigation/index.d.ts +1 -0
  33. package/src/components/WebView/utils/shouldAllowNavigation/shouldAllowNavigation.d.ts +5 -0
  34. package/src/errors/WebViewFailedToLoadError.d.ts +84 -1
  35. 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.81.0";
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 turnkeyOrigins = ['https://recovery.turnkey.com', 'https://export.turnkey.com'];
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 = 8000,
312
- loadingTimeout: _loadingTimeout = 10000
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
- const requestUrl = getUrl(request.url);
415
- // Invalid URL, never navigate to it
416
- if (!requestUrl) return false;
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.81.0";
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 turnkeyOrigins = ['https://recovery.turnkey.com', 'https://export.turnkey.com'];
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 = 8000,
290
- loadingTimeout: _loadingTimeout = 10000
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
- const requestUrl = getUrl(request.url);
393
- // Invalid URL, never navigate to it
394
- if (!requestUrl) return false;
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({