@dynamic-labs/react-native-extension 4.80.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.
Files changed (27) hide show
  1. package/android/EmbeddedWebViewController.kt +476 -0
  2. package/android/EmbeddedWebViewModule.kt +55 -0
  3. package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  4. package/android/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  5. package/android/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  6. package/android/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  7. package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  8. package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  9. package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  10. package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  11. package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  12. package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  13. package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  14. package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  15. package/expo-module.config.json +5 -2
  16. package/index.cjs +172 -39
  17. package/index.js +172 -39
  18. package/ios/EmbeddedWebViewController.swift +426 -0
  19. package/ios/EmbeddedWebViewModule.swift +62 -0
  20. package/ios/Keychain.podspec +2 -2
  21. package/package.json +6 -6
  22. package/src/ReactNativeExtension/ReactNativeExtension.d.ts +24 -1
  23. package/src/components/WebView/EmbeddedWebView/EmbeddedWebView.d.ts +7 -0
  24. package/src/components/WebView/EmbeddedWebView/index.d.ts +1 -0
  25. package/src/components/WebView/utils/shouldAllowNavigation/index.d.ts +1 -0
  26. package/src/components/WebView/utils/shouldAllowNavigation/shouldAllowNavigation.d.ts +5 -0
  27. 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.80.0";
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 turnkeyOrigins = ['https://recovery.turnkey.com', 'https://export.turnkey.com'];
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
- 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
- }
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.80.0";
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 turnkeyOrigins = ['https://recovery.turnkey.com', 'https://export.turnkey.com'];
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
- 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
- }
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({