[url_launcher] Add a new launchUrl to platform interface (#5966)

This creates a new platform interface method for launching that closely
parallels the new public-facing API, so that implementations can switch
to implementing a more platform-neutral implementation. This will pave
the way for things like cleanly implementing
`externalNonBrowserApplication` support on non-iOS platforms.

A follow-up will switch the app-facing package to call this new methods
instead of the legacy method.

Implementation packages can adopt the new method as is useful for them;
eventually we can do a cleanup pass if we want to fully deprecate the
old method.
diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md
index 1a9c575..78818ef 100644
--- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.1.0
+
+* Adds a new `launchUrl` method corresponding to the new app-facing interface.
+
 ## 2.0.5
 
 * Updates code for new analysis options.
diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/src/types.dart b/packages/url_launcher/url_launcher_platform_interface/lib/src/types.dart
new file mode 100644
index 0000000..08d87e0
--- /dev/null
+++ b/packages/url_launcher/url_launcher_platform_interface/lib/src/types.dart
@@ -0,0 +1,69 @@
+// 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.
+enum PreferredLaunchMode {
+  /// 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 [PreferredLaunchMode.inAppWebView].
+///
+/// Not all options are supported on all platforms. This is a superset of
+/// available options exposed across all implementations.
+@immutable
+class InAppWebViewConfiguration {
+  /// Creates a new WebViewConfiguration with the given settings.
+  const InAppWebViewConfiguration({
+    this.enableJavaScript = true,
+    this.enableDomStorage = true,
+    this.headers = const <String, String>{},
+  });
+
+  /// Whether or not JavaScript is enabled for the web content.
+  final bool enableJavaScript;
+
+  /// Whether or not DOM storage is enabled for the web content.
+  final bool enableDomStorage;
+
+  /// Additional headers to pass in the load request.
+  final Map<String, String> headers;
+}
+
+/// Options for [launchUrl].
+@immutable
+class LaunchOptions {
+  /// Creates a new parameter object with the given options.
+  const LaunchOptions({
+    this.mode = PreferredLaunchMode.platformDefault,
+    this.webViewConfiguration = const InAppWebViewConfiguration(),
+    this.webOnlyWindowName,
+  });
+
+  /// The requested launch mode.
+  final PreferredLaunchMode mode;
+
+  /// Configuration for the web view in [PreferredLaunchMode.inAppWebView] mode.
+  final InAppWebViewConfiguration webViewConfiguration;
+
+  /// A web-platform-specific option to set the link target.
+  ///
+  /// Default behaviour when unset should be to open the url in a new tab.
+  final String? webOnlyWindowName;
+}
diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/src/url_launcher_platform.dart b/packages/url_launcher/url_launcher_platform_interface/lib/src/url_launcher_platform.dart
new file mode 100644
index 0000000..aa499db
--- /dev/null
+++ b/packages/url_launcher/url_launcher_platform_interface/lib/src/url_launcher_platform.dart
@@ -0,0 +1,94 @@
+// 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:plugin_platform_interface/plugin_platform_interface.dart';
+import 'package:url_launcher_platform_interface/link.dart';
+import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
+
+import '../method_channel_url_launcher.dart';
+
+/// The interface that implementations of url_launcher must implement.
+///
+/// Platform implementations should extend this class rather than implement it as `url_launcher`
+/// does not consider newly added methods to be breaking changes. Extending this class
+/// (using `extends`) ensures that the subclass will get the default implementation, while
+/// platform implementations that `implements` this interface will be broken by newly added
+/// [UrlLauncherPlatform] methods.
+abstract class UrlLauncherPlatform extends PlatformInterface {
+  /// Constructs a UrlLauncherPlatform.
+  UrlLauncherPlatform() : super(token: _token);
+
+  static final Object _token = Object();
+
+  static UrlLauncherPlatform _instance = MethodChannelUrlLauncher();
+
+  /// The default instance of [UrlLauncherPlatform] to use.
+  ///
+  /// Defaults to [MethodChannelUrlLauncher].
+  static UrlLauncherPlatform get instance => _instance;
+
+  /// Platform-specific plugins should set this with their own platform-specific
+  /// class that extends [UrlLauncherPlatform] when they register themselves.
+  // TODO(amirh): Extract common platform interface logic.
+  // https://github.com/flutter/flutter/issues/43368
+  static set instance(UrlLauncherPlatform instance) {
+    PlatformInterface.verify(instance, _token);
+    _instance = instance;
+  }
+
+  /// The delegate used by the Link widget to build itself.
+  LinkDelegate? get linkDelegate;
+
+  /// Returns `true` if this platform is able to launch [url].
+  Future<bool> canLaunch(String url) {
+    throw UnimplementedError('canLaunch() has not been implemented.');
+  }
+
+  /// Passes [url] to the underlying platform for handling.
+  ///
+  /// Returns `true` if the given [url] was successfully launched.
+  ///
+  /// For documentation on the other arguments, see the `launch` documentation
+  /// in `package:url_launcher/url_launcher.dart`.
+  Future<bool> launch(
+    String url, {
+    required bool useSafariVC,
+    required bool useWebView,
+    required bool enableJavaScript,
+    required bool enableDomStorage,
+    required bool universalLinksOnly,
+    required Map<String, String> headers,
+    String? webOnlyWindowName,
+  }) {
+    throw UnimplementedError('launch() has not been implemented.');
+  }
+
+  /// Passes [url] to the underlying platform for handling.
+  ///
+  /// Returns `true` if the given [url] was successfully launched.
+  Future<bool> launchUrl(String url, LaunchOptions options) {
+    final bool isWebURL = url.startsWith('http:') || url.startsWith('https:');
+    final bool useWebView = options.mode == PreferredLaunchMode.inAppWebView ||
+        (isWebURL && options.mode == PreferredLaunchMode.platformDefault);
+
+    return launch(
+      url,
+      useSafariVC: useWebView,
+      useWebView: useWebView,
+      enableJavaScript: options.webViewConfiguration.enableJavaScript,
+      enableDomStorage: options.webViewConfiguration.enableDomStorage,
+      universalLinksOnly:
+          options.mode == PreferredLaunchMode.externalNonBrowserApplication,
+      headers: options.webViewConfiguration.headers,
+      webOnlyWindowName: options.webOnlyWindowName,
+    );
+  }
+
+  /// Closes the WebView, if one was opened earlier by [launch].
+  Future<void> closeWebView() {
+    throw UnimplementedError('closeWebView() has not been implemented.');
+  }
+}
diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart
index 18d64ef..3312c2f 100644
--- a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart
+++ b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart
@@ -2,69 +2,5 @@
 // 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:plugin_platform_interface/plugin_platform_interface.dart';
-import 'package:url_launcher_platform_interface/link.dart';
-
-import 'method_channel_url_launcher.dart';
-
-/// The interface that implementations of url_launcher must implement.
-///
-/// Platform implementations should extend this class rather than implement it as `url_launcher`
-/// does not consider newly added methods to be breaking changes. Extending this class
-/// (using `extends`) ensures that the subclass will get the default implementation, while
-/// platform implementations that `implements` this interface will be broken by newly added
-/// [UrlLauncherPlatform] methods.
-abstract class UrlLauncherPlatform extends PlatformInterface {
-  /// Constructs a UrlLauncherPlatform.
-  UrlLauncherPlatform() : super(token: _token);
-
-  static final Object _token = Object();
-
-  static UrlLauncherPlatform _instance = MethodChannelUrlLauncher();
-
-  /// The default instance of [UrlLauncherPlatform] to use.
-  ///
-  /// Defaults to [MethodChannelUrlLauncher].
-  static UrlLauncherPlatform get instance => _instance;
-
-  /// Platform-specific plugins should set this with their own platform-specific
-  /// class that extends [UrlLauncherPlatform] when they register themselves.
-  // TODO(amirh): Extract common platform interface logic.
-  // https://github.com/flutter/flutter/issues/43368
-  static set instance(UrlLauncherPlatform instance) {
-    PlatformInterface.verify(instance, _token);
-    _instance = instance;
-  }
-
-  /// The delegate used by the Link widget to build itself.
-  LinkDelegate? get linkDelegate;
-
-  /// Returns `true` if this platform is able to launch [url].
-  Future<bool> canLaunch(String url) {
-    throw UnimplementedError('canLaunch() has not been implemented.');
-  }
-
-  /// Returns `true` if the given [url] was successfully launched.
-  ///
-  /// For documentation on the other arguments, see the `launch` documentation
-  /// in `package:url_launcher/url_launcher.dart`.
-  Future<bool> launch(
-    String url, {
-    required bool useSafariVC,
-    required bool useWebView,
-    required bool enableJavaScript,
-    required bool enableDomStorage,
-    required bool universalLinksOnly,
-    required Map<String, String> headers,
-    String? webOnlyWindowName,
-  }) {
-    throw UnimplementedError('launch() has not been implemented.');
-  }
-
-  /// Closes the WebView, if one was opened earlier by [launch].
-  Future<void> closeWebView() {
-    throw UnimplementedError('closeWebView() has not been implemented.');
-  }
-}
+export 'src/types.dart';
+export 'src/url_launcher_platform.dart';
diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml
index 140a1ae..76461ff 100644
--- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml
@@ -4,7 +4,7 @@
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
 # NOTE: We strongly prefer non-breaking changes, even at the expense of a
 # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
-version: 2.0.5
+version: 2.1.0
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/url_launcher/url_launcher_platform_interface/test/url_launcher_platform_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/url_launcher_platform_test.dart
new file mode 100644
index 0000000..f764f67
--- /dev/null
+++ b/packages/url_launcher/url_launcher_platform_interface/test/url_launcher_platform_test.dart
@@ -0,0 +1,121 @@
+// 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_platform_interface/link.dart';
+import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
+
+class CapturingUrlLauncher extends UrlLauncherPlatform {
+  String? url;
+  bool? useSafariVC;
+  bool? useWebView;
+  bool? enableJavaScript;
+  bool? enableDomStorage;
+  bool? universalLinksOnly;
+  Map<String, String> headers = <String, String>{};
+  String? webOnlyWindowName;
+
+  @override
+  final LinkDelegate? linkDelegate = null;
+
+  @override
+  Future<bool> launch(
+    String url, {
+    required bool useSafariVC,
+    required bool useWebView,
+    required bool enableJavaScript,
+    required bool enableDomStorage,
+    required bool universalLinksOnly,
+    required Map<String, String> headers,
+    String? webOnlyWindowName,
+  }) async {
+    this.url = url;
+    this.useSafariVC = useSafariVC;
+    this.useWebView = useWebView;
+    this.enableJavaScript = enableJavaScript;
+    this.enableDomStorage = enableDomStorage;
+    this.universalLinksOnly = universalLinksOnly;
+    this.headers = headers;
+    this.webOnlyWindowName = webOnlyWindowName;
+
+    return true;
+  }
+}
+
+void main() {
+  test('launchUrl calls through to launch with default options for web URL',
+      () async {
+    final CapturingUrlLauncher launcher = CapturingUrlLauncher();
+
+    await launcher.launchUrl('https://flutter.dev', const LaunchOptions());
+
+    expect(launcher.url, 'https://flutter.dev');
+    expect(launcher.useSafariVC, true);
+    expect(launcher.useWebView, true);
+    expect(launcher.enableJavaScript, true);
+    expect(launcher.enableDomStorage, true);
+    expect(launcher.universalLinksOnly, false);
+    expect(launcher.headers, isEmpty);
+    expect(launcher.webOnlyWindowName, null);
+  });
+
+  test('launchUrl calls through to launch with default options for non-web URL',
+      () async {
+    final CapturingUrlLauncher launcher = CapturingUrlLauncher();
+
+    await launcher.launchUrl('tel:123456789', const LaunchOptions());
+
+    expect(launcher.url, 'tel:123456789');
+    expect(launcher.useSafariVC, false);
+    expect(launcher.useWebView, false);
+    expect(launcher.enableJavaScript, true);
+    expect(launcher.enableDomStorage, true);
+    expect(launcher.universalLinksOnly, false);
+    expect(launcher.headers, isEmpty);
+    expect(launcher.webOnlyWindowName, null);
+  });
+
+  test('launchUrl calls through to launch with universal links', () async {
+    final CapturingUrlLauncher launcher = CapturingUrlLauncher();
+
+    await launcher.launchUrl(
+        'https://flutter.dev',
+        const LaunchOptions(
+            mode: PreferredLaunchMode.externalNonBrowserApplication));
+
+    expect(launcher.url, 'https://flutter.dev');
+    expect(launcher.useSafariVC, false);
+    expect(launcher.useWebView, false);
+    expect(launcher.enableJavaScript, true);
+    expect(launcher.enableDomStorage, true);
+    expect(launcher.universalLinksOnly, true);
+    expect(launcher.headers, isEmpty);
+    expect(launcher.webOnlyWindowName, null);
+  });
+
+  test('launchUrl calls through to launch with all non-default options',
+      () async {
+    final CapturingUrlLauncher launcher = CapturingUrlLauncher();
+
+    await launcher.launchUrl(
+        'https://flutter.dev',
+        const LaunchOptions(
+          mode: PreferredLaunchMode.externalApplication,
+          webViewConfiguration: InAppWebViewConfiguration(
+              enableJavaScript: false,
+              enableDomStorage: false,
+              headers: <String, String>{'foo': 'bar'}),
+          webOnlyWindowName: 'a_name',
+        ));
+
+    expect(launcher.url, 'https://flutter.dev');
+    expect(launcher.useSafariVC, false);
+    expect(launcher.useWebView, false);
+    expect(launcher.enableJavaScript, false);
+    expect(launcher.enableDomStorage, false);
+    expect(launcher.universalLinksOnly, false);
+    expect(launcher.headers['foo'], 'bar');
+    expect(launcher.webOnlyWindowName, 'a_name');
+  });
+}