diff --git a/packages/url_launcher/url_launcher_android/CHANGELOG.md b/packages/url_launcher/url_launcher_android/CHANGELOG.md index b9270e6a010..cf0da2faf25 100644 --- a/packages/url_launcher/url_launcher_android/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_android/CHANGELOG.md @@ -1,3 +1,10 @@ +## 6.2.0 + +* Adds support for `inAppBrowserView` as a separate launch mode option from + `inAppWebView` mode. `inAppBrowserView` is the preferred in-app mode for most uses, + but does not support `closeInAppWebView`. +* Implements `supportsMode` and `supportsCloseForMode`. + ## 6.1.1 * Updates annotations lib to 1.7.0. diff --git a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/Messages.java b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/Messages.java index eab0d87f5b3..f2294f048f8 100644 --- a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/Messages.java +++ b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/Messages.java @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v10.0.0), do not edit directly. +// Autogenerated from Pigeon (v10.1.4), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.urllauncher; @@ -190,9 +190,15 @@ public interface UrlLauncherApi { /** Opens the URL externally, returning true if successful. */ @NonNull Boolean launchUrl(@NonNull String url, @NonNull Map headers); - /** Opens the URL in an in-app WebView, returning true if it opens successfully. */ + /** + * Opens the URL in an in-app Custom Tab or WebView, returning true if it opens successfully. + */ @NonNull - Boolean openUrlInWebView(@NonNull String url, @NonNull WebViewOptions options); + Boolean openUrlInApp( + @NonNull String url, @NonNull Boolean allowCustomTab, @NonNull WebViewOptions options); + + @NonNull + Boolean supportsCustomTabs(); /** Closes the view opened by [openUrlInSafariViewController]. */ void closeWebView(); @@ -205,7 +211,9 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable UrlLaunche { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl", getCodec()); + binaryMessenger, + "dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.canLaunchUrl", + getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -228,7 +236,9 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable UrlLaunche { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.UrlLauncherApi.launchUrl", getCodec()); + binaryMessenger, + "dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.launchUrl", + getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -252,16 +262,42 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable UrlLaunche { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.UrlLauncherApi.openUrlInWebView", getCodec()); + binaryMessenger, + "dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.openUrlInApp", + getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; String urlArg = (String) args.get(0); - WebViewOptions optionsArg = (WebViewOptions) args.get(1); + Boolean allowCustomTabArg = (Boolean) args.get(1); + WebViewOptions optionsArg = (WebViewOptions) args.get(2); + try { + Boolean output = api.openUrlInApp(urlArg, allowCustomTabArg, optionsArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.supportsCustomTabs", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); try { - Boolean output = api.openUrlInWebView(urlArg, optionsArg); + Boolean output = api.supportsCustomTabs(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); @@ -276,7 +312,9 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable UrlLaunche { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.UrlLauncherApi.closeWebView", getCodec()); + binaryMessenger, + "dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.closeWebView", + getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { diff --git a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java index 8ee9bffbb58..028338c6981 100644 --- a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java +++ b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java @@ -16,9 +16,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.browser.customtabs.CustomTabsClient; import androidx.browser.customtabs.CustomTabsIntent; import io.flutter.plugins.urllauncher.Messages.UrlLauncherApi; import io.flutter.plugins.urllauncher.Messages.WebViewOptions; +import java.util.Collections; import java.util.Locale; import java.util.Map; @@ -95,14 +97,16 @@ void setActivity(@Nullable Activity activity) { } @Override - public @NonNull Boolean openUrlInWebView(@NonNull String url, @NonNull WebViewOptions options) { + public @NonNull Boolean openUrlInApp( + @NonNull String url, @NonNull Boolean allowCustomTab, @NonNull WebViewOptions options) { ensureActivity(); assert activity != null; Bundle headersBundle = extractBundle(options.getHeaders()); - // Try to launch using Custom Tabs if they have the necessary functionality. - if (!containsRestrictedHeader(options.getHeaders())) { + // Try to launch using Custom Tabs if they have the necessary functionality, unless the caller + // specifically requested a web view. + if (allowCustomTab && !containsRestrictedHeader(options.getHeaders())) { Uri uri = Uri.parse(url); if (openCustomTab(activity, uri, headersBundle)) { return true; @@ -131,6 +135,11 @@ public void closeWebView() { applicationContext.sendBroadcast(new Intent(WebViewActivity.ACTION_CLOSE)); } + @Override + public @NonNull Boolean supportsCustomTabs() { + return CustomTabsClient.getPackageName(applicationContext, Collections.emptyList()) != null; + } + private static boolean openCustomTab( @NonNull Context context, @NonNull Uri uri, @NonNull Bundle headersBundle) { CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build(); diff --git a/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/UrlLauncherTest.java b/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/UrlLauncherTest.java index b8bb3b4f21e..3bffbc614f0 100644 --- a/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/UrlLauncherTest.java +++ b/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/UrlLauncherTest.java @@ -130,7 +130,7 @@ public void launch_returnsTrue() { } @Test - public void openWebView_opensUrl_inWebView() { + public void openUrlInApp_opensUrlInWebViewIfNecessary() { Activity activity = mock(Activity.class); UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext()); api.setActivity(activity); @@ -141,8 +141,9 @@ public void openWebView_opensUrl_inWebView() { headers.put("key", "value"); boolean result = - api.openUrlInWebView( + api.openUrlInApp( url, + true, new Messages.WebViewOptions.Builder() .setEnableJavaScript(enableJavaScript) .setEnableDomStorage(enableDomStorage) @@ -162,15 +163,39 @@ public void openWebView_opensUrl_inWebView() { } @Test - public void openWebView_opensUrl_inCustomTabs() { + public void openWebView_opensUrlInWebViewIfRequested() { Activity activity = mock(Activity.class); UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext()); api.setActivity(activity); String url = "https://flutter.dev"; boolean result = - api.openUrlInWebView( + api.openUrlInApp( url, + false, + new Messages.WebViewOptions.Builder() + .setEnableJavaScript(false) + .setEnableDomStorage(false) + .setHeaders(new HashMap<>()) + .build()); + + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(activity).startActivity(intentCaptor.capture()); + assertTrue(result); + assertEquals(url, intentCaptor.getValue().getExtras().getString(WebViewActivity.URL_EXTRA)); + } + + @Test + public void openWebView_opensUrlInCustomTabs() { + Activity activity = mock(Activity.class); + UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext()); + api.setActivity(activity); + String url = "https://flutter.dev"; + + boolean result = + api.openUrlInApp( + url, + true, new Messages.WebViewOptions.Builder() .setEnableJavaScript(false) .setEnableDomStorage(false) @@ -185,7 +210,7 @@ public void openWebView_opensUrl_inCustomTabs() { } @Test - public void openWebView_opensUrl_inCustomTabs_withCORSAllowedHeader() { + public void openWebView_opensUrlInCustomTabsWithCORSAllowedHeader() { Activity activity = mock(Activity.class); UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext()); api.setActivity(activity); @@ -195,8 +220,9 @@ public void openWebView_opensUrl_inCustomTabs_withCORSAllowedHeader() { headers.put(headerKey, "text/plain"); boolean result = - api.openUrlInWebView( + api.openUrlInApp( url, + true, new Messages.WebViewOptions.Builder() .setEnableJavaScript(false) .setEnableDomStorage(false) @@ -214,7 +240,7 @@ public void openWebView_opensUrl_inCustomTabs_withCORSAllowedHeader() { } @Test - public void openWebView_fallsbackTo_inWebView() { + public void openWebView_fallsBackToWebViewIfCustomTabFails() { Activity activity = mock(Activity.class); UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext()); api.setActivity(activity); @@ -224,8 +250,9 @@ public void openWebView_fallsbackTo_inWebView() { .startActivity(any(), isNull()); // for custom tabs intent boolean result = - api.openUrlInWebView( + api.openUrlInApp( url, + true, new Messages.WebViewOptions.Builder() .setEnableJavaScript(false) .setEnableDomStorage(false) @@ -251,8 +278,9 @@ public void openWebView_handlesEnableJavaScript() { HashMap headers = new HashMap<>(); headers.put("key", "value"); - api.openUrlInWebView( + api.openUrlInApp( "https://flutter.dev", + true, new Messages.WebViewOptions.Builder() .setEnableJavaScript(enableJavaScript) .setEnableDomStorage(false) @@ -277,8 +305,9 @@ public void openWebView_handlesHeaders() { headers.put(key1, "value"); headers.put(key2, "value2"); - api.openUrlInWebView( + api.openUrlInApp( "https://flutter.dev", + true, new Messages.WebViewOptions.Builder() .setEnableJavaScript(false) .setEnableDomStorage(false) @@ -303,8 +332,9 @@ public void openWebView_handlesEnableDomStorage() { HashMap headers = new HashMap<>(); headers.put("key", "value"); - api.openUrlInWebView( + api.openUrlInApp( "https://flutter.dev", + true, new Messages.WebViewOptions.Builder() .setEnableJavaScript(false) .setEnableDomStorage(enableDomStorage) @@ -327,8 +357,9 @@ public void openWebView_throwsForNoCurrentActivity() { assertThrows( Messages.FlutterError.class, () -> - api.openUrlInWebView( + api.openUrlInApp( "https://flutter.dev", + true, new Messages.WebViewOptions.Builder() .setEnableJavaScript(false) .setEnableDomStorage(false) @@ -350,8 +381,9 @@ public void openWebView_returnsFalse() { .startActivity(any()); // for webview intent boolean result = - api.openUrlInWebView( + api.openUrlInApp( "https://flutter.dev", + true, new Messages.WebViewOptions.Builder() .setEnableJavaScript(false) .setEnableDomStorage(false) diff --git a/packages/url_launcher/url_launcher_android/example/lib/main.dart b/packages/url_launcher/url_launcher_android/example/lib/main.dart index df28069ca1d..36c32f2ba2b 100644 --- a/packages/url_launcher/url_launcher_android/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_android/example/lib/main.dart @@ -39,6 +39,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; bool _hasCallSupport = false; + bool _hasCustomTabSupport = false; Future? _launched; String _phone = ''; @@ -51,73 +52,77 @@ class _MyHomePageState extends State { _hasCallSupport = result; }); }); + // Check for Android Custom Tab support. + launcher + .supportsMode(PreferredLaunchMode.inAppBrowserView) + .then((bool result) { + setState(() { + _hasCustomTabSupport = result; + }); + }); } Future _launchInBrowser(String url) async { - if (!await launcher.launch( + if (!await launcher.launchUrl( + url, + const LaunchOptions(mode: PreferredLaunchMode.externalApplication), + )) { + throw Exception('Could not launch $url'); + } + } + + Future _launchInCustomTab(String url) async { + if (!await launcher.launchUrl( url, - useSafariVC: false, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, + const LaunchOptions(mode: PreferredLaunchMode.inAppBrowserView), )) { throw Exception('Could not launch $url'); } } Future _launchInWebView(String url) async { - if (!await launcher.launch( + if (!await launcher.launchUrl( url, - useSafariVC: true, - useWebView: true, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, + const LaunchOptions(mode: PreferredLaunchMode.inAppWebView), )) { throw Exception('Could not launch $url'); } } Future _launchInWebViewWithCustomHeaders(String url) async { - if (!await launcher.launch( + if (!await launcher.launchUrl( url, - useSafariVC: true, - useWebView: true, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {'my_header_key': 'my_header_value'}, + const LaunchOptions( + mode: PreferredLaunchMode.inAppWebView, + webViewConfiguration: InAppWebViewConfiguration( + headers: {'my_header_key': 'my_header_value'}, + )), )) { throw Exception('Could not launch $url'); } } - Future _launchInWebViewWithJavaScript(String url) async { - if (!await launcher.launch( + Future _launchInWebViewWithoutJavaScript(String url) async { + if (!await launcher.launchUrl( url, - useSafariVC: true, - useWebView: true, - enableJavaScript: true, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, + const LaunchOptions( + mode: PreferredLaunchMode.inAppWebView, + webViewConfiguration: InAppWebViewConfiguration( + enableJavaScript: false, + )), )) { throw Exception('Could not launch $url'); } } - Future _launchInWebViewWithDomStorage(String url) async { - if (!await launcher.launch( + Future _launchInWebViewWithoutDomStorage(String url) async { + if (!await launcher.launchUrl( url, - useSafariVC: true, - useWebView: true, - enableJavaScript: false, - enableDomStorage: true, - universalLinksOnly: false, - headers: {}, + const LaunchOptions( + mode: PreferredLaunchMode.inAppWebView, + webViewConfiguration: InAppWebViewConfiguration( + enableDomStorage: false, + )), )) { throw Exception('Could not launch $url'); } @@ -133,22 +138,12 @@ class _MyHomePageState extends State { Future _makePhoneCall(String phoneNumber) async { // Use `Uri` to ensure that `phoneNumber` is properly URL-encoded. - // Just using 'tel:$phoneNumber' would create invalid URLs in some cases, - // such as spaces in the input, which would cause `launch` to fail on some - // platforms. + // Just using 'tel:$phoneNumber' would create invalid URLs in some cases. final Uri launchUri = Uri( scheme: 'tel', path: phoneNumber, ); - await launcher.launch( - launchUri.toString(), - useSafariVC: false, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: true, - headers: {}, - ); + await launcher.launchUrl(launchUri.toString(), const LaunchOptions()); } @override @@ -186,36 +181,45 @@ class _MyHomePageState extends State { padding: EdgeInsets.all(16.0), child: Text(toLaunch), ), + ElevatedButton( + onPressed: _hasCustomTabSupport + ? () => setState(() { + _launched = _launchInBrowser(toLaunch); + }) + : null, + child: const Text('Launch in browser'), + ), + const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInBrowser(toLaunch); + _launched = _launchInCustomTab(toLaunch); }), - child: const Text('Launch in browser'), + child: const Text('Launch in Android Custom Tab'), ), const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebView(toLaunch); }), - child: const Text('Launch in app'), + child: const Text('Launch in web view'), ), ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewWithCustomHeaders(toLaunch); }), - child: const Text('Launch in app (Custom headers)'), + child: const Text('Launch in web view (Custom headers)'), ), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInWebViewWithJavaScript(toLaunch); + _launched = _launchInWebViewWithoutJavaScript(toLaunch); }), - child: const Text('Launch in app (JavaScript ON)'), + child: const Text('Launch in web view (JavaScript OFF)'), ), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInWebViewWithDomStorage(toLaunch); + _launched = _launchInWebViewWithoutDomStorage(toLaunch); }), - child: const Text('Launch in app (DOM storage ON)'), + child: const Text('Launch in web view (DOM storage OFF)'), ), const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( @@ -225,7 +229,7 @@ class _MyHomePageState extends State { launcher.closeWebView(); }); }), - child: const Text('Launch in app + close after 5 seconds'), + child: const Text('Launch in web view + close after 5 seconds'), ), const Padding(padding: EdgeInsets.all(16.0)), FutureBuilder(future: _launched, builder: _launchStatus), diff --git a/packages/url_launcher/url_launcher_android/example/pubspec.yaml b/packages/url_launcher/url_launcher_android/example/pubspec.yaml index e8aee1779b0..d234884c6b1 100644 --- a/packages/url_launcher/url_launcher_android/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_android/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - url_launcher_platform_interface: ^2.0.3 + url_launcher_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/url_launcher/url_launcher_android/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_android/lib/src/messages.g.dart index 9aed8f7f60f..9d6ce26ed85 100644 --- a/packages/url_launcher/url_launcher_android/lib/src/messages.g.dart +++ b/packages/url_launcher/url_launcher_android/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v10.0.0), do not edit directly. +// Autogenerated from Pigeon (v10.1.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import @@ -79,7 +79,8 @@ class UrlLauncherApi { /// Returns true if the URL can definitely be launched. Future canLaunchUrl(String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl', codec, + 'dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.canLaunchUrl', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_url]) as List?; @@ -108,7 +109,8 @@ class UrlLauncherApi { Future launchUrl( String arg_url, Map arg_headers) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec, + 'dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.launchUrl', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_url, arg_headers]) as List?; @@ -133,15 +135,44 @@ class UrlLauncherApi { } } - /// Opens the URL in an in-app WebView, returning true if it opens - /// successfully. - Future openUrlInWebView( - String arg_url, WebViewOptions arg_options) async { + /// Opens the URL in an in-app Custom Tab or WebView, returning true if it + /// opens successfully. + Future openUrlInApp(String arg_url, bool arg_allowCustomTab, + WebViewOptions arg_options) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UrlLauncherApi.openUrlInWebView', codec, + 'dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.openUrlInApp', + codec, binaryMessenger: _binaryMessenger); final List? replyList = - await channel.send([arg_url, arg_options]) as List?; + await channel.send([arg_url, arg_allowCustomTab, arg_options]) + as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + Future supportsCustomTabs() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.supportsCustomTabs', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -166,7 +197,8 @@ class UrlLauncherApi { /// Closes the view opened by [openUrlInSafariViewController]. Future closeWebView() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UrlLauncherApi.closeWebView', codec, + 'dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.closeWebView', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send(null) as List?; if (replyList == null) { diff --git a/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart index 7b53b85f793..f121084b757 100644 --- a/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart +++ b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart @@ -49,8 +49,6 @@ class UrlLauncherAndroid extends UrlLauncherPlatform { return _hostApi.closeWebView(); } - // TODO(stuartmorgan): Implement launchUrl, and make this a passthrough - // to launchUrl. See also https://github.com/flutter/flutter/issues/66721 @override Future launch( String url, { @@ -62,16 +60,57 @@ class UrlLauncherAndroid extends UrlLauncherPlatform { required Map headers, String? webOnlyWindowName, }) async { + return launchUrl( + url, + LaunchOptions( + mode: useWebView + ? PreferredLaunchMode.inAppWebView + : PreferredLaunchMode.externalApplication, + webViewConfiguration: InAppWebViewConfiguration( + enableDomStorage: enableDomStorage, + enableJavaScript: enableJavaScript, + headers: headers))); + } + + @override + Future launchUrl(String url, LaunchOptions options) async { + final bool inApp; + switch (options.mode) { + case PreferredLaunchMode.inAppWebView: + case PreferredLaunchMode.inAppBrowserView: + inApp = true; + break; + case PreferredLaunchMode.externalApplication: + case PreferredLaunchMode.externalNonBrowserApplication: + // TODO(stuartmorgan): Add full support for + // externalNonBrowsingApplication; see + // https://github.com/flutter/flutter/issues/66721. + // Currently it's treated the same as externalApplication. + inApp = false; + break; + case PreferredLaunchMode.platformDefault: + // Intentionally treat any new values as platformDefault; see comment in + // supportsMode. + // ignore: no_default_cases + default: + // By default, open web URLs in the application. + inApp = url.startsWith('http:') || url.startsWith('https:'); + break; + } + final bool succeeded; - if (useWebView) { - succeeded = await _hostApi.openUrlInWebView( + if (inApp) { + succeeded = await _hostApi.openUrlInApp( url, + // Prefer custom tabs unless a webview was specifically requested. + options.mode != PreferredLaunchMode.inAppWebView, WebViewOptions( - enableJavaScript: enableJavaScript, - enableDomStorage: enableDomStorage, - headers: headers)); + enableJavaScript: options.webViewConfiguration.enableJavaScript, + enableDomStorage: options.webViewConfiguration.enableDomStorage, + headers: options.webViewConfiguration.headers)); } else { - succeeded = await _hostApi.launchUrl(url, headers); + succeeded = + await _hostApi.launchUrl(url, options.webViewConfiguration.headers); } // TODO(stuartmorgan): Remove this special handling as part of a // breaking change to rework failure handling across all platform. The @@ -84,6 +123,29 @@ class UrlLauncherAndroid extends UrlLauncherPlatform { return succeeded; } + @override + Future supportsMode(PreferredLaunchMode mode) async { + switch (mode) { + case PreferredLaunchMode.platformDefault: + case PreferredLaunchMode.inAppWebView: + case PreferredLaunchMode.externalApplication: + return true; + case PreferredLaunchMode.inAppBrowserView: + return _hostApi.supportsCustomTabs(); + // Default is a desired behavior here since support for new modes is + // always opt-in, and the enum lives in a different package, so silently + // adding "false" for new values is the correct behavior. + // ignore: no_default_cases + default: + return false; + } + } + + @override + Future supportsCloseForMode(PreferredLaunchMode mode) async { + return mode == PreferredLaunchMode.inAppWebView; + } + // Returns the part of [url] up to the first ':', or an empty string if there // is no ':'. This deliberately does not use [Uri] to extract the scheme // so that it works on strings that aren't actually valid URLs, since Android diff --git a/packages/url_launcher/url_launcher_android/pigeons/messages.dart b/packages/url_launcher/url_launcher_android/pigeons/messages.dart index 84e507d7024..d7184413409 100644 --- a/packages/url_launcher/url_launcher_android/pigeons/messages.dart +++ b/packages/url_launcher/url_launcher_android/pigeons/messages.dart @@ -33,9 +33,11 @@ abstract class UrlLauncherApi { /// Opens the URL externally, returning true if successful. bool launchUrl(String url, Map headers); - /// Opens the URL in an in-app WebView, returning true if it opens - /// successfully. - bool openUrlInWebView(String url, WebViewOptions options); + /// Opens the URL in an in-app Custom Tab or WebView, returning true if it + /// opens successfully. + bool openUrlInApp(String url, bool allowCustomTab, WebViewOptions options); + + bool supportsCustomTabs(); /// Closes the view opened by [openUrlInSafariViewController]. void closeWebView(); diff --git a/packages/url_launcher/url_launcher_android/pubspec.yaml b/packages/url_launcher/url_launcher_android/pubspec.yaml index 09e865d6d58..5b5d06e8f23 100644 --- a/packages/url_launcher/url_launcher_android/pubspec.yaml +++ b/packages/url_launcher/url_launcher_android/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_android description: Android implementation of the url_launcher plugin. repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.1.1 +version: 6.2.0 environment: sdk: ">=2.19.0 <4.0.0" flutter: ">=3.7.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - url_launcher_platform_interface: ^2.0.3 + url_launcher_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart index 3b3d012a168..2b331cbd027 100644 --- a/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart +++ b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart @@ -52,7 +52,7 @@ void main() { }); }); - group('launch without webview', () { + group('legacy launch without webview', () { test('calls through', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); final bool launched = await launcher.launch( @@ -88,7 +88,7 @@ void main() { final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); await expectLater( launcher.launch( - 'noactivity://', + 'https://noactivity', useSafariVC: false, useWebView: false, enableJavaScript: false, @@ -116,7 +116,7 @@ void main() { }); }); - group('launch with webview', () { + group('legacy launch with webview', () { test('calls through', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); final bool launched = await launcher.launch( @@ -130,6 +130,7 @@ void main() { ); expect(launched, true); expect(api.usedWebView, true); + expect(api.allowedCustomTab, false); expect(api.passedWebViewOptions?.enableDomStorage, false); expect(api.passedWebViewOptions?.enableJavaScript, false); expect(api.passedWebViewOptions?.headers, isEmpty); @@ -169,7 +170,7 @@ void main() { final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); await expectLater( launcher.launch( - 'noactivity://scheme', + 'https://noactivity', useSafariVC: false, useWebView: true, enableJavaScript: false, @@ -197,12 +198,198 @@ void main() { }); }); - group('closeWebView', () { + group('launch without webview', () { test('calls through', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); - await launcher.closeWebView(); + final bool launched = await launcher.launchUrl( + 'http://example.com/', + const LaunchOptions(mode: PreferredLaunchMode.externalApplication), + ); + expect(launched, true); + expect(api.usedWebView, false); + expect(api.passedWebViewOptions?.headers, isEmpty); + }); - expect(api.closed, true); + test('passes headers', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + await launcher.launchUrl( + 'http://example.com/', + const LaunchOptions( + mode: PreferredLaunchMode.externalApplication, + webViewConfiguration: InAppWebViewConfiguration( + headers: {'key': 'value'})), + ); + expect(api.passedWebViewOptions?.headers.length, 1); + expect(api.passedWebViewOptions?.headers['key'], 'value'); + }); + + test('passes through no-activity exception', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + await expectLater( + launcher.launchUrl('https://noactivity', const LaunchOptions()), + throwsA(isA())); + }); + + test('throws if there is no handling activity', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + await expectLater( + launcher.launchUrl('unknown://scheme', const LaunchOptions()), + throwsA(isA().having( + (PlatformException e) => e.code, 'code', 'ACTIVITY_NOT_FOUND'))); + }); + }); + + group('launch with webview', () { + test('calls through', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + final bool launched = await launcher.launchUrl('http://example.com/', + const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)); + expect(launched, true); + expect(api.usedWebView, true); + expect(api.allowedCustomTab, false); + expect(api.passedWebViewOptions?.enableDomStorage, true); + expect(api.passedWebViewOptions?.enableJavaScript, true); + expect(api.passedWebViewOptions?.headers, isEmpty); + }); + + test('passes enableJavaScript to webview', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + await launcher.launchUrl( + 'http://example.com/', + const LaunchOptions( + mode: PreferredLaunchMode.inAppWebView, + webViewConfiguration: + InAppWebViewConfiguration(enableJavaScript: false))); + + expect(api.passedWebViewOptions?.enableJavaScript, false); + }); + + test('passes enableDomStorage to webview', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + await launcher.launchUrl( + 'http://example.com/', + const LaunchOptions( + mode: PreferredLaunchMode.inAppWebView, + webViewConfiguration: + InAppWebViewConfiguration(enableDomStorage: false))); + + expect(api.passedWebViewOptions?.enableDomStorage, false); + }); + + test('passes through no-activity exception', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + await expectLater( + launcher.launchUrl('https://noactivity', + const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)), + throwsA(isA())); + }); + + test('throws if there is no handling activity', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + await expectLater( + launcher.launchUrl('unknown://scheme', + const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)), + throwsA(isA().having( + (PlatformException e) => e.code, 'code', 'ACTIVITY_NOT_FOUND'))); + }); + }); + + group('launch with custom tab', () { + test('calls through', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + final bool launched = await launcher.launchUrl('http://example.com/', + const LaunchOptions(mode: PreferredLaunchMode.inAppBrowserView)); + expect(launched, true); + expect(api.usedWebView, true); + expect(api.allowedCustomTab, true); + }); + }); + + group('launch with platform default', () { + test('uses custom tabs for http', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + final bool launched = await launcher.launchUrl( + 'http://example.com/', const LaunchOptions()); + expect(launched, true); + expect(api.usedWebView, true); + expect(api.allowedCustomTab, true); + }); + + test('uses custom tabs for https', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + final bool launched = await launcher.launchUrl( + 'https://example.com/', const LaunchOptions()); + expect(launched, true); + expect(api.usedWebView, true); + expect(api.allowedCustomTab, true); + }); + + test('uses external for other schemes', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + final bool launched = await launcher.launchUrl( + 'supportedcustomscheme://example.com/', const LaunchOptions()); + expect(launched, true); + expect(api.usedWebView, false); + }); + }); + + group('supportsMode', () { + test('returns true for platformDefault', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + expect(await launcher.supportsMode(PreferredLaunchMode.platformDefault), + true); + }); + + test('returns true for external application', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + expect( + await launcher.supportsMode(PreferredLaunchMode.externalApplication), + true); + }); + + test('returns true for in app web view', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + expect( + await launcher.supportsMode(PreferredLaunchMode.inAppWebView), true); + }); + + test('returns true for in app browser view when available', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + api.hasCustomTabSupport = true; + expect(await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView), + true); + }); + + test('returns false for in app browser view when not available', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + api.hasCustomTabSupport = false; + expect(await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView), + false); + }); + }); + + group('supportsCloseForMode', () { + test('returns true for in app web view', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + expect( + await launcher.supportsCloseForMode(PreferredLaunchMode.inAppWebView), + true); + }); + + test('returns false for other modes', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + expect( + await launcher + .supportsCloseForMode(PreferredLaunchMode.externalApplication), + false); + expect( + await launcher.supportsCloseForMode( + PreferredLaunchMode.externalNonBrowserApplication), + false); + expect( + await launcher + .supportsCloseForMode(PreferredLaunchMode.inAppBrowserView), + false); }); }); } @@ -211,8 +398,10 @@ void main() { /// /// See _launch for the behaviors. class _FakeUrlLauncherApi implements UrlLauncherApi { + bool hasCustomTabSupport = true; WebViewOptions? passedWebViewOptions; bool? usedWebView; + bool? allowedCustomTab; bool? closed; /// A domain that will be treated as having no handler, even for http(s). @@ -237,20 +426,29 @@ class _FakeUrlLauncherApi implements UrlLauncherApi { } @override - Future openUrlInWebView(String url, WebViewOptions options) async { + Future openUrlInApp( + String url, bool allowCustomTab, WebViewOptions options) async { passedWebViewOptions = options; usedWebView = true; + allowedCustomTab = allowCustomTab; return _launch(url); } + @override + Future supportsCustomTabs() async { + return hasCustomTabSupport; + } + bool _launch(String url) { final String scheme = url.split(':')[0]; switch (scheme) { case 'http': case 'https': + case 'supportedcustomscheme': + if (url.endsWith('noactivity')) { + throw PlatformException(code: 'NO_ACTIVITY'); + } return !url.contains(specialHandlerDomain); - case 'noactivity': - throw PlatformException(code: 'NO_ACTIVITY'); default: return false; } diff --git a/packages/url_launcher/url_launcher_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md index ad52522d002..ae630129705 100644 --- a/packages/url_launcher/url_launcher_ios/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.2.0 + +* Implements `supportsMode` and `supportsCloseForMode`. + ## 6.1.5 * Adds pub topics to package metadata. diff --git a/packages/url_launcher/url_launcher_ios/example/pubspec.yaml b/packages/url_launcher/url_launcher_ios/example/pubspec.yaml index d755c553f6a..3daaa9acd5c 100644 --- a/packages/url_launcher/url_launcher_ios/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - url_launcher_platform_interface: ^2.0.3 + url_launcher_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart index 2f0e9f47b94..66969787fba 100644 --- a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart +++ b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart @@ -45,11 +45,79 @@ class UrlLauncherIOS extends UrlLauncherPlatform { required bool universalLinksOnly, required Map headers, String? webOnlyWindowName, - }) { + }) async { + final PreferredLaunchMode mode; if (useSafariVC) { + mode = PreferredLaunchMode.inAppBrowserView; + } else if (universalLinksOnly) { + mode = PreferredLaunchMode.externalNonBrowserApplication; + } else { + mode = PreferredLaunchMode.externalApplication; + } + return launchUrl( + url, + LaunchOptions( + mode: mode, + webViewConfiguration: InAppWebViewConfiguration( + enableDomStorage: enableDomStorage, + enableJavaScript: enableJavaScript, + headers: headers))); + } + + @override + Future launchUrl(String url, LaunchOptions options) async { + final bool inApp; + switch (options.mode) { + case PreferredLaunchMode.inAppWebView: + case PreferredLaunchMode.inAppBrowserView: + // The iOS implementation doesn't distinguish between these two modes; + // both are treated as inAppBrowserView. + inApp = true; + break; + case PreferredLaunchMode.externalApplication: + case PreferredLaunchMode.externalNonBrowserApplication: + inApp = false; + break; + case PreferredLaunchMode.platformDefault: + // Intentionally treat any new values as platformDefault; support for any + // new mode requires intentional opt-in, otherwise falling back is the + // documented behavior. + // ignore: no_default_cases + default: + // By default, open web URLs in the application. + inApp = url.startsWith('http:') || url.startsWith('https:'); + break; + } + + if (inApp) { return _hostApi.openUrlInSafariViewController(url); } else { - return _hostApi.launchUrl(url, universalLinksOnly); + return _hostApi.launchUrl(url, + options.mode == PreferredLaunchMode.externalNonBrowserApplication); } } + + @override + Future supportsMode(PreferredLaunchMode mode) async { + switch (mode) { + case PreferredLaunchMode.platformDefault: + case PreferredLaunchMode.inAppWebView: + case PreferredLaunchMode.inAppBrowserView: + case PreferredLaunchMode.externalApplication: + case PreferredLaunchMode.externalNonBrowserApplication: + return true; + // Default is a desired behavior here since support for new modes is + // always opt-in, and the enum lives in a different package, so silently + // adding "false" for new values is the correct behavior. + // ignore: no_default_cases + default: + return false; + } + } + + @override + Future supportsCloseForMode(PreferredLaunchMode mode) async { + return mode == PreferredLaunchMode.inAppWebView || + mode == PreferredLaunchMode.inAppBrowserView; + } } diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml index 8b45ed75054..6047568083b 100644 --- a/packages/url_launcher/url_launcher_ios/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_ios description: iOS implementation of the url_launcher plugin. repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.1.5 +version: 6.2.0 environment: sdk: ">=2.19.0 <4.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - url_launcher_platform_interface: ^2.0.3 + url_launcher_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart index 9274173f90b..bacea3132c1 100644 --- a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart +++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart @@ -11,36 +11,37 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('UrlLauncherIOS', () { - late _FakeUrlLauncherApi api; + late _FakeUrlLauncherApi api; - setUp(() { - api = _FakeUrlLauncherApi(); - }); + setUp(() { + api = _FakeUrlLauncherApi(); + }); - test('registers instance', () { - UrlLauncherIOS.registerWith(); - expect(UrlLauncherPlatform.instance, isA()); - }); + test('registers instance', () { + UrlLauncherIOS.registerWith(); + expect(UrlLauncherPlatform.instance, isA()); + }); - test('canLaunch success', () async { + group('canLaunch', () { + test('handles success', () async { final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); expect(await launcher.canLaunch('http://example.com/'), true); }); - test('canLaunch failure', () async { + test('handles failure', () async { final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); expect(await launcher.canLaunch('unknown://scheme'), false); }); - test('canLaunch invalid URL passes the PlatformException through', - () async { + test('passes invalid URL PlatformException through', () async { final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); await expectLater(launcher.canLaunch('invalid://u r l'), throwsA(isA())); }); + }); - test('launch success', () async { + group('legacy launch', () { + test('handles success', () async { final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); expect( await launcher.launch( @@ -56,7 +57,7 @@ void main() { expect(api.passedUniversalLinksOnly, false); }); - test('launch failure', () async { + test('handles failure', () async { final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); expect( await launcher.launch( @@ -72,7 +73,7 @@ void main() { expect(api.passedUniversalLinksOnly, false); }); - test('launch invalid URL passes the PlatformException through', () async { + test('passes invalid URL PlatformException through', () async { final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); await expectLater( launcher.launch( @@ -87,7 +88,7 @@ void main() { throwsA(isA())); }); - test('launch force SafariVC', () async { + test('force SafariVC is handled', () async { final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); expect( await launcher.launch( @@ -103,7 +104,7 @@ void main() { expect(api.usedSafariViewController, true); }); - test('launch universal links only', () async { + test('universal links only is handled', () async { final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); expect( await launcher.launch( @@ -119,7 +120,7 @@ void main() { expect(api.passedUniversalLinksOnly, true); }); - test('launch force SafariVC to false', () async { + test('disallowing SafariVC is handled', () async { final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); expect( await launcher.launch( @@ -134,11 +135,171 @@ void main() { true); expect(api.usedSafariViewController, false); }); + }); + + test('closeWebView calls through', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + await launcher.closeWebView(); + expect(api.closed, true); + }); + + group('launch without webview', () { + test('calls through', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + final bool launched = await launcher.launchUrl( + 'http://example.com/', + const LaunchOptions(mode: PreferredLaunchMode.externalApplication), + ); + expect(launched, true); + expect(api.usedSafariViewController, false); + }); + + test('passes invalid URL PlatformException through', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + await expectLater( + launcher.launchUrl('invalid://u r l', const LaunchOptions()), + throwsA(isA())); + }); + }); + + group('launch with Safari view controller', () { + test('calls through with inAppWebView', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + final bool launched = await launcher.launchUrl('http://example.com/', + const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)); + expect(launched, true); + expect(api.usedSafariViewController, true); + }); + + test('calls through with inAppBrowserView', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + final bool launched = await launcher.launchUrl('http://example.com/', + const LaunchOptions(mode: PreferredLaunchMode.inAppBrowserView)); + expect(launched, true); + expect(api.usedSafariViewController, true); + }); + + test('passes invalid URL PlatformException through', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + await expectLater( + launcher.launchUrl('invalid://u r l', + const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)), + throwsA(isA())); + }); + }); + + group('launch with universal links', () { + test('calls through', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + final bool launched = await launcher.launchUrl( + 'http://example.com/', + const LaunchOptions( + mode: PreferredLaunchMode.externalNonBrowserApplication), + ); + expect(launched, true); + expect(api.usedSafariViewController, false); + expect(api.passedUniversalLinksOnly, true); + }); + + test('passes invalid URL PlatformException through', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + await expectLater( + launcher.launchUrl( + 'invalid://u r l', + const LaunchOptions( + mode: PreferredLaunchMode.externalNonBrowserApplication)), + throwsA(isA())); + }); + }); + + group('launch with platform default', () { + test('uses Safari view controller for http', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + final bool launched = await launcher.launchUrl( + 'http://example.com/', const LaunchOptions()); + expect(launched, true); + expect(api.usedSafariViewController, true); + }); + + test('uses Safari view controller for https', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + final bool launched = await launcher.launchUrl( + 'https://example.com/', const LaunchOptions()); + expect(launched, true); + expect(api.usedSafariViewController, true); + }); + + test('uses standard external for other schemes', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + final bool launched = await launcher.launchUrl( + 'supportedcustomscheme://example.com/', const LaunchOptions()); + expect(launched, true); + expect(api.usedSafariViewController, false); + expect(api.passedUniversalLinksOnly, false); + }); + }); + + group('supportsMode', () { + test('returns true for platformDefault', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + expect(await launcher.supportsMode(PreferredLaunchMode.platformDefault), + true); + }); + + test('returns true for external application', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + expect( + await launcher.supportsMode(PreferredLaunchMode.externalApplication), + true); + }); + + test('returns true for external non-browser application', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + expect( + await launcher + .supportsMode(PreferredLaunchMode.externalNonBrowserApplication), + true); + }); - test('closeWebView default behavior', () async { + test('returns true for in app web view', () async { final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); - await launcher.closeWebView(); - expect(api.closed, true); + expect( + await launcher.supportsMode(PreferredLaunchMode.inAppWebView), true); + }); + + test('returns true for in app browser view', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + expect(await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView), + true); + }); + }); + + group('supportsCloseForMode', () { + test('returns true for in app web view', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + expect( + await launcher.supportsCloseForMode(PreferredLaunchMode.inAppWebView), + true); + }); + + test('returns true for in app browser view', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + expect( + await launcher + .supportsCloseForMode(PreferredLaunchMode.inAppBrowserView), + true); + }); + + test('returns false for other modes', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + expect( + await launcher + .supportsCloseForMode(PreferredLaunchMode.externalApplication), + false); + expect( + await launcher.supportsCloseForMode( + PreferredLaunchMode.externalNonBrowserApplication), + false); }); }); } @@ -179,6 +340,7 @@ class _FakeUrlLauncherApi implements UrlLauncherApi { switch (scheme) { case 'http': case 'https': + case 'supportedcustomscheme': return true; case 'invalid': throw PlatformException(code: 'argument_error'); diff --git a/packages/url_launcher/url_launcher_linux/CHANGELOG.md b/packages/url_launcher/url_launcher_linux/CHANGELOG.md index a7f308737b4..538fb7737a3 100644 --- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.1.0 + +* Implements `supportsMode` and `supportsCloseForMode`. + ## 3.0.6 * Adds pub topics to package metadata. diff --git a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml index 0dd4a4cf8a3..92aff2e3d2c 100644 --- a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - url_launcher_platform_interface: ^2.0.0 + url_launcher_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart b/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart index 87ef3142e3f..286ac923ce9 100644 --- a/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart +++ b/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart @@ -51,4 +51,16 @@ class UrlLauncherLinux extends UrlLauncherPlatform { }, ).then((bool? value) => value ?? false); } + + @override + Future supportsMode(PreferredLaunchMode mode) async { + return mode == PreferredLaunchMode.platformDefault || + mode == PreferredLaunchMode.externalApplication; + } + + @override + Future supportsCloseForMode(PreferredLaunchMode mode) async { + // No supported mode is closeable. + return false; + } } diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml index 2649beeecb9..315ecd2ffb2 100644 --- a/packages/url_launcher/url_launcher_linux/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_linux description: Linux implementation of the url_launcher plugin. repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 3.0.6 +version: 3.1.0 environment: sdk: ">=2.19.0 <4.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - url_launcher_platform_interface: ^2.0.3 + url_launcher_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart b/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart index 4e62cc44619..c7e6c8e328c 100644 --- a/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart +++ b/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart @@ -10,7 +10,7 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('$UrlLauncherLinux', () { + group('UrlLauncherLinux', () { const MethodChannel channel = MethodChannel('plugins.flutter.io/url_launcher_linux'); final List log = []; @@ -142,6 +142,47 @@ void main() { expect(launched, false); }); + + group('supportsMode', () { + test('returns true for platformDefault', () async { + final UrlLauncherLinux launcher = UrlLauncherLinux(); + expect(await launcher.supportsMode(PreferredLaunchMode.platformDefault), + true); + }); + + test('returns true for external application', () async { + final UrlLauncherLinux launcher = UrlLauncherLinux(); + expect( + await launcher + .supportsMode(PreferredLaunchMode.externalApplication), + true); + }); + + test('returns false for other modes', () async { + final UrlLauncherLinux launcher = UrlLauncherLinux(); + expect( + await launcher.supportsMode( + PreferredLaunchMode.externalNonBrowserApplication), + false); + expect( + await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView), + false); + expect(await launcher.supportsMode(PreferredLaunchMode.inAppWebView), + false); + }); + }); + + test('supportsCloseForMode returns false', () async { + final UrlLauncherLinux launcher = UrlLauncherLinux(); + expect( + await launcher + .supportsCloseForMode(PreferredLaunchMode.platformDefault), + false); + expect( + await launcher + .supportsCloseForMode(PreferredLaunchMode.externalApplication), + false); + }); }); } diff --git a/packages/url_launcher/url_launcher_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md index e2f5c6855ed..ac8a05af5c9 100644 --- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.1.0 + +* Implements `supportsMode` and `supportsCloseForMode`. + ## 3.0.7 * Adds pub topics to package metadata. diff --git a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml index 0ad23902782..3c98f474d2f 100644 --- a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - url_launcher_platform_interface: ^2.0.0 + url_launcher_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart b/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart index 55c07b798cd..1d229737c34 100644 --- a/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart +++ b/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart @@ -57,6 +57,18 @@ class UrlLauncherMacOS extends UrlLauncherPlatform { return result.value; } + @override + Future supportsMode(PreferredLaunchMode mode) async { + return mode == PreferredLaunchMode.platformDefault || + mode == PreferredLaunchMode.externalApplication; + } + + @override + Future supportsCloseForMode(PreferredLaunchMode mode) async { + // No supported mode is closeable. + return false; + } + Exception _getInvalidUrlException(String url) { // TODO(stuartmorgan): Make this an actual ArgumentError. This should be // coordinated across all platforms as a breaking change to have them all diff --git a/packages/url_launcher/url_launcher_macos/pubspec.yaml b/packages/url_launcher/url_launcher_macos/pubspec.yaml index 23d5f6a2f47..ba7066c1617 100644 --- a/packages/url_launcher/url_launcher_macos/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_macos description: macOS implementation of the url_launcher plugin. repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 3.0.7 +version: 3.1.0 environment: sdk: ">=2.19.0 <4.0.0" @@ -20,7 +20,7 @@ flutter: dependencies: flutter: sdk: flutter - url_launcher_platform_interface: ^2.0.3 + url_launcher_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart b/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart index a7147af7749..e9cc3c6c6dc 100644 --- a/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart +++ b/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart @@ -106,6 +106,47 @@ void main() { throwsA(isA())); }); }); + + group('supportsMode', () { + test('returns true for platformDefault', () async { + final UrlLauncherMacOS launcher = UrlLauncherMacOS(api: api); + expect(await launcher.supportsMode(PreferredLaunchMode.platformDefault), + true); + }); + + test('returns true for external application', () async { + final UrlLauncherMacOS launcher = UrlLauncherMacOS(api: api); + expect( + await launcher + .supportsMode(PreferredLaunchMode.externalApplication), + true); + }); + + test('returns false for other modes', () async { + final UrlLauncherMacOS launcher = UrlLauncherMacOS(api: api); + expect( + await launcher.supportsMode( + PreferredLaunchMode.externalNonBrowserApplication), + false); + expect( + await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView), + false); + expect(await launcher.supportsMode(PreferredLaunchMode.inAppWebView), + false); + }); + }); + + test('supportsCloseForMode returns false', () async { + final UrlLauncherMacOS launcher = UrlLauncherMacOS(api: api); + expect( + await launcher + .supportsCloseForMode(PreferredLaunchMode.platformDefault), + false); + expect( + await launcher + .supportsCloseForMode(PreferredLaunchMode.externalApplication), + false); + }); }); } diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index 836afdcde8a..76979c9c4c6 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.0 + +* Implements `supportsMode` and `supportsCloseForMode`. + ## 2.1.0 * Adds `launchUrl` implementation. diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart index c9091863ad3..994e3b28bad 100644 --- a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart +++ b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart @@ -8,6 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import 'package:url_launcher_web/url_launcher_web.dart'; import 'url_launcher_web_test.mocks.dart'; @@ -218,5 +219,30 @@ void main() { }); }); }); + + group('supportsMode', () { + testWidgets('returns true for platformDefault', (WidgetTester _) async { + expect(plugin.supportsMode(PreferredLaunchMode.platformDefault), + completion(isTrue)); + }); + + testWidgets('returns false for other modes', (WidgetTester _) async { + expect(plugin.supportsMode(PreferredLaunchMode.externalApplication), + completion(isFalse)); + expect( + plugin.supportsMode( + PreferredLaunchMode.externalNonBrowserApplication), + completion(isFalse)); + expect(plugin.supportsMode(PreferredLaunchMode.inAppBrowserView), + completion(isFalse)); + expect(plugin.supportsMode(PreferredLaunchMode.inAppWebView), + completion(isFalse)); + }); + }); + + testWidgets('supportsCloseForMode returns false', (WidgetTester _) async { + expect(plugin.supportsCloseForMode(PreferredLaunchMode.platformDefault), + completion(isFalse)); + }); }); } diff --git a/packages/url_launcher/url_launcher_web/example/pubspec.yaml b/packages/url_launcher/url_launcher_web/example/pubspec.yaml index 9915164471a..d0964233150 100644 --- a/packages/url_launcher/url_launcher_web/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/example/pubspec.yaml @@ -16,6 +16,6 @@ dev_dependencies: integration_test: sdk: flutter mockito: 5.4.1 - url_launcher_platform_interface: ^2.0.3 + url_launcher_platform_interface: ^2.2.0 url_launcher_web: path: ../ diff --git a/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart index bf96bf2aa29..0dd1012f708 100644 --- a/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart +++ b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart @@ -111,4 +111,17 @@ class UrlLauncherPlugin extends UrlLauncherPlatform { final String? windowName = options.webOnlyWindowName; return openNewWindow(url, webOnlyWindowName: windowName) != null; } + + @override + Future supportsMode(PreferredLaunchMode mode) async { + // Web doesn't allow any control over the destination beyond + // webOnlyWindowName, so don't claim support for any mode beyond default. + return mode == PreferredLaunchMode.platformDefault; + } + + @override + Future supportsCloseForMode(PreferredLaunchMode mode) async { + // No supported mode is closeable. + return false; + } } diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index 95dd915d287..b70a09439e7 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_web description: Web platform implementation of url_launcher repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.1.0 +version: 2.2.0 environment: sdk: ">=3.1.0 <4.0.0" @@ -21,7 +21,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - url_launcher_platform_interface: ^2.1.0 + url_launcher_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index 933cc105527..b63c9e19e27 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.1.0 + +* Implements `supportsMode` and `supportsCloseForMode`. + ## 3.0.8 * Adds pub topics to package metadata. diff --git a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml index 77106bc48a7..08bf314b5e1 100644 --- a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: flutter: sdk: flutter - url_launcher_platform_interface: ^2.0.0 + url_launcher_platform_interface: ^2.2.0 url_launcher_windows: # When depending on this package from a real application you should use: # url_launcher_windows: ^x.y.z diff --git a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart index 41c403e56f8..790a45149be 100644 --- a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart +++ b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart @@ -45,4 +45,16 @@ class UrlLauncherWindows extends UrlLauncherPlatform { // Failure is handled via a PlatformException from `launchUrl`. return true; } + + @override + Future supportsMode(PreferredLaunchMode mode) async { + return mode == PreferredLaunchMode.platformDefault || + mode == PreferredLaunchMode.externalApplication; + } + + @override + Future supportsCloseForMode(PreferredLaunchMode mode) async { + // No supported mode is closeable. + return false; + } } diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index 6b17b9d7c55..118d928dab3 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_windows description: Windows implementation of the url_launcher plugin. repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 3.0.8 +version: 3.1.0 environment: sdk: ">=2.19.0 <4.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - url_launcher_platform_interface: ^2.0.3 + url_launcher_platform_interface: ^2.2.0 dev_dependencies: flutter_test: diff --git a/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart b/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart index 7f48f64fa92..0be939bad19 100644 --- a/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart +++ b/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart @@ -77,6 +77,45 @@ void main() { expect(api.argument, 'http://example.com/'); }); }); + + group('supportsMode', () { + test('returns true for platformDefault', () async { + final UrlLauncherWindows launcher = UrlLauncherWindows(api: api); + expect(await launcher.supportsMode(PreferredLaunchMode.platformDefault), + true); + }); + + test('returns true for external application', () async { + final UrlLauncherWindows launcher = UrlLauncherWindows(api: api); + expect( + await launcher.supportsMode(PreferredLaunchMode.externalApplication), + true); + }); + + test('returns false for other modes', () async { + final UrlLauncherWindows launcher = UrlLauncherWindows(api: api); + expect( + await launcher + .supportsMode(PreferredLaunchMode.externalNonBrowserApplication), + false); + expect(await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView), + false); + expect( + await launcher.supportsMode(PreferredLaunchMode.inAppWebView), false); + }); + }); + + test('supportsCloseForMode returns false', () async { + final UrlLauncherWindows launcher = UrlLauncherWindows(api: api); + expect( + await launcher + .supportsCloseForMode(PreferredLaunchMode.platformDefault), + false); + expect( + await launcher + .supportsCloseForMode(PreferredLaunchMode.externalApplication), + false); + }); } class _FakeUrlLauncherApi implements UrlLauncherApi {