Skip to content

Commit

Permalink
[url_launcher] Add a new launchUrl to platform interface (flutter#5966)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
stuartmorgan authored Jun 17, 2022
1 parent c7aa994 commit c3955d2
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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.');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/u
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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
}

0 comments on commit c3955d2

Please sign in to comment.