diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index cebc8177291b..0f3c087d8f83 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,18 @@ +## 6.2.0 + +* Adds `supportsLaunchMode` for checking whether the current platform supports a + given launch mode, to allow clients that will only work with specific modes + to avoid fallback to a different mode. +* Adds `supportsCloseForLaunchMode` to allow checking programatically if a + launched URL will be able to be closed. Previously the documented behvaior was + that it worked only with the `inAppWebView` launch mode, but this is no longer + true on all platforms with the addition of `inAppBrowserView`. +* Updates the documention for `launchUrl` to clarify that clients should not + rely on any specific behavior of the `platformDefault` launch mode. Changes + to the handling of `platformDefault`, such as Android's recent change from + `inAppWebView` to the new `inAppBrowserView`, are not considered breaking. +* Updates minimum supported SDK version to Flutter 3.13. + ## 6.1.14 * Updates documentation to mention support for Android Custom Tabs. diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index afd60143a54b..1eeee6da4fe3 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -66,7 +66,9 @@ See [`-[UIApplication canOpenURL:]`](https://developer.apple.com/documentation/u Add any URL schemes passed to `canLaunchUrl` as `` entries in your `AndroidManifest.xml`, otherwise it will return false in most cases starting -on Android 11 (API 30) or higher. A `` +on Android 11 (API 30) or higher. Checking for +`supportsLaunchMode(PreferredLaunchMode.inAppBrowserView)` also requires +a `` entry to return anything but false. A `` element must be added to your manifest as a child of the root element. Example: @@ -85,6 +87,10 @@ Example: + + + + ``` @@ -210,10 +216,16 @@ if (!await launchUrl(uri)) { If you need to access files outside of your application's sandbox, you will need to have the necessary [entitlements](https://docs.flutter.dev/desktop#entitlements-and-the-app-sandbox). -## Browser vs in-app Handling +## Browser vs in-app handling On some platforms, web URLs can be launched either in an in-app web view, or in the default browser. The default behavior depends on the platform (see [`launchUrl`](https://pub.dev/documentation/url_launcher/latest/url_launcher/launchUrl.html) for details), but a specific mode can be used on supported platforms by -passing a `LaunchMode`. +passing a `PreferredLaunchMode`. + +Platforms that do no support a requested `PreferredLaunchMode` will +automatically fall back to a supported mode (usually `platformDefault`). If +your application needs to avoid that fallback behavior, however, you can check +if the current platform supports a given mode with `supportsLaunchMode` before +calling `launchUrl`. diff --git a/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml b/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml index fe01f2fba9a8..5cfc75883726 100644 --- a/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml +++ b/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml @@ -20,6 +20,10 @@ + + + + diff --git a/packages/url_launcher/url_launcher/example/lib/main.dart b/packages/url_launcher/url_launcher/example/lib/main.dart index 57a0ce9ef470..40307a3f715d 100644 --- a/packages/url_launcher/url_launcher/example/lib/main.dart +++ b/packages/url_launcher/url_launcher/example/lib/main.dart @@ -62,7 +62,13 @@ class _MyHomePageState extends State { } } - Future _launchInWebViewOrVC(Uri url) async { + Future _launchInBrowserView(Uri url) async { + if (!await launchUrl(url, mode: LaunchMode.inAppBrowserView)) { + throw Exception('Could not launch $url'); + } + } + + Future _launchInWebView(Uri url) async { if (!await launchUrl(url, mode: LaunchMode.inAppWebView)) { throw Exception('Could not launch $url'); } @@ -99,7 +105,7 @@ class _MyHomePageState extends State { } } - Future _launchUniversalLinkIos(Uri url) async { + Future _launchUniversalLinkIOS(Uri url) async { final bool nativeAppLaunchSucceeded = await launchUrl( url, mode: LaunchMode.externalNonBrowserApplication, @@ -107,7 +113,7 @@ class _MyHomePageState extends State { if (!nativeAppLaunchSucceeded) { await launchUrl( url, - mode: LaunchMode.inAppWebView, + mode: LaunchMode.inAppBrowserView, ); } } @@ -173,7 +179,7 @@ class _MyHomePageState extends State { const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInWebViewOrVC(toLaunch); + _launched = _launchInBrowserView(toLaunch); }), child: const Text('Launch in app'), ), @@ -198,7 +204,7 @@ class _MyHomePageState extends State { const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( onPressed: () => setState(() { - _launched = _launchUniversalLinkIos(toLaunch); + _launched = _launchUniversalLinkIOS(toLaunch); }), child: const Text( 'Launch a universal link in a native app, fallback to Safari.(Youtube)'), @@ -206,7 +212,7 @@ class _MyHomePageState extends State { const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInWebViewOrVC(toLaunch); + _launched = _launchInWebView(toLaunch); Timer(const Duration(seconds: 5), () { closeInAppWebView(); }); diff --git a/packages/url_launcher/url_launcher/example/pubspec.yaml b/packages/url_launcher/url_launcher/example/pubspec.yaml index e09df486b417..18cc9c450443 100644 --- a/packages/url_launcher/url_launcher/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the url_launcher plugin. publish_to: none environment: - sdk: ">=3.0.0 <4.0.0" - flutter: ">=3.10.0" + sdk: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher/lib/src/link.dart b/packages/url_launcher/url_launcher/lib/src/link.dart index cea6845b12ea..b5c47403097f 100644 --- a/packages/url_launcher/url_launcher/lib/src/link.dart +++ b/packages/url_launcher/url_launcher/lib/src/link.dart @@ -126,7 +126,7 @@ class DefaultLinkDelegate extends StatelessWidget { success = await launchUrl( url, mode: _useWebView - ? LaunchMode.inAppWebView + ? LaunchMode.inAppBrowserView : LaunchMode.externalApplication, ); } on PlatformException { diff --git a/packages/url_launcher/url_launcher/lib/src/type_conversion.dart b/packages/url_launcher/url_launcher/lib/src/type_conversion.dart index 970f04dced57..3169e25dfa7a 100644 --- a/packages/url_launcher/url_launcher/lib/src/type_conversion.dart +++ b/packages/url_launcher/url_launcher/lib/src/type_conversion.dart @@ -22,6 +22,8 @@ PreferredLaunchMode convertLaunchMode(LaunchMode mode) { switch (mode) { case LaunchMode.platformDefault: return PreferredLaunchMode.platformDefault; + case LaunchMode.inAppBrowserView: + return PreferredLaunchMode.inAppBrowserView; case LaunchMode.inAppWebView: return PreferredLaunchMode.inAppWebView; case LaunchMode.externalApplication: diff --git a/packages/url_launcher/url_launcher/lib/src/types.dart b/packages/url_launcher/url_launcher/lib/src/types.dart index 359e293ef82e..2bf56e1b5d8b 100644 --- a/packages/url_launcher/url_launcher/lib/src/types.dart +++ b/packages/url_launcher/url_launcher/lib/src/types.dart @@ -14,9 +14,12 @@ enum LaunchMode { /// implementation. platformDefault, - /// Loads the URL in an in-app web view (e.g., Android Custom Tabs, Safari View Controller). + /// Loads the URL in an in-app web view (e.g., Android WebView). inAppWebView, + /// Loads the URL in an in-app web view (e.g., Android Custom Tabs, SFSafariViewController). + inAppBrowserView, + /// Passes the URL to the OS to be handled by another application. externalApplication, diff --git a/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart b/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart index 45193ff17cb3..ca0bcc6109d0 100644 --- a/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart +++ b/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart @@ -25,7 +25,8 @@ Future launchUrlString( WebViewConfiguration webViewConfiguration = const WebViewConfiguration(), String? webOnlyWindowName, }) async { - if (mode == LaunchMode.inAppWebView && + if ((mode == LaunchMode.inAppWebView || + mode == LaunchMode.inAppBrowserView) && !(urlString.startsWith('https:') || urlString.startsWith('http:'))) { throw ArgumentError.value(urlString, 'urlString', 'To use an in-app web view, you must provide an http(s) URL.'); diff --git a/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart b/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart index b3ce6c279f39..062621656932 100644 --- a/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart +++ b/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart @@ -11,25 +11,13 @@ import 'type_conversion.dart'; /// Passes [url] to the underlying platform for handling. /// -/// [mode] support varies significantly by platform: -/// - [LaunchMode.platformDefault] is supported on all platforms: -/// - On iOS and Android, this treats web URLs as -/// [LaunchMode.inAppWebView], and all other URLs as -/// [LaunchMode.externalApplication]. -/// - On Windows, macOS, and Linux this behaves like -/// [LaunchMode.externalApplication]. -/// - On web, this uses `webOnlyWindowName` for web URLs, and behaves like -/// [LaunchMode.externalApplication] for any other content. -/// - [LaunchMode.inAppWebView] is currently only supported on iOS and -/// Android. If a non-web URL is passed with this mode, an [ArgumentError] -/// will be thrown. -/// - [LaunchMode.externalApplication] is supported on all platforms. -/// On iOS, this should be used in cases where sharing the cookies of the -/// user's browser is important, such as SSO flows, since Safari View -/// Controller does not share the browser's context. -/// - [LaunchMode.externalNonBrowserApplication] is supported on iOS 10+. -/// This setting is used to require universal links to open in a non-browser -/// application. +/// [mode] support varies significantly by platform. Clients can use +/// [supportsLaunchMode] to query for support, but platforms will fall back to +/// other modes if the requested mode is not supported, so checking is not +/// required. The default behavior of [LaunchMode.platformDefault] is up to each +/// platform, and its behavior for a given platform may change over time as new +/// modes are supported, so clients that want a specific mode should request it +/// rather than rely on any currently observed default behavior. /// /// For web, [webOnlyWindowName] specifies a target for the launch. This /// supports the standard special link target names. For example: @@ -45,7 +33,8 @@ Future launchUrl( WebViewConfiguration webViewConfiguration = const WebViewConfiguration(), String? webOnlyWindowName, }) async { - if (mode == LaunchMode.inAppWebView && + if ((mode == LaunchMode.inAppWebView || + mode == LaunchMode.inAppBrowserView) && !(url.scheme == 'https' || url.scheme == 'http')) { throw ArgumentError.value(url, 'url', 'To use an in-app web view, you must provide an http(s) URL.'); @@ -81,8 +70,26 @@ Future canLaunchUrl(Uri url) async { /// Closes the current in-app web view, if one was previously opened by /// [launchUrl]. /// -/// If [launchUrl] was never called with [LaunchMode.inAppWebView], then this -/// call will have no effect. +/// This works only if [supportsCloseForLaunchMode] returns true for the mode +/// that was used by [launchUrl]. Future closeInAppWebView() async { return UrlLauncherPlatform.instance.closeWebView(); } + +/// Returns true if [mode] is supported by the current platform implementation. +/// +/// Calling [launchUrl] with an unsupported mode will fall back to a supported +/// mode, so calling this method is only necessary for cases where the caller +/// needs to know which mode will be used. +Future supportsLaunchMode(PreferredLaunchMode mode) { + return UrlLauncherPlatform.instance.supportsMode(mode); +} + +/// Returns true if [closeInAppWebView] is supported for [mode] in the current +/// platform implementation. +/// +/// If this returns false, [closeInAppWebView] will not work when launching +/// URLs with [mode]. +Future supportsCloseForLaunchMode(PreferredLaunchMode mode) { + return UrlLauncherPlatform.instance.supportsMode(mode); +} diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 9feae723f2f1..7546e6bf7de4 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.1.14 +version: 6.2.0 environment: - sdk: ">=3.0.0 <4.0.0" - flutter: ">=3.10.0" + sdk: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" flutter: plugin: @@ -28,15 +28,15 @@ flutter: dependencies: flutter: sdk: flutter - url_launcher_android: ^6.0.13 - url_launcher_ios: ^6.0.13 + url_launcher_android: ^6.2.0 + url_launcher_ios: ^6.2.0 # Allow either the pure-native or Dart/native hybrid versions of the desktop # implementations, as both are compatible. - url_launcher_linux: ">=2.0.0 <4.0.0" - url_launcher_macos: ">=2.0.0 <4.0.0" - url_launcher_platform_interface: ^2.1.0 - url_launcher_web: ^2.0.0 - url_launcher_windows: ">=2.0.0 <4.0.0" + url_launcher_linux: ^3.1.0 + url_launcher_macos: ^3.1.0 + url_launcher_platform_interface: ^2.2.0 + url_launcher_web: ^2.2.0 + url_launcher_windows: ^3.1.0 dev_dependencies: flutter_test: diff --git a/packages/url_launcher/url_launcher/test/link_test.dart b/packages/url_launcher/url_launcher/test/link_test.dart index 1585420d9b29..052ca2556e39 100644 --- a/packages/url_launcher/url_launcher/test/link_test.dart +++ b/packages/url_launcher/url_launcher/test/link_test.dart @@ -86,7 +86,7 @@ void main() { mock ..setLaunchExpectations( url: 'http://example.com/foobar', - launchMode: PreferredLaunchMode.inAppWebView, + launchMode: PreferredLaunchMode.inAppBrowserView, universalLinksOnly: false, enableJavaScript: true, enableDomStorage: true, diff --git a/packages/url_launcher/url_launcher/test/mocks/mock_url_launcher_platform.dart b/packages/url_launcher/url_launcher/test/mocks/mock_url_launcher_platform.dart index 05c8b5e4b375..fc0181d4a4ed 100644 --- a/packages/url_launcher/url_launcher/test/mocks/mock_url_launcher_platform.dart +++ b/packages/url_launcher/url_launcher/test/mocks/mock_url_launcher_platform.dart @@ -107,4 +107,16 @@ class MockUrlLauncher extends Fake Future closeWebView() async { closeWebViewCalled = true; } + + @override + Future supportsMode(PreferredLaunchMode mode) async { + launchMode = mode; + return response!; + } + + @override + Future supportsCloseForMode(PreferredLaunchMode mode) async { + launchMode = mode; + return response!; + } } diff --git a/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart b/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart index d71d07fc8fc4..0e6e76c38ea4 100644 --- a/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart +++ b/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart @@ -248,4 +248,40 @@ void main() { expect(await launchUrl(emailLaunchUrl), isTrue); }); }); + + group('supportsLaunchMode', () { + test('handles returning true', () async { + const PreferredLaunchMode mode = PreferredLaunchMode.inAppBrowserView; + mock.setResponse(true); + + expect(await supportsLaunchMode(mode), true); + expect(mock.launchMode, mode); + }); + + test('handles returning false', () async { + const PreferredLaunchMode mode = PreferredLaunchMode.inAppBrowserView; + mock.setResponse(false); + + expect(await supportsLaunchMode(mode), false); + expect(mock.launchMode, mode); + }); + }); + + group('supportsCloseForLaunchMode', () { + test('handles returning true', () async { + const PreferredLaunchMode mode = PreferredLaunchMode.inAppBrowserView; + mock.setResponse(true); + + expect(await supportsCloseForLaunchMode(mode), true); + expect(mock.launchMode, mode); + }); + + test('handles returning false', () async { + const PreferredLaunchMode mode = PreferredLaunchMode.inAppBrowserView; + mock.setResponse(false); + + expect(await supportsCloseForLaunchMode(mode), false); + expect(mock.launchMode, mode); + }); + }); }