[url_launcher] Replace primary APIs with cleaner versions (#5310)
diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md
index 0c38f48..b1ebc4b 100644
--- a/packages/url_launcher/url_launcher/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher/CHANGELOG.md
@@ -1,6 +1,19 @@
-## NEXT
+## 6.1.0
+* Introduces new `launchUrl` and `canLaunchUrl` APIs; `launch` and `canLaunch`
+ are now deprecated. These new APIs:
+ * replace the `String` URL argument with a `Uri`, to prevent common issues
+ with providing invalid URL strings.
+ * replace `forceSafariVC` and `forceWebView` with `LaunchMode`, which makes
+ the API platform-neutral, and standardizes the default behavior between
+ Android and iOS.
+ * move web view configuration options into a new `WebViewConfiguration`
+ object. The default behavior for JavaScript and DOM storage is now enabled
+ rather than disabled.
+* Also deprecates `closeWebView` in favor of `closeInAppWebView` to clarify
+ that it is specific to the in-app web view launch option.
* Adds OS version support information to README.
+* Reorganizes and clarifies README.
## 6.0.20
diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md
index 7f8699e..0cdbe1b 100644
--- a/packages/url_launcher/url_launcher/README.md
+++ b/packages/url_launcher/url_launcher/README.md
@@ -18,14 +18,14 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
-const String _url = 'https://flutter.dev';
+final Uri _url = Uri.parse('https://flutter.dev');
void main() => runApp(
const MaterialApp(
home: Material(
child: Center(
child: RaisedButton(
- onPressed: _launchURL,
+ onPressed: _launchUrl,
child: Text('Show Flutter homepage'),
),
),
@@ -33,8 +33,8 @@
),
);
-void _launchURL() async {
- if (!await launch(_url)) throw 'Could not launch $_url';
+void _launchUrl() async {
+ if (!await launchUrl(_url)) throw 'Could not launch $_url';
}
```
@@ -43,7 +43,7 @@
## Configuration
### iOS
-Add any URL schemes passed to `canLaunch` as `LSApplicationQueriesSchemes` entries in your Info.plist file.
+Add any URL schemes passed to `canLaunchUrl` as `LSApplicationQueriesSchemes` entries in your Info.plist file.
Example:
```
@@ -59,7 +59,7 @@
### Android
Starting from API 30 Android requires package visibility configuration in your
-`AndroidManifest.xml` otherwise `canLaunch` will return `false`. A `<queries>`
+`AndroidManifest.xml` otherwise `canLaunchUrl` will return `false`. A `<queries>`
element must be added to your manifest as a child of the root element.
The snippet below shows an example for an application that uses `https`, `tel`,
@@ -94,34 +94,53 @@
## Supported URL schemes
-The [`launch`](https://pub.dev/documentation/url_launcher/latest/url_launcher/launch.html) method
-takes a string argument containing a URL. This URL
-can be formatted using a number of different URL schemes. The supported
-URL schemes depend on the underlying platform and installed apps.
+The provided URL is passed directly to the host platform for handling. The
+supported URL schemes therefore depend on the platform and installed apps.
Commonly used schemes include:
| Scheme | Example | Action |
|:---|:---|:---|
-| `https:<URL>` | `https://flutter.dev` | Open URL in the default browser |
-| `mailto:<email address>?subject=<subject>&body=<body>` | `mailto:smith@example.org?subject=News&body=New%20plugin` | Create email to <email address> in the default email app |
-| `tel:<phone number>` | `tel:+1-555-010-999` | Make a phone call to <phone number> using the default phone app |
-| `sms:<phone number>` | `sms:5550101234` | Send an SMS message to <phone number> using the default messaging app |
+| `https:<URL>` | `https://flutter.dev` | Open `<URL>` in the default browser |
+| `mailto:<email address>?subject=<subject>&body=<body>` | `mailto:smith@example.org?subject=News&body=New%20plugin` | Create email to `<email address>` in the default email app |
+| `tel:<phone number>` | `tel:+1-555-010-999` | Make a phone call to `<phone number>` using the default phone app |
+| `sms:<phone number>` | `sms:5550101234` | Send an SMS message to `<phone number>` using the default messaging app |
| `file:<path>` | `file:/home` | Open file or folder using default app association, supported on desktop platforms |
More details can be found here for [iOS](https://developer.apple.com/library/content/featuredarticles/iPhoneURLScheme_Reference/Introduction/Introduction.html)
and [Android](https://developer.android.com/guide/components/intents-common.html)
-**Note**: URL schemes are only supported if there are apps installed on the device that can
+URL schemes are only supported if there are apps installed on the device that can
support them. For example, iOS simulators don't have a default email or phone
apps installed, so can't open `tel:` or `mailto:` links.
+### Checking supported schemes
+
+If you need to know at runtime whether a scheme is guaranteed to work before
+using it (for instance, to adjust your UI based on what is available), you can
+check with [`canLaunchUrl`](https://pub.dev/documentation/url_launcher/latest/url_launcher/canLaunchUrl.html).
+
+However, `canLaunchUrl` can return false even if `launchUrl` would work in
+some circumstances (in web applications, on mobile without the necessary
+configuration as described above, etc.), so in cases where you can provide
+fallback behavior it is better to use `launchUrl` directly and handle failure.
+For example, a UI button that would have sent feedback email using a `mailto` URL
+might instead open a web-based feedback form using an `https` URL on failure,
+rather than disabling the button if `canLaunchUrl` returns false for `mailto`.
+
### Encoding URLs
URLs must be properly encoded, especially when including spaces or other special
-characters. This can be done using the
+characters. In general this is handled automatically by the
[`Uri` class](https://api.dart.dev/dart-core/Uri-class.html).
-For example:
+
+**However**, for any scheme other than `http` or `https`, you should use the
+`query` parameter and the `encodeQueryParameters` function shown below rather
+than `Uri`'s `queryParameters` constructor argument for any query parameters,
+due to [a bug](https://github.com/dart-lang/sdk/issues/43838) in the way `Uri`
+encodes query parameters. Using `queryParameters` will result in spaces being
+converted to `+` in many cases.
+
```dart
String? encodeQueryParameters(Map<String, String> params) {
return params.entries
@@ -137,43 +156,24 @@
}),
);
-launch(emailLaunchUri.toString());
+launchUrl(emailLaunchUri);
```
-**Warning**: For any scheme other than `http` or `https`, you should use the
-`query` parameter and the `encodeQueryParameters` function shown above rather
-than `Uri`'s `queryParameters` constructor argument, due to
-[a bug](https://github.com/dart-lang/sdk/issues/43838) in the way `Uri`
-encodes query parameters. Using `queryParameters` will result in spaces being
-converted to `+` in many cases.
+### URLs not handled by `Uri`
-### Handling missing URL receivers
+In rare cases, you may need to launch a URL that the host system considers
+valid, but cannot be expressed by `Uri`. For those cases, alternate APIs using
+strings are available by importing `url_launcher_string.dart`.
-A particular mobile device may not be able to receive all supported URL schemes.
-For example, a tablet may not have a cellular radio and thus no support for
-launching a URL using the `sms` scheme, or a device may not have an email app
-and thus no support for launching a URL using the `mailto` scheme.
+Using these APIs in any other cases is **strongly discouraged**, as providing
+invalid URL strings was a very common source of errors with this plugin's
+original APIs.
-We recommend checking which URL schemes are supported using the
-[`canLaunch`](https://pub.dev/documentation/url_launcher/latest/url_launcher/canLaunch.html)
-in most cases. If the `canLaunch` method returns false, as a
-best practice we suggest adjusting the application UI so that the unsupported
-URL is never triggered; for example, if the `mailto` scheme is not supported, a
-UI button that would have sent feedback email could be changed to instead open
-a web-based feedback form using an `https` URL.
+### File scheme handling
-## Browser vs In-app Handling
-By default, Android opens up a browser when handling URLs. You can pass
-`forceWebView: true` parameter to tell the plugin to open a WebView instead.
-If you do this for a URL of a page containing JavaScript, make sure to pass in
-`enableJavaScript: true`, or else the launch method will not work properly. On
-iOS, the default behavior is to open all web URLs within the app. Everything
-else is redirected to the app handler.
+`file:` scheme can be used on desktop platforms: Windows, macOS, and Linux.
-## File scheme handling
-`file:` scheme can be used on desktop platforms: `macOS`, `Linux` and `Windows`.
-
-We recommend checking first whether the directory or file exists before calling `launch`.
+We recommend checking first whether the directory or file exists before calling `launchUrl`.
Example:
```dart
@@ -181,13 +181,21 @@
final Uri uri = Uri.file(filePath);
if (await File(uri.toFilePath()).exists()) {
- if (!await launch(uri.toString())) {
+ if (!await launchUrl(uri)) {
throw 'Could not launch $uri';
}
}
```
-### macOS file access configuration
+#### macOS file access configuration
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
+
+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`.
diff --git a/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart
index b527c22..51c2ec8 100644
--- a/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart
+++ b/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart
@@ -13,18 +13,23 @@
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('canLaunch', (WidgetTester _) async {
- expect(await canLaunch('randomstring'), false);
+ expect(
+ await canLaunchUrl(Uri(scheme: 'randomscheme', path: 'a_path')), false);
// Generally all devices should have some default browser.
- expect(await canLaunch('http://flutter.dev'), true);
- expect(await canLaunch('https://www.google.com/404'), true);
+ expect(await canLaunchUrl(Uri(scheme: 'http', host: 'flutter.dev')), true);
+ expect(await canLaunchUrl(Uri(scheme: 'https', host: 'flutter.dev')), true);
// SMS handling is available by default on most platforms.
if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) {
- expect(await canLaunch('sms:5555555555'), true);
+ expect(await canLaunchUrl(Uri(scheme: 'sms', path: '5555555555')), true);
}
- // tel: and mailto: links may not be openable on every device. iOS
- // simulators notably can't open these link types.
+ // Sanity-check legacy API.
+ // ignore: deprecated_member_use
+ expect(await canLaunch('randomstring'), false);
+ // Generally all devices should have some default browser.
+ // ignore: deprecated_member_use
+ expect(await canLaunch('https://flutter.dev'), true);
});
}
diff --git a/packages/url_launcher/url_launcher/example/lib/main.dart b/packages/url_launcher/url_launcher/example/lib/main.dart
index a5e38ce..898e806 100644
--- a/packages/url_launcher/url_launcher/example/lib/main.dart
+++ b/packages/url_launcher/url_launcher/example/lib/main.dart
@@ -44,67 +44,62 @@
void initState() {
super.initState();
// Check for phone call support.
- canLaunch('tel:123').then((bool result) {
+ canLaunchUrl(Uri(scheme: 'tel', path: '123')).then((bool result) {
setState(() {
_hasCallSupport = result;
});
});
}
- Future<void> _launchInBrowser(String url) async {
- if (!await launch(
+ Future<void> _launchInBrowser(Uri url) async {
+ if (!await launchUrl(
url,
- forceSafariVC: false,
- forceWebView: false,
- headers: <String, String>{'my_header_key': 'my_header_value'},
+ mode: LaunchMode.externalApplication,
)) {
throw 'Could not launch $url';
}
}
- Future<void> _launchInWebViewOrVC(String url) async {
- if (!await launch(
+ Future<void> _launchInWebViewOrVC(Uri url) async {
+ if (!await launchUrl(
url,
- forceSafariVC: true,
- forceWebView: true,
- headers: <String, String>{'my_header_key': 'my_header_value'},
+ mode: LaunchMode.inAppWebView,
+ webViewConfiguration: const WebViewConfiguration(
+ headers: <String, String>{'my_header_key': 'my_header_value'}),
)) {
throw 'Could not launch $url';
}
}
- Future<void> _launchInWebViewWithJavaScript(String url) async {
- if (!await launch(
+ Future<void> _launchInWebViewWithoutJavaScript(Uri url) async {
+ if (!await launchUrl(
url,
- forceSafariVC: true,
- forceWebView: true,
- enableJavaScript: true,
+ mode: LaunchMode.inAppWebView,
+ webViewConfiguration: const WebViewConfiguration(enableJavaScript: false),
)) {
throw 'Could not launch $url';
}
}
- Future<void> _launchInWebViewWithDomStorage(String url) async {
- if (!await launch(
+ Future<void> _launchInWebViewWithoutDomStorage(Uri url) async {
+ if (!await launchUrl(
url,
- forceSafariVC: true,
- forceWebView: true,
- enableDomStorage: true,
+ mode: LaunchMode.inAppWebView,
+ webViewConfiguration: const WebViewConfiguration(enableDomStorage: false),
)) {
throw 'Could not launch $url';
}
}
- Future<void> _launchUniversalLinkIos(String url) async {
- final bool nativeAppLaunchSucceeded = await launch(
+ Future<void> _launchUniversalLinkIos(Uri url) async {
+ final bool nativeAppLaunchSucceeded = await launchUrl(
url,
- forceSafariVC: false,
- universalLinksOnly: true,
+ mode: LaunchMode.externalNonBrowserApplication,
);
if (!nativeAppLaunchSucceeded) {
- await launch(
+ await launchUrl(
url,
- forceSafariVC: true,
+ mode: LaunchMode.inAppWebView,
);
}
}
@@ -118,22 +113,19 @@
}
Future<void> _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.
final Uri launchUri = Uri(
scheme: 'tel',
path: phoneNumber,
);
- await launch(launchUri.toString());
+ await launchUrl(launchUri);
}
@override
Widget build(BuildContext context) {
// onPressed calls using this URL are not gated on a 'canLaunch' check
// because the assumption is that every device can launch a web URL.
- const String toLaunch = 'https://www.cylog.org/headers/';
+ final Uri toLaunch =
+ Uri(scheme: 'https', host: 'www.cylog.org', path: 'headers/');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
@@ -160,9 +152,9 @@
? const Text('Make phone call')
: const Text('Calling not supported'),
),
- const Padding(
- padding: EdgeInsets.all(16.0),
- child: Text(toLaunch),
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Text(toLaunch.toString()),
),
ElevatedButton(
onPressed: () => setState(() {
@@ -179,15 +171,15 @@
),
ElevatedButton(
onPressed: () => setState(() {
- _launched = _launchInWebViewWithJavaScript(toLaunch);
+ _launched = _launchInWebViewWithoutJavaScript(toLaunch);
}),
- child: const Text('Launch in app(JavaScript ON)'),
+ child: const Text('Launch in app (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 app (DOM storage OFF)'),
),
const Padding(padding: EdgeInsets.all(16.0)),
ElevatedButton(
@@ -203,7 +195,7 @@
_launched = _launchInWebViewOrVC(toLaunch);
Timer(const Duration(seconds: 5), () {
print('Closing WebView after 5 seconds...');
- closeWebView();
+ closeInAppWebView();
});
}),
child: const Text('Launch in app + close after 5 seconds'),
diff --git a/packages/url_launcher/url_launcher/lib/src/legacy_api.dart b/packages/url_launcher/url_launcher/lib/src/legacy_api.dart
new file mode 100644
index 0000000..a61b200
--- /dev/null
+++ b/packages/url_launcher/url_launcher/lib/src/legacy_api.dart
@@ -0,0 +1,154 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
+
+/// Parses the specified URL string and delegates handling of it to the
+/// underlying platform.
+///
+/// The returned future completes with a [PlatformException] on invalid URLs and
+/// schemes which cannot be handled, that is when [canLaunch] would complete
+/// with false.
+///
+/// By default when [forceSafariVC] is unset, the launcher
+/// opens web URLs in the Safari View Controller, anything else is opened
+/// using the default handler on the platform. If set to true, it opens the
+/// URL in the Safari View Controller. If false, the URL is opened in the
+/// default browser of the phone. Note that to work with universal links on iOS,
+/// this must be set to false to let the platform's system handle the URL.
+/// Set this to false if you want to use the cookies/context of the main browser
+/// of the app (such as SSO flows). This setting will nullify [universalLinksOnly]
+/// and will always launch a web content in the built-in Safari View Controller regardless
+/// if the url is a universal link or not.
+///
+/// [universalLinksOnly] is only used in iOS with iOS version >= 10.0. This setting is only validated
+/// when [forceSafariVC] is set to false. The default value of this setting is false.
+/// By default (when unset), the launcher will either launch the url in a browser (when the
+/// url is not a universal link), or launch the respective native app content (when
+/// the url is a universal link). When set to true, the launcher will only launch
+/// the content if the url is a universal link and the respective app for the universal
+/// link is installed on the user's device; otherwise throw a [PlatformException].
+///
+/// [forceWebView] is an Android only setting. If null or false, the URL is
+/// always launched with the default browser on device. If set to true, the URL
+/// is launched in a WebView. Unlike iOS, browser context is shared across
+/// WebViews.
+/// [enableJavaScript] is an Android only setting. If true, WebView enable
+/// javascript.
+/// [enableDomStorage] is an Android only setting. If true, WebView enable
+/// DOM storage.
+/// [headers] is an Android only setting that adds headers to the WebView.
+/// When not using a WebView, the header information is passed to the browser,
+/// some Android browsers do not support the [Browser.EXTRA_HEADERS](https://developer.android.com/reference/android/provider/Browser#EXTRA_HEADERS)
+/// intent extra and the header information will be lost.
+/// [webOnlyWindowName] is an Web only setting . _blank opens the new url in new tab ,
+/// _self opens the new url in current tab.
+/// Default behaviour is to open the url in new tab.
+///
+/// Note that if any of the above are set to true but the URL is not a web URL,
+/// this will throw a [PlatformException].
+///
+/// [statusBarBrightness] Sets the status bar brightness of the application
+/// after opening a link on iOS. Does nothing if no value is passed. This does
+/// not handle resetting the previous status bar style.
+///
+/// Returns true if launch url is successful; false is only returned when [universalLinksOnly]
+/// is set to true and the universal link failed to launch.
+@Deprecated('Use launchUrl instead')
+Future<bool> launch(
+ String urlString, {
+ bool? forceSafariVC,
+ bool forceWebView = false,
+ bool enableJavaScript = false,
+ bool enableDomStorage = false,
+ bool universalLinksOnly = false,
+ Map<String, String> headers = const <String, String>{},
+ Brightness? statusBarBrightness,
+ String? webOnlyWindowName,
+}) async {
+ final Uri? url = Uri.tryParse(urlString.trimLeft());
+ final bool isWebURL =
+ url != null && (url.scheme == 'http' || url.scheme == 'https');
+
+ if ((forceSafariVC == true || forceWebView == true) && !isWebURL) {
+ throw PlatformException(
+ code: 'NOT_A_WEB_SCHEME',
+ message: 'To use webview or safariVC, you need to pass'
+ 'in a web URL. This $urlString is not a web URL.');
+ }
+
+ /// [true] so that ui is automatically computed if [statusBarBrightness] is set.
+ bool previousAutomaticSystemUiAdjustment = true;
+ if (statusBarBrightness != null &&
+ defaultTargetPlatform == TargetPlatform.iOS &&
+ _ambiguate(WidgetsBinding.instance) != null) {
+ previousAutomaticSystemUiAdjustment = _ambiguate(WidgetsBinding.instance)!
+ .renderView
+ .automaticSystemUiAdjustment;
+ _ambiguate(WidgetsBinding.instance)!
+ .renderView
+ .automaticSystemUiAdjustment = false;
+ SystemChrome.setSystemUIOverlayStyle(statusBarBrightness == Brightness.light
+ ? SystemUiOverlayStyle.dark
+ : SystemUiOverlayStyle.light);
+ }
+
+ final bool result = await UrlLauncherPlatform.instance.launch(
+ urlString,
+ useSafariVC: forceSafariVC ?? isWebURL,
+ useWebView: forceWebView,
+ enableJavaScript: enableJavaScript,
+ enableDomStorage: enableDomStorage,
+ universalLinksOnly: universalLinksOnly,
+ headers: headers,
+ webOnlyWindowName: webOnlyWindowName,
+ );
+
+ if (statusBarBrightness != null &&
+ _ambiguate(WidgetsBinding.instance) != null) {
+ _ambiguate(WidgetsBinding.instance)!
+ .renderView
+ .automaticSystemUiAdjustment = previousAutomaticSystemUiAdjustment;
+ }
+
+ return result;
+}
+
+/// Checks whether the specified URL can be handled by some app installed on the
+/// device.
+///
+/// On some systems, such as recent versions of Android and iOS, this will
+/// always return false unless the application has been configuration to allow
+/// querying the system for launch support. See
+/// [the README](https://pub.dev/packages/url_launcher#configuration) for
+/// details.
+@Deprecated('Use canLaunchUrl instead')
+Future<bool> canLaunch(String urlString) async {
+ return await UrlLauncherPlatform.instance.canLaunch(urlString);
+}
+
+/// Closes the current WebView, if one was previously opened via a call to [launch].
+///
+/// If [launch] was never called, then this call will not have any effect.
+///
+/// On Android systems, if [launch] was called without `forceWebView` being set to `true`
+/// Or on IOS systems, if [launch] was called without `forceSafariVC` being set to `true`,
+/// this call will not do anything either, simply because there is no
+/// WebView/SafariViewController available to be closed.
+@Deprecated('Use closeInAppWebView instead')
+Future<void> closeWebView() async {
+ return await UrlLauncherPlatform.instance.closeWebView();
+}
+
+/// This allows a value of type T or T? to be treated as a value of type T?.
+///
+/// We use this so that APIs that have become non-nullable can still be used
+/// with `!` and `?` on the stable branch.
+// TODO(ianh): Remove this once we roll stable in late 2021.
+T? _ambiguate<T>(T? value) => value;
diff --git a/packages/url_launcher/url_launcher/lib/src/link.dart b/packages/url_launcher/url_launcher/lib/src/link.dart
index 016f97d..76cb977 100644
--- a/packages/url_launcher/url_launcher/lib/src/link.dart
+++ b/packages/url_launcher/url_launcher/lib/src/link.dart
@@ -6,10 +6,12 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
-import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher_platform_interface/link.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
+import 'types.dart';
+import 'url_launcher_uri.dart';
+
/// The function used to push routes to the Flutter framework.
@visibleForTesting
Future<ByteData> Function(Object?, String) pushRouteToFrameworkFunction =
@@ -107,7 +109,8 @@
}
Future<void> _followLink(BuildContext context) async {
- if (!link.uri!.hasScheme) {
+ final Uri url = link.uri!;
+ if (!url.hasScheme) {
// A uri that doesn't have a scheme is an internal route name. In this
// case, we push it via Flutter's navigation system instead of letting the
// browser handle it.
@@ -116,18 +119,18 @@
return;
}
- // At this point, we know that the link is external. So we use the `launch`
- // API to open the link.
- final String urlString = link.uri.toString();
- if (await canLaunch(urlString)) {
- await launch(
- urlString,
- forceSafariVC: _useWebView,
- forceWebView: _useWebView,
+ // At this point, we know that the link is external. So we use the
+ // `launchUrl` API to open the link.
+ if (await canLaunchUrl(url)) {
+ await launchUrl(
+ url,
+ mode: _useWebView
+ ? LaunchMode.inAppWebView
+ : LaunchMode.externalApplication,
);
} else {
FlutterError.reportError(FlutterErrorDetails(
- exception: 'Could not launch link $urlString',
+ exception: 'Could not launch link ${url.toString()}',
stack: StackTrace.current,
library: 'url_launcher',
context: ErrorDescription('during launching a link'),
diff --git a/packages/url_launcher/url_launcher/lib/src/types.dart b/packages/url_launcher/url_launcher/lib/src/types.dart
new file mode 100644
index 0000000..bcfcb78
--- /dev/null
+++ b/packages/url_launcher/url_launcher/lib/src/types.dart
@@ -0,0 +1,54 @@
+// 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.
+
+import 'package:flutter/foundation.dart';
+
+/// The desired mode to launch a URL.
+///
+/// Support for these modes varies by platform. Platforms that do not support
+/// the requested mode may substitute another mode. See [launchUrl] for more
+/// details.
+enum LaunchMode {
+ /// Leaves the decision of how to launch the URL to the platform
+ /// implementation.
+ platformDefault,
+
+ /// Loads the URL in an in-app web view (e.g., Safari View Controller).
+ inAppWebView,
+
+ /// Passes the URL to the OS to be handled by another application.
+ externalApplication,
+
+ /// Passes the URL to the OS to be handled by another non-browser application.
+ externalNonBrowserApplication,
+}
+
+/// Additional configuration options for [LaunchMode.inAppWebView].
+@immutable
+class WebViewConfiguration {
+ /// Creates a new WebViewConfiguration with the given settings.
+ const WebViewConfiguration({
+ this.enableJavaScript = true,
+ this.enableDomStorage = true,
+ this.headers = const <String, String>{},
+ });
+
+ /// Whether or not JavaScript is enabled for the web content.
+ ///
+ /// Disabling this may not be supported on all platforms.
+ final bool enableJavaScript;
+
+ /// Whether or not DOM storage is enabled for the web content.
+ ///
+ /// Disabling this may not be supported on all platforms.
+ final bool enableDomStorage;
+
+ /// Additional headers to pass in the load request.
+ ///
+ /// On Android, this may work even when not loading in an in-app web view.
+ /// When loading in an external browsers, this sets
+ /// [Browser.EXTRA_HEADERS](https://developer.android.com/reference/android/provider/Browser#EXTRA_HEADERS)
+ /// Not all browsers support this, so it is not guaranteed to be honored.
+ final Map<String, String> headers;
+}
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
new file mode 100644
index 0000000..bee2a80
--- /dev/null
+++ b/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart
@@ -0,0 +1,65 @@
+// 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.
+
+import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
+
+import 'types.dart';
+
+/// String version of [launchUrl].
+///
+/// This should be used only in the very rare case of needing to launch a URL
+/// that is considered valid by the host platform, but not by Dart's [Uri]
+/// class. In all other cases, use [launchUrl] instead, as that will ensure
+/// that you are providing a valid URL.
+///
+/// The behavior of this method when passing an invalid URL is entirely
+/// platform-specific; no effort is made by the plugin to make the URL valid.
+/// Some platforms may provide best-effort interpretation of an invalid URL,
+/// others will immediately fail if the URL can't be parsed according to the
+/// official standards that define URL formats.
+Future<bool> launchUrlString(
+ String urlString, {
+ LaunchMode mode = LaunchMode.platformDefault,
+ WebViewConfiguration webViewConfiguration = const WebViewConfiguration(),
+ String? webOnlyWindowName,
+}) async {
+ final bool isWebURL =
+ urlString.startsWith('http:') || urlString.startsWith('https:');
+ if (mode == LaunchMode.inAppWebView && !isWebURL) {
+ throw ArgumentError.value(urlString, 'urlString',
+ 'To use an in-app web view, you must provide an http(s) URL.');
+ }
+ final bool useWebView = mode == LaunchMode.inAppWebView ||
+ (isWebURL && mode == LaunchMode.platformDefault);
+
+ // TODO(stuartmorgan): Create a replacement platform interface method that
+ // uses something more like the new argument structure, and switch to using
+ // that, to support launch mode on more platforms.
+ return await UrlLauncherPlatform.instance.launch(
+ urlString,
+ useSafariVC: useWebView,
+ useWebView: useWebView,
+ enableJavaScript: webViewConfiguration.enableJavaScript,
+ enableDomStorage: webViewConfiguration.enableDomStorage,
+ universalLinksOnly: mode == LaunchMode.externalNonBrowserApplication,
+ headers: webViewConfiguration.headers,
+ webOnlyWindowName: webOnlyWindowName,
+ );
+}
+
+/// String version of [canLaunchUrl].
+///
+/// This should be used only in the very rare case of needing to check a URL
+/// that is considered valid by the host platform, but not by Dart's [Uri]
+/// class. In all other cases, use [canLaunchUrl] instead, as that will ensure
+/// that you are providing a valid URL.
+///
+/// The behavior of this method when passing an invalid URL is entirely
+/// platform-specific; no effort is made by the plugin to make the URL valid.
+/// Some platforms may provide best-effort interpretation of an invalid URL,
+/// others will immediately fail if the URL can't be parsed according to the
+/// official standards that define URL formats.
+Future<bool> canLaunchUrlString(String urlString) async {
+ return await UrlLauncherPlatform.instance.canLaunch(urlString);
+}
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
new file mode 100644
index 0000000..1ca787f
--- /dev/null
+++ b/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart
@@ -0,0 +1,90 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:url_launcher/url_launcher_string.dart';
+import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
+
+import 'types.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.
+///
+/// For web, [webOnlyWindowName] specifies a target for the launch. This
+/// supports the standard special link target names. For example:
+/// - "_blank" opens the new URL in a new tab.
+/// - "_self" opens the new URL in the current tab.
+/// Default behaviour when unset is to open the url in a new tab.
+///
+/// Returns true if the URL was launched successful, otherwise either returns
+/// false or throws a [PlatformException] depending on the failure.
+Future<bool> launchUrl(
+ Uri url, {
+ LaunchMode mode = LaunchMode.platformDefault,
+ WebViewConfiguration webViewConfiguration = const WebViewConfiguration(),
+ String? webOnlyWindowName,
+}) async {
+ final bool isWebURL = url.scheme == 'http' || url.scheme == 'https';
+ if (mode == LaunchMode.inAppWebView && !isWebURL) {
+ throw ArgumentError.value(url, 'url',
+ 'To use an in-app web view, you must provide an http(s) URL.');
+ }
+ // TODO(stuartmorgan): Use UrlLauncherPlatform directly once a new API
+ // that better matches these parameters has been added. For now, delegate to
+ // launchUrlString so that there's only one copy of the parameter translation
+ // logic.
+ return await launchUrlString(
+ url.toString(),
+ mode: mode,
+ webViewConfiguration: webViewConfiguration,
+ webOnlyWindowName: webOnlyWindowName,
+ );
+}
+
+/// Checks whether the specified URL can be handled by some app installed on the
+/// device.
+///
+/// Returns true if it is possible to verify that there is a handler available.
+/// A false return value can indicate either that there is no handler available,
+/// or that the application does not have permission to check. For example:
+/// - On recent versions of Android and iOS, this will always return false
+/// unless the application has been configuration to allow
+/// querying the system for launch support. See
+/// [the README](https://pub.dev/packages/url_launcher#configuration) for
+/// details.
+/// - On web, this will always return false except for a few specific schemes
+/// that are always assumed to be supported (such as http(s)), as web pages
+/// are never allowed to query installed applications.
+Future<bool> canLaunchUrl(Uri url) async {
+ return await UrlLauncherPlatform.instance.canLaunch(url.toString());
+}
+
+/// 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.
+Future<void> closeInAppWebView() async {
+ return await UrlLauncherPlatform.instance.closeWebView();
+}
diff --git a/packages/url_launcher/url_launcher/lib/url_launcher.dart b/packages/url_launcher/url_launcher/lib/url_launcher.dart
index f28c460..36c7b60 100644
--- a/packages/url_launcher/url_launcher/lib/url_launcher.dart
+++ b/packages/url_launcher/url_launcher/lib/url_launcher.dart
@@ -2,150 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'dart:async';
-
-import 'package:flutter/foundation.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter/widgets.dart';
-import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
-
-/// Parses the specified URL string and delegates handling of it to the
-/// underlying platform.
-///
-/// The returned future completes with a [PlatformException] on invalid URLs and
-/// schemes which cannot be handled, that is when [canLaunch] would complete
-/// with false.
-///
-/// By default when [forceSafariVC] is unset, the launcher
-/// opens web URLs in the Safari View Controller, anything else is opened
-/// using the default handler on the platform. If set to true, it opens the
-/// URL in the Safari View Controller. If false, the URL is opened in the
-/// default browser of the phone. Note that to work with universal links on iOS,
-/// this must be set to false to let the platform's system handle the URL.
-/// Set this to false if you want to use the cookies/context of the main browser
-/// of the app (such as SSO flows). This setting will nullify [universalLinksOnly]
-/// and will always launch a web content in the built-in Safari View Controller regardless
-/// if the url is a universal link or not.
-///
-/// [universalLinksOnly] is only used in iOS with iOS version >= 10.0. This setting is only validated
-/// when [forceSafariVC] is set to false. The default value of this setting is false.
-/// By default (when unset), the launcher will either launch the url in a browser (when the
-/// url is not a universal link), or launch the respective native app content (when
-/// the url is a universal link). When set to true, the launcher will only launch
-/// the content if the url is a universal link and the respective app for the universal
-/// link is installed on the user's device; otherwise throw a [PlatformException].
-///
-/// [forceWebView] is an Android only setting. If null or false, the URL is
-/// always launched with the default browser on device. If set to true, the URL
-/// is launched in a WebView. Unlike iOS, browser context is shared across
-/// WebViews.
-/// [enableJavaScript] is an Android only setting. If true, WebView enable
-/// javascript.
-/// [enableDomStorage] is an Android only setting. If true, WebView enable
-/// DOM storage.
-/// [headers] is an Android only setting that adds headers to the WebView.
-/// When not using a WebView, the header information is passed to the browser,
-/// some Android browsers do not support the [Browser.EXTRA_HEADERS](https://developer.android.com/reference/android/provider/Browser#EXTRA_HEADERS)
-/// intent extra and the header information will be lost.
-/// [webOnlyWindowName] is an Web only setting . _blank opens the new url in new tab ,
-/// _self opens the new url in current tab.
-/// Default behaviour is to open the url in new tab.
-///
-/// Note that if any of the above are set to true but the URL is not a web URL,
-/// this will throw a [PlatformException].
-///
-/// [statusBarBrightness] Sets the status bar brightness of the application
-/// after opening a link on iOS. Does nothing if no value is passed. This does
-/// not handle resetting the previous status bar style.
-///
-/// Returns true if launch url is successful; false is only returned when [universalLinksOnly]
-/// is set to true and the universal link failed to launch.
-Future<bool> launch(
- String urlString, {
- bool? forceSafariVC,
- bool forceWebView = false,
- bool enableJavaScript = false,
- bool enableDomStorage = false,
- bool universalLinksOnly = false,
- Map<String, String> headers = const <String, String>{},
- Brightness? statusBarBrightness,
- String? webOnlyWindowName,
-}) async {
- final Uri? url = Uri.tryParse(urlString.trimLeft());
- final bool isWebURL =
- url != null && (url.scheme == 'http' || url.scheme == 'https');
-
- if ((forceSafariVC == true || forceWebView == true) && !isWebURL) {
- throw PlatformException(
- code: 'NOT_A_WEB_SCHEME',
- message: 'To use webview or safariVC, you need to pass'
- 'in a web URL. This $urlString is not a web URL.');
- }
-
- /// [true] so that ui is automatically computed if [statusBarBrightness] is set.
- bool previousAutomaticSystemUiAdjustment = true;
- if (statusBarBrightness != null &&
- defaultTargetPlatform == TargetPlatform.iOS &&
- _ambiguate(WidgetsBinding.instance) != null) {
- previousAutomaticSystemUiAdjustment = _ambiguate(WidgetsBinding.instance)!
- .renderView
- .automaticSystemUiAdjustment;
- _ambiguate(WidgetsBinding.instance)!
- .renderView
- .automaticSystemUiAdjustment = false;
- SystemChrome.setSystemUIOverlayStyle(statusBarBrightness == Brightness.light
- ? SystemUiOverlayStyle.dark
- : SystemUiOverlayStyle.light);
- }
-
- final bool result = await UrlLauncherPlatform.instance.launch(
- urlString,
- useSafariVC: forceSafariVC ?? isWebURL,
- useWebView: forceWebView,
- enableJavaScript: enableJavaScript,
- enableDomStorage: enableDomStorage,
- universalLinksOnly: universalLinksOnly,
- headers: headers,
- webOnlyWindowName: webOnlyWindowName,
- );
-
- if (statusBarBrightness != null &&
- _ambiguate(WidgetsBinding.instance) != null) {
- _ambiguate(WidgetsBinding.instance)!
- .renderView
- .automaticSystemUiAdjustment = previousAutomaticSystemUiAdjustment;
- }
-
- return result;
-}
-
-/// Checks whether the specified URL can be handled by some app installed on the
-/// device.
-///
-/// On some systems, such as recent versions of Android and iOS, this will
-/// always return false unless the application has been configuration to allow
-/// querying the system for launch support. See
-/// [the README](https://pub.dev/packages/url_launcher#configuration) for
-/// details.
-Future<bool> canLaunch(String urlString) async {
- return await UrlLauncherPlatform.instance.canLaunch(urlString);
-}
-
-/// Closes the current WebView, if one was previously opened via a call to [launch].
-///
-/// If [launch] was never called, then this call will not have any effect.
-///
-/// On Android systems, if [launch] was called without `forceWebView` being set to `true`
-/// Or on IOS systems, if [launch] was called without `forceSafariVC` being set to `true`,
-/// this call will not do anything either, simply because there is no
-/// WebView/SafariViewController available to be closed.
-Future<void> closeWebView() async {
- return await UrlLauncherPlatform.instance.closeWebView();
-}
-
-/// This allows a value of type T or T? to be treated as a value of type T?.
-///
-/// We use this so that APIs that have become non-nullable can still be used
-/// with `!` and `?` on the stable branch.
-// TODO(ianh): Remove this once we roll stable in late 2021.
-T? _ambiguate<T>(T? value) => value;
+export 'src/legacy_api.dart';
+export 'src/types.dart';
+export 'src/url_launcher_uri.dart';
diff --git a/packages/url_launcher/url_launcher/lib/url_launcher_string.dart b/packages/url_launcher/url_launcher/lib/url_launcher_string.dart
new file mode 100644
index 0000000..b5a12b1
--- /dev/null
+++ b/packages/url_launcher/url_launcher/lib/url_launcher_string.dart
@@ -0,0 +1,13 @@
+// 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.
+
+// Provides a String-based alterantive to the Uri-based primary API.
+//
+// This is provided as a separate import because it's much easier to use
+// incorrectly, so should require explicit opt-in (to avoid issues such as
+// IDE auto-complete to the more error-prone APIs just by importing the
+// main API).
+
+export 'src/types.dart';
+export 'src/url_launcher_string.dart';
diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml
index feb0a2c..6803d71 100644
--- a/packages/url_launcher/url_launcher/pubspec.yaml
+++ b/packages/url_launcher/url_launcher/pubspec.yaml
@@ -3,7 +3,7 @@
web, phone, SMS, and email schemes.
repository: https://github.com/flutter/plugins/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.0.20
+version: 6.1.0
environment:
sdk: ">=2.14.0 <3.0.0"
diff --git a/packages/url_launcher/url_launcher/test/link_test.dart b/packages/url_launcher/url_launcher/test/link_test.dart
index f7a98a0..6242397 100644
--- a/packages/url_launcher/url_launcher/test/link_test.dart
+++ b/packages/url_launcher/url_launcher/test/link_test.dart
@@ -9,7 +9,7 @@
import 'package:url_launcher/src/link.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
-import 'mock_url_launcher_platform.dart';
+import 'mocks/mock_url_launcher_platform.dart';
void main() {
late MockUrlLauncher mock;
@@ -58,8 +58,8 @@
useSafariVC: false,
useWebView: false,
universalLinksOnly: false,
- enableJavaScript: false,
- enableDomStorage: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
headers: <String, String>{},
webOnlyWindowName: null,
)
@@ -88,8 +88,8 @@
useSafariVC: true,
useWebView: true,
universalLinksOnly: false,
- enableJavaScript: false,
- enableDomStorage: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
headers: <String, String>{},
webOnlyWindowName: null,
)
diff --git a/packages/url_launcher/url_launcher/test/mock_url_launcher_platform.dart b/packages/url_launcher/url_launcher/test/mocks/mock_url_launcher_platform.dart
similarity index 100%
rename from packages/url_launcher/url_launcher/test/mock_url_launcher_platform.dart
rename to packages/url_launcher/url_launcher/test/mocks/mock_url_launcher_platform.dart
diff --git a/packages/url_launcher/url_launcher/test/url_launcher_test.dart b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart
similarity index 98%
rename from packages/url_launcher/url_launcher/test/url_launcher_test.dart
rename to packages/url_launcher/url_launcher/test/src/legacy_api_test.dart
index 4e980cb..4594ab2 100644
--- a/packages/url_launcher/url_launcher/test/url_launcher_test.dart
+++ b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart
@@ -8,10 +8,10 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show PlatformException;
import 'package:flutter_test/flutter_test.dart';
-import 'package:url_launcher/url_launcher.dart';
+import 'package:url_launcher/src/legacy_api.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
-import 'mock_url_launcher_platform.dart';
+import '../mocks/mock_url_launcher_platform.dart';
void main() {
final MockUrlLauncher mock = MockUrlLauncher();
diff --git a/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart b/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart
new file mode 100644
index 0000000..95b2f5c
--- /dev/null
+++ b/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart
@@ -0,0 +1,279 @@
+// 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.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:url_launcher/src/types.dart';
+import 'package:url_launcher/src/url_launcher_string.dart';
+import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
+
+import '../mocks/mock_url_launcher_platform.dart';
+
+void main() {
+ final MockUrlLauncher mock = MockUrlLauncher();
+ UrlLauncherPlatform.instance = mock;
+
+ group('canLaunchUrlString', () {
+ test('handles returning true', () async {
+ const String urlString = 'https://flutter.dev';
+ mock
+ ..setCanLaunchExpectations(urlString)
+ ..setResponse(true);
+
+ final bool result = await canLaunchUrlString(urlString);
+
+ expect(result, isTrue);
+ });
+
+ test('handles returning false', () async {
+ const String urlString = 'https://flutter.dev';
+ mock
+ ..setCanLaunchExpectations(urlString)
+ ..setResponse(false);
+
+ final bool result = await canLaunchUrlString(urlString);
+
+ expect(result, isFalse);
+ });
+ });
+
+ group('launchUrlString', () {
+ test('default behavior with web URL', () async {
+ const String urlString = 'https://flutter.dev';
+ mock
+ ..setLaunchExpectations(
+ url: urlString,
+ useSafariVC: true,
+ useWebView: true,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrlString(urlString), isTrue);
+ });
+
+ test('default behavior with non-web URL', () async {
+ const String urlString = 'customscheme:foo';
+ mock
+ ..setLaunchExpectations(
+ url: urlString,
+ useSafariVC: false,
+ useWebView: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrlString(urlString), isTrue);
+ });
+
+ test('explicit default launch mode with web URL', () async {
+ const String urlString = 'https://flutter.dev';
+ mock
+ ..setLaunchExpectations(
+ url: urlString,
+ useSafariVC: true,
+ useWebView: true,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrlString(urlString, mode: LaunchMode.platformDefault),
+ isTrue);
+ });
+
+ test('explicit default launch mode with non-web URL', () async {
+ const String urlString = 'customscheme:foo';
+ mock
+ ..setLaunchExpectations(
+ url: urlString,
+ useSafariVC: false,
+ useWebView: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrlString(urlString, mode: LaunchMode.platformDefault),
+ isTrue);
+ });
+
+ test('in-app webview', () async {
+ const String urlString = 'https://flutter.dev';
+ mock
+ ..setLaunchExpectations(
+ url: urlString,
+ useSafariVC: true,
+ useWebView: true,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrlString(urlString, mode: LaunchMode.inAppWebView),
+ isTrue);
+ });
+
+ test('external browser', () async {
+ const String urlString = 'https://flutter.dev';
+ mock
+ ..setLaunchExpectations(
+ url: urlString,
+ useSafariVC: false,
+ useWebView: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(
+ await launchUrlString(urlString,
+ mode: LaunchMode.externalApplication),
+ isTrue);
+ });
+
+ test('external non-browser only', () async {
+ const String urlString = 'https://flutter.dev';
+ mock
+ ..setLaunchExpectations(
+ url: urlString,
+ useSafariVC: false,
+ useWebView: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: true,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(
+ await launchUrlString(urlString,
+ mode: LaunchMode.externalNonBrowserApplication),
+ isTrue);
+ });
+
+ test('in-app webview without javascript', () async {
+ const String urlString = 'https://flutter.dev';
+ mock
+ ..setLaunchExpectations(
+ url: urlString,
+ useSafariVC: true,
+ useWebView: true,
+ enableJavaScript: false,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(
+ await launchUrlString(urlString,
+ mode: LaunchMode.inAppWebView,
+ webViewConfiguration:
+ const WebViewConfiguration(enableJavaScript: false)),
+ isTrue);
+ });
+
+ test('in-app webview without DOM storage', () async {
+ const String urlString = 'https://flutter.dev';
+ mock
+ ..setLaunchExpectations(
+ url: urlString,
+ useSafariVC: true,
+ useWebView: true,
+ enableJavaScript: true,
+ enableDomStorage: false,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(
+ await launchUrlString(urlString,
+ mode: LaunchMode.inAppWebView,
+ webViewConfiguration:
+ const WebViewConfiguration(enableDomStorage: false)),
+ isTrue);
+ });
+
+ test('in-app webview with headers', () async {
+ const String urlString = 'https://flutter.dev';
+ mock
+ ..setLaunchExpectations(
+ url: urlString,
+ useSafariVC: true,
+ useWebView: true,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{'key': 'value'},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(
+ await launchUrlString(urlString,
+ mode: LaunchMode.inAppWebView,
+ webViewConfiguration: const WebViewConfiguration(
+ headers: <String, String>{'key': 'value'})),
+ isTrue);
+ });
+
+ test('cannot launch a non-web URL in a webview', () async {
+ expect(
+ () async => await launchUrlString('tel:555-555-5555',
+ mode: LaunchMode.inAppWebView),
+ throwsA(isA<ArgumentError>()));
+ });
+
+ test('non-web URL with default options', () async {
+ const String emailLaunchUrlString =
+ 'mailto:smith@example.com?subject=Hello';
+ mock
+ ..setLaunchExpectations(
+ url: emailLaunchUrlString,
+ useSafariVC: false,
+ useWebView: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrlString(emailLaunchUrlString), isTrue);
+ });
+
+ test('allows non-parseable url', () async {
+ // Not a valid Dart [Uri], but a valid URL on at least some platforms.
+ const String urlString =
+ 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1';
+ mock
+ ..setLaunchExpectations(
+ url: urlString,
+ useSafariVC: false,
+ useWebView: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrlString(urlString), isTrue);
+ });
+ });
+}
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
new file mode 100644
index 0000000..8286e0c
--- /dev/null
+++ b/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart
@@ -0,0 +1,262 @@
+// 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.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:url_launcher/src/types.dart';
+import 'package:url_launcher/src/url_launcher_uri.dart';
+import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
+
+import '../mocks/mock_url_launcher_platform.dart';
+
+void main() {
+ final MockUrlLauncher mock = MockUrlLauncher();
+ UrlLauncherPlatform.instance = mock;
+
+ test('closeInAppWebView', () async {
+ await closeInAppWebView();
+ expect(mock.closeWebViewCalled, isTrue);
+ });
+
+ group('canLaunchUrl', () {
+ test('handles returning true', () async {
+ final Uri url = Uri.parse('https://flutter.dev');
+ mock
+ ..setCanLaunchExpectations(url.toString())
+ ..setResponse(true);
+
+ final bool result = await canLaunchUrl(url);
+
+ expect(result, isTrue);
+ });
+
+ test('handles returning false', () async {
+ final Uri url = Uri.parse('https://flutter.dev');
+ mock
+ ..setCanLaunchExpectations(url.toString())
+ ..setResponse(false);
+
+ final bool result = await canLaunchUrl(url);
+
+ expect(result, isFalse);
+ });
+ });
+
+ group('launchUrl', () {
+ test('default behavior with web URL', () async {
+ final Uri url = Uri.parse('https://flutter.dev');
+ mock
+ ..setLaunchExpectations(
+ url: url.toString(),
+ useSafariVC: true,
+ useWebView: true,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrl(url), isTrue);
+ });
+
+ test('default behavior with non-web URL', () async {
+ final Uri url = Uri.parse('customscheme:foo');
+ mock
+ ..setLaunchExpectations(
+ url: url.toString(),
+ useSafariVC: false,
+ useWebView: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrl(url), isTrue);
+ });
+
+ test('explicit default launch mode with web URL', () async {
+ final Uri url = Uri.parse('https://flutter.dev');
+ mock
+ ..setLaunchExpectations(
+ url: url.toString(),
+ useSafariVC: true,
+ useWebView: true,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrl(url, mode: LaunchMode.platformDefault), isTrue);
+ });
+
+ test('explicit default launch mode with non-web URL', () async {
+ final Uri url = Uri.parse('customscheme:foo');
+ mock
+ ..setLaunchExpectations(
+ url: url.toString(),
+ useSafariVC: false,
+ useWebView: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrl(url, mode: LaunchMode.platformDefault), isTrue);
+ });
+
+ test('in-app webview', () async {
+ final Uri url = Uri.parse('https://flutter.dev');
+ mock
+ ..setLaunchExpectations(
+ url: url.toString(),
+ useSafariVC: true,
+ useWebView: true,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrl(url, mode: LaunchMode.inAppWebView), isTrue);
+ });
+
+ test('external browser', () async {
+ final Uri url = Uri.parse('https://flutter.dev');
+ mock
+ ..setLaunchExpectations(
+ url: url.toString(),
+ useSafariVC: false,
+ useWebView: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(
+ await launchUrl(url, mode: LaunchMode.externalApplication), isTrue);
+ });
+
+ test('external non-browser only', () async {
+ final Uri url = Uri.parse('https://flutter.dev');
+ mock
+ ..setLaunchExpectations(
+ url: url.toString(),
+ useSafariVC: false,
+ useWebView: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: true,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(
+ await launchUrl(url, mode: LaunchMode.externalNonBrowserApplication),
+ isTrue);
+ });
+
+ test('in-app webview without javascript', () async {
+ final Uri url = Uri.parse('https://flutter.dev');
+ mock
+ ..setLaunchExpectations(
+ url: url.toString(),
+ useSafariVC: true,
+ useWebView: true,
+ enableJavaScript: false,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(
+ await launchUrl(url,
+ mode: LaunchMode.inAppWebView,
+ webViewConfiguration:
+ const WebViewConfiguration(enableJavaScript: false)),
+ isTrue);
+ });
+
+ test('in-app webview without DOM storage', () async {
+ final Uri url = Uri.parse('https://flutter.dev');
+ mock
+ ..setLaunchExpectations(
+ url: url.toString(),
+ useSafariVC: true,
+ useWebView: true,
+ enableJavaScript: true,
+ enableDomStorage: false,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(
+ await launchUrl(url,
+ mode: LaunchMode.inAppWebView,
+ webViewConfiguration:
+ const WebViewConfiguration(enableDomStorage: false)),
+ isTrue);
+ });
+
+ test('in-app webview with headers', () async {
+ final Uri url = Uri.parse('https://flutter.dev');
+ mock
+ ..setLaunchExpectations(
+ url: url.toString(),
+ useSafariVC: true,
+ useWebView: true,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{'key': 'value'},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(
+ await launchUrl(url,
+ mode: LaunchMode.inAppWebView,
+ webViewConfiguration: const WebViewConfiguration(
+ headers: <String, String>{'key': 'value'})),
+ isTrue);
+ });
+
+ test('cannot launch a non-web URL in a webview', () async {
+ expect(
+ () async => await launchUrl(Uri(scheme: 'tel', path: '555-555-5555'),
+ mode: LaunchMode.inAppWebView),
+ throwsA(isA<ArgumentError>()));
+ });
+
+ test('non-web URL with default options', () async {
+ final Uri emailLaunchUrl = Uri(
+ scheme: 'mailto',
+ path: 'smith@example.com',
+ queryParameters: <String, String>{'subject': 'Hello'},
+ );
+ mock
+ ..setLaunchExpectations(
+ url: emailLaunchUrl.toString(),
+ useSafariVC: false,
+ useWebView: false,
+ enableJavaScript: true,
+ enableDomStorage: true,
+ universalLinksOnly: false,
+ headers: <String, String>{},
+ webOnlyWindowName: null,
+ )
+ ..setResponse(true);
+ expect(await launchUrl(emailLaunchUrl), isTrue);
+ });
+ });
+}