[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);
+    });
+  });
+}