From 59c46cb2c1b0c0821dcbb280d114969175e73b52 Mon Sep 17 00:00:00 2001 From: sherzod-bakhodirov Date: Mon, 23 Mar 2026 20:40:59 +0500 Subject: [PATCH 1/5] feat: implement mechanism to open new window via events --- .../@magic-sdk/react-native-expo/package.json | 1 + .../react-native-expo/src/lib/links.ts | 15 ++++++++++++++ .../src/react-native-webview-controller.tsx | 20 ++++++++++++++++++- .../types/src/core/message-types.ts | 1 + yarn.lock | 1 + 5 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 packages/@magic-sdk/react-native-expo/src/lib/links.ts diff --git a/packages/@magic-sdk/react-native-expo/package.json b/packages/@magic-sdk/react-native-expo/package.json index f5ddc2959..1d2d16ec2 100644 --- a/packages/@magic-sdk/react-native-expo/package.json +++ b/packages/@magic-sdk/react-native-expo/package.json @@ -23,6 +23,7 @@ "@react-native-async-storage/async-storage": "^1.15.5", "buffer": "~5.6.0", "expo-application": "^7.0.8", + "expo-web-browser": "14.0.2", "localforage": "^1.7.4", "lodash": "^4.17.19", "process": "~0.11.10", diff --git a/packages/@magic-sdk/react-native-expo/src/lib/links.ts b/packages/@magic-sdk/react-native-expo/src/lib/links.ts new file mode 100644 index 000000000..e3811f2c6 --- /dev/null +++ b/packages/@magic-sdk/react-native-expo/src/lib/links.ts @@ -0,0 +1,15 @@ +import { Linking } from 'react-native'; +import * as WebBrowser from 'expo-web-browser'; + +export const openInBrowser = async (url: string) => { + const supported = await Linking.canOpenURL(url); + if (supported) { + await Linking.openURL(url); + } +}; + +export const openInApp = async (url: string) => { + await WebBrowser.openBrowserAsync(url, { + presentationStyle: WebBrowser.WebBrowserPresentationStyle.PAGE_SHEET, // iOS + }); +}; diff --git a/packages/@magic-sdk/react-native-expo/src/react-native-webview-controller.tsx b/packages/@magic-sdk/react-native-expo/src/react-native-webview-controller.tsx index 108394fee..7bb738725 100644 --- a/packages/@magic-sdk/react-native-expo/src/react-native-webview-controller.tsx +++ b/packages/@magic-sdk/react-native-expo/src/react-native-webview-controller.tsx @@ -3,13 +3,14 @@ import { Linking, StyleSheet, View } from 'react-native'; import { WebView } from 'react-native-webview'; import { SafeAreaView } from 'react-native-safe-area-context'; import { ViewController, createModalNotReadyError } from '@magic-sdk/provider'; -import { MagicMessageEvent } from '@magic-sdk/types'; +import { MagicIncomingWindowMessage, MagicMessageEvent } from '@magic-sdk/types'; import { isTypedArray } from 'lodash'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { EventRegister } from 'react-native-event-listeners'; /* global NodeJS */ import Global = NodeJS.Global; import { useInternetConnection } from './hooks'; +import { openInApp, openInBrowser } from './lib/links'; const MAGIC_PAYLOAD_FLAG_TYPED_ARRAY = 'MAGIC_PAYLOAD_FLAG_TYPED_ARRAY'; const OPEN_IN_DEVICE_BROWSER = 'open_in_device_browser'; @@ -91,6 +92,23 @@ export class ReactNativeWebViewController extends ViewController { this.isConnectedToInternet = isConnected; }, [isConnected]); + useEffect(() => { + const removeHandler = this.on(MagicIncomingWindowMessage.MAGIC_OPEN_MOBILE_URL, (event: MagicMessageEvent) => { + const url = event.data.response.result?.url; + const type = event.data.response.result?.type ?? 'drawer'; + + if (!url) return; + + if (type === 'drawer') { + openInApp(url); + } else { + openInBrowser(url); + } + }); + + return removeHandler; + }, []); + useEffect(() => { // reset lastMessage when webview is first mounted AsyncStorage.setItem(LAST_MESSAGE_TIME, ''); diff --git a/packages/@magic-sdk/types/src/core/message-types.ts b/packages/@magic-sdk/types/src/core/message-types.ts index a96c940f7..2e92c6937 100644 --- a/packages/@magic-sdk/types/src/core/message-types.ts +++ b/packages/@magic-sdk/types/src/core/message-types.ts @@ -12,6 +12,7 @@ export enum MagicIncomingWindowMessage { MAGIC_POPUP_RESPONSE = 'MAGIC_POPUP_RESPONSE', MAGIC_POPUP_OAUTH_VERIFY_RESPONSE = 'MAGIC_POPUP_OAUTH_VERIFY_RESPONSE', MAGIC_THIRD_PARTY_WALLET_REQUEST = 'MAGIC_THIRD_PARTY_WALLET_REQUEST', + MAGIC_OPEN_MOBILE_URL = 'MAGIC_OPEN_MOBILE_URL', } export enum MagicOutgoingWindowMessage { diff --git a/yarn.lock b/yarn.lock index 0e99866d0..9dff532f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3683,6 +3683,7 @@ __metadata: expo: ^54.0.30 expo-application: ^7.0.8 expo-modules-core: ^3.0.29 + expo-web-browser: 14.0.2 jest-expo: ~54.0.16 localforage: ^1.7.4 lodash: ^4.17.19 From 016b8fc284646ef7016e86e2c8467a6ff28c09fb Mon Sep 17 00:00:00 2001 From: sherzod-bakhodirov Date: Tue, 24 Mar 2026 22:24:58 +0500 Subject: [PATCH 2/5] feat: implement mechanism to open new window in rn bare --- .../@magic-sdk/react-native-bare/package.json | 1 + .../react-native-bare/src/lib/links.ts | 35 +++++++++++++++++++ .../src/react-native-webview-controller.tsx | 20 ++++++++++- .../react-native-expo/src/lib/links.ts | 12 +++++-- yarn.lock | 1 + 5 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 packages/@magic-sdk/react-native-bare/src/lib/links.ts diff --git a/packages/@magic-sdk/react-native-bare/package.json b/packages/@magic-sdk/react-native-bare/package.json index 266fb208d..4dcaf6d90 100644 --- a/packages/@magic-sdk/react-native-bare/package.json +++ b/packages/@magic-sdk/react-native-bare/package.json @@ -27,6 +27,7 @@ "lodash": "^4.17.19", "process": "~0.11.10", "react-native-event-listeners": "^1.0.7", + "react-native-inappbrowser-reborn": "^3.7.0", "react-native-uuid": "^2.0.3", "tslib": "^2.0.3", "whatwg-url": "~8.1.0" diff --git a/packages/@magic-sdk/react-native-bare/src/lib/links.ts b/packages/@magic-sdk/react-native-bare/src/lib/links.ts new file mode 100644 index 000000000..6ceb7fb35 --- /dev/null +++ b/packages/@magic-sdk/react-native-bare/src/lib/links.ts @@ -0,0 +1,35 @@ +import { Linking } from 'react-native'; +import { InAppBrowser } from 'react-native-inappbrowser-reborn'; + +export const openInBrowser = async (url: string) => { + const supported = await Linking.canOpenURL(url); + + if (supported) { + await Linking.openURL(url); + } else { + console.warn(`Cannot open URL: ${url}`); + } +}; + +export const openInApp = async (url: string) => { + const isInAppBrowserAvailable = await InAppBrowser.isAvailable(); + + if (!isInAppBrowserAvailable) { + await openInBrowser(url); + return; + } + + try { + await InAppBrowser.open(url, { + // iOS + dismissButtonStyle: 'done', + modalPresentationStyle: 'pageSheet', + + // Android + showTitle: true, + enableUrlBarHiding: true, + }); + } catch (e) { + await openInBrowser(url); + } +}; diff --git a/packages/@magic-sdk/react-native-bare/src/react-native-webview-controller.tsx b/packages/@magic-sdk/react-native-bare/src/react-native-webview-controller.tsx index 59ff62517..9b372a58e 100644 --- a/packages/@magic-sdk/react-native-bare/src/react-native-webview-controller.tsx +++ b/packages/@magic-sdk/react-native-bare/src/react-native-webview-controller.tsx @@ -3,7 +3,7 @@ import { Linking, StyleSheet, View } from 'react-native'; import { WebView } from 'react-native-webview'; import { SafeAreaView } from 'react-native-safe-area-context'; import { ViewController, createModalNotReadyError } from '@magic-sdk/provider'; -import { MagicMessageEvent } from '@magic-sdk/types'; +import { MagicIncomingWindowMessage, MagicMessageEvent } from '@magic-sdk/types'; import { isTypedArray } from 'lodash'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { EventRegister } from 'react-native-event-listeners'; @@ -13,6 +13,7 @@ import { useInternetConnection } from './hooks'; import { getRefreshTokenInKeychain, setRefreshTokenInKeychain } from './native-crypto/keychain'; import { getDpop } from './native-crypto/dpop'; import { checkNativeModules } from './native-crypto/check-native-modules'; +import { openInApp, openInBrowser } from './lib/links'; const MAGIC_PAYLOAD_FLAG_TYPED_ARRAY = 'MAGIC_PAYLOAD_FLAG_TYPED_ARRAY'; const OPEN_IN_DEVICE_BROWSER = 'open_in_device_browser'; @@ -117,6 +118,23 @@ export class ReactNativeWebViewController extends ViewController { } }, [mountOverlay]); + useEffect(() => { + const removeHandler = this.on(MagicIncomingWindowMessage.MAGIC_OPEN_MOBILE_URL, (event: MagicMessageEvent) => { + const url = event.data.response.result?.url; + const type = event.data.response.result?.type ?? 'drawer'; + + if (!url) return; + + if (type === 'drawer') { + openInApp(url); + } else { + openInBrowser(url); + } + }); + + return removeHandler; + }, []); + /** * Saves a reference to the underlying `` node so we can interact * with incoming messages. diff --git a/packages/@magic-sdk/react-native-expo/src/lib/links.ts b/packages/@magic-sdk/react-native-expo/src/lib/links.ts index e3811f2c6..8be617b16 100644 --- a/packages/@magic-sdk/react-native-expo/src/lib/links.ts +++ b/packages/@magic-sdk/react-native-expo/src/lib/links.ts @@ -5,11 +5,17 @@ export const openInBrowser = async (url: string) => { const supported = await Linking.canOpenURL(url); if (supported) { await Linking.openURL(url); + } else { + console.warn(`Cannot open URL: ${url}`); } }; export const openInApp = async (url: string) => { - await WebBrowser.openBrowserAsync(url, { - presentationStyle: WebBrowser.WebBrowserPresentationStyle.PAGE_SHEET, // iOS - }); + try { + await WebBrowser.openBrowserAsync(url, { + presentationStyle: WebBrowser.WebBrowserPresentationStyle.PAGE_SHEET, // iOS + }); + } catch (e) { + await openInBrowser(url); + } }; diff --git a/yarn.lock b/yarn.lock index 9dff532f3..792c130e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3646,6 +3646,7 @@ __metadata: react-native: ~0.78.1 react-native-device-info: ^10.3.0 react-native-event-listeners: ^1.0.7 + react-native-inappbrowser-reborn: ^3.7.0 react-native-keychain: ^10.0.0 react-native-safe-area-context: 5.3.0 react-native-uuid: ^2.0.3 From 6b1e6e5a76d8fef05567df2413fd197e43fcc674 Mon Sep 17 00:00:00 2001 From: sherzod-bakhodirov Date: Tue, 24 Mar 2026 22:44:49 +0500 Subject: [PATCH 3/5] chore: add react-native-inappbrowser-reborn to transfromIgnore in jest --- packages/@magic-sdk/react-native-bare/jest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@magic-sdk/react-native-bare/jest.config.ts b/packages/@magic-sdk/react-native-bare/jest.config.ts index a5e166e8b..591c76e49 100644 --- a/packages/@magic-sdk/react-native-bare/jest.config.ts +++ b/packages/@magic-sdk/react-native-bare/jest.config.ts @@ -8,7 +8,7 @@ const config: Config.InitialOptions = { '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest', }, transformIgnorePatterns: [ - 'node_modules/(?!((react-native|@magic-sdk/provider|@magic-sdk/react-native.*|react-navigation|@react-native|react-native-gesture-handler|react-native-event-listeners)/).*)', + 'node_modules/(?!((react-native|@magic-sdk/provider|@magic-sdk/react-native.*|react-navigation|@react-native|react-native-gesture-handler|react-native-event-listeners|react-native-inappbrowser-reborn)/).*)', ], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], }; From a56a5eb305681a0c3057447ce561056968eb076c Mon Sep 17 00:00:00 2001 From: sherzod-bakhodirov Date: Wed, 25 Mar 2026 15:47:13 +0500 Subject: [PATCH 4/5] chore: remove in app browser functionality from RN SDKs --- .../@magic-sdk/react-native-bare/package.json | 1 - .../react-native-bare/src/lib/links.ts | 24 ------------------- .../src/react-native-webview-controller.tsx | 10 ++------ .../@magic-sdk/react-native-expo/package.json | 1 - .../react-native-expo/src/lib/links.ts | 11 --------- .../src/react-native-webview-controller.tsx | 10 ++------ yarn.lock | 2 -- 7 files changed, 4 insertions(+), 55 deletions(-) diff --git a/packages/@magic-sdk/react-native-bare/package.json b/packages/@magic-sdk/react-native-bare/package.json index 4dcaf6d90..266fb208d 100644 --- a/packages/@magic-sdk/react-native-bare/package.json +++ b/packages/@magic-sdk/react-native-bare/package.json @@ -27,7 +27,6 @@ "lodash": "^4.17.19", "process": "~0.11.10", "react-native-event-listeners": "^1.0.7", - "react-native-inappbrowser-reborn": "^3.7.0", "react-native-uuid": "^2.0.3", "tslib": "^2.0.3", "whatwg-url": "~8.1.0" diff --git a/packages/@magic-sdk/react-native-bare/src/lib/links.ts b/packages/@magic-sdk/react-native-bare/src/lib/links.ts index 6ceb7fb35..7f5267642 100644 --- a/packages/@magic-sdk/react-native-bare/src/lib/links.ts +++ b/packages/@magic-sdk/react-native-bare/src/lib/links.ts @@ -1,5 +1,4 @@ import { Linking } from 'react-native'; -import { InAppBrowser } from 'react-native-inappbrowser-reborn'; export const openInBrowser = async (url: string) => { const supported = await Linking.canOpenURL(url); @@ -10,26 +9,3 @@ export const openInBrowser = async (url: string) => { console.warn(`Cannot open URL: ${url}`); } }; - -export const openInApp = async (url: string) => { - const isInAppBrowserAvailable = await InAppBrowser.isAvailable(); - - if (!isInAppBrowserAvailable) { - await openInBrowser(url); - return; - } - - try { - await InAppBrowser.open(url, { - // iOS - dismissButtonStyle: 'done', - modalPresentationStyle: 'pageSheet', - - // Android - showTitle: true, - enableUrlBarHiding: true, - }); - } catch (e) { - await openInBrowser(url); - } -}; diff --git a/packages/@magic-sdk/react-native-bare/src/react-native-webview-controller.tsx b/packages/@magic-sdk/react-native-bare/src/react-native-webview-controller.tsx index 9b372a58e..b200ac61e 100644 --- a/packages/@magic-sdk/react-native-bare/src/react-native-webview-controller.tsx +++ b/packages/@magic-sdk/react-native-bare/src/react-native-webview-controller.tsx @@ -13,7 +13,7 @@ import { useInternetConnection } from './hooks'; import { getRefreshTokenInKeychain, setRefreshTokenInKeychain } from './native-crypto/keychain'; import { getDpop } from './native-crypto/dpop'; import { checkNativeModules } from './native-crypto/check-native-modules'; -import { openInApp, openInBrowser } from './lib/links'; +import { openInBrowser } from './lib/links'; const MAGIC_PAYLOAD_FLAG_TYPED_ARRAY = 'MAGIC_PAYLOAD_FLAG_TYPED_ARRAY'; const OPEN_IN_DEVICE_BROWSER = 'open_in_device_browser'; @@ -121,15 +121,9 @@ export class ReactNativeWebViewController extends ViewController { useEffect(() => { const removeHandler = this.on(MagicIncomingWindowMessage.MAGIC_OPEN_MOBILE_URL, (event: MagicMessageEvent) => { const url = event.data.response.result?.url; - const type = event.data.response.result?.type ?? 'drawer'; - if (!url) return; - if (type === 'drawer') { - openInApp(url); - } else { - openInBrowser(url); - } + openInBrowser(url); }); return removeHandler; diff --git a/packages/@magic-sdk/react-native-expo/package.json b/packages/@magic-sdk/react-native-expo/package.json index 1d2d16ec2..f5ddc2959 100644 --- a/packages/@magic-sdk/react-native-expo/package.json +++ b/packages/@magic-sdk/react-native-expo/package.json @@ -23,7 +23,6 @@ "@react-native-async-storage/async-storage": "^1.15.5", "buffer": "~5.6.0", "expo-application": "^7.0.8", - "expo-web-browser": "14.0.2", "localforage": "^1.7.4", "lodash": "^4.17.19", "process": "~0.11.10", diff --git a/packages/@magic-sdk/react-native-expo/src/lib/links.ts b/packages/@magic-sdk/react-native-expo/src/lib/links.ts index 8be617b16..0320c3805 100644 --- a/packages/@magic-sdk/react-native-expo/src/lib/links.ts +++ b/packages/@magic-sdk/react-native-expo/src/lib/links.ts @@ -1,5 +1,4 @@ import { Linking } from 'react-native'; -import * as WebBrowser from 'expo-web-browser'; export const openInBrowser = async (url: string) => { const supported = await Linking.canOpenURL(url); @@ -9,13 +8,3 @@ export const openInBrowser = async (url: string) => { console.warn(`Cannot open URL: ${url}`); } }; - -export const openInApp = async (url: string) => { - try { - await WebBrowser.openBrowserAsync(url, { - presentationStyle: WebBrowser.WebBrowserPresentationStyle.PAGE_SHEET, // iOS - }); - } catch (e) { - await openInBrowser(url); - } -}; diff --git a/packages/@magic-sdk/react-native-expo/src/react-native-webview-controller.tsx b/packages/@magic-sdk/react-native-expo/src/react-native-webview-controller.tsx index 7bb738725..e9a6cdbe1 100644 --- a/packages/@magic-sdk/react-native-expo/src/react-native-webview-controller.tsx +++ b/packages/@magic-sdk/react-native-expo/src/react-native-webview-controller.tsx @@ -10,7 +10,7 @@ import { EventRegister } from 'react-native-event-listeners'; /* global NodeJS */ import Global = NodeJS.Global; import { useInternetConnection } from './hooks'; -import { openInApp, openInBrowser } from './lib/links'; +import { openInBrowser } from './lib/links'; const MAGIC_PAYLOAD_FLAG_TYPED_ARRAY = 'MAGIC_PAYLOAD_FLAG_TYPED_ARRAY'; const OPEN_IN_DEVICE_BROWSER = 'open_in_device_browser'; @@ -95,15 +95,9 @@ export class ReactNativeWebViewController extends ViewController { useEffect(() => { const removeHandler = this.on(MagicIncomingWindowMessage.MAGIC_OPEN_MOBILE_URL, (event: MagicMessageEvent) => { const url = event.data.response.result?.url; - const type = event.data.response.result?.type ?? 'drawer'; - if (!url) return; - if (type === 'drawer') { - openInApp(url); - } else { - openInBrowser(url); - } + openInBrowser(url); }); return removeHandler; diff --git a/yarn.lock b/yarn.lock index 792c130e4..0e99866d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3646,7 +3646,6 @@ __metadata: react-native: ~0.78.1 react-native-device-info: ^10.3.0 react-native-event-listeners: ^1.0.7 - react-native-inappbrowser-reborn: ^3.7.0 react-native-keychain: ^10.0.0 react-native-safe-area-context: 5.3.0 react-native-uuid: ^2.0.3 @@ -3684,7 +3683,6 @@ __metadata: expo: ^54.0.30 expo-application: ^7.0.8 expo-modules-core: ^3.0.29 - expo-web-browser: 14.0.2 jest-expo: ~54.0.16 localforage: ^1.7.4 lodash: ^4.17.19 From 0b61c4f769d0212c82d92ff0c93e28454ca1e20c Mon Sep 17 00:00:00 2001 From: sherzod-bakhodirov Date: Wed, 25 Mar 2026 16:04:44 +0500 Subject: [PATCH 5/5] chore: implements tests --- .../test/spec/lib/links.spec.ts | 42 +++++++++++++++++++ .../test/spec/lib/links.spec.ts | 42 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 packages/@magic-sdk/react-native-bare/test/spec/lib/links.spec.ts create mode 100644 packages/@magic-sdk/react-native-expo/test/spec/lib/links.spec.ts diff --git a/packages/@magic-sdk/react-native-bare/test/spec/lib/links.spec.ts b/packages/@magic-sdk/react-native-bare/test/spec/lib/links.spec.ts new file mode 100644 index 000000000..18226728d --- /dev/null +++ b/packages/@magic-sdk/react-native-bare/test/spec/lib/links.spec.ts @@ -0,0 +1,42 @@ +import { Linking } from 'react-native'; +import { openInBrowser } from '../../../src/lib/links'; + +const mockCanOpenURL = Linking.canOpenURL as jest.Mock; +const mockOpenURL = Linking.openURL as jest.Mock; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('openInBrowser', () => { + it('calls Linking.openURL when the URL is supported', async () => { + const url = 'https://magic.link'; + mockCanOpenURL.mockResolvedValue(true); + mockOpenURL.mockResolvedValue(undefined); + + await openInBrowser(url); + + expect(mockCanOpenURL).toHaveBeenCalledWith(url); + expect(mockOpenURL).toHaveBeenCalledWith(url); + }); + + it('does not call Linking.openURL when the URL is not supported', async () => { + const url = 'unsupported://scheme'; + mockCanOpenURL.mockResolvedValue(false); + + await openInBrowser(url); + + expect(mockCanOpenURL).toHaveBeenCalledWith(url); + expect(mockOpenURL).not.toHaveBeenCalled(); + }); + + it('logs a warning when the URL is not supported', async () => { + const url = 'unsupported://scheme'; + mockCanOpenURL.mockResolvedValue(false); + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + await openInBrowser(url); + + expect(warnSpy).toHaveBeenCalledWith(`Cannot open URL: ${url}`); + }); +}); diff --git a/packages/@magic-sdk/react-native-expo/test/spec/lib/links.spec.ts b/packages/@magic-sdk/react-native-expo/test/spec/lib/links.spec.ts new file mode 100644 index 000000000..18226728d --- /dev/null +++ b/packages/@magic-sdk/react-native-expo/test/spec/lib/links.spec.ts @@ -0,0 +1,42 @@ +import { Linking } from 'react-native'; +import { openInBrowser } from '../../../src/lib/links'; + +const mockCanOpenURL = Linking.canOpenURL as jest.Mock; +const mockOpenURL = Linking.openURL as jest.Mock; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('openInBrowser', () => { + it('calls Linking.openURL when the URL is supported', async () => { + const url = 'https://magic.link'; + mockCanOpenURL.mockResolvedValue(true); + mockOpenURL.mockResolvedValue(undefined); + + await openInBrowser(url); + + expect(mockCanOpenURL).toHaveBeenCalledWith(url); + expect(mockOpenURL).toHaveBeenCalledWith(url); + }); + + it('does not call Linking.openURL when the URL is not supported', async () => { + const url = 'unsupported://scheme'; + mockCanOpenURL.mockResolvedValue(false); + + await openInBrowser(url); + + expect(mockCanOpenURL).toHaveBeenCalledWith(url); + expect(mockOpenURL).not.toHaveBeenCalled(); + }); + + it('logs a warning when the URL is not supported', async () => { + const url = 'unsupported://scheme'; + mockCanOpenURL.mockResolvedValue(false); + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + await openInBrowser(url); + + expect(warnSpy).toHaveBeenCalledWith(`Cannot open URL: ${url}`); + }); +});