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