Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[url_launcher] Replace primary APIs with cleaner versions #5310

Merged
merged 17 commits into from
Apr 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions packages/url_launcher/url_launcher/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is changing the default setting not a braking change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case the old API with the old config style is still available. You only need to migrate to the WebViewConfiguration if you start using the new URI/String based APIs (not the one under legacy)

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

Expand Down
106 changes: 57 additions & 49 deletions packages/url_launcher/url_launcher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,23 @@ To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml fil
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'),
),
),
),
),
);

void _launchURL() async {
if (!await launch(_url)) throw 'Could not launch $_url';
void _launchUrl() async {
if (!await launchUrl(_url)) throw 'Could not launch $_url';
}
```

Expand All @@ -43,7 +43,7 @@ See the example app for more complex examples.
## 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:
```
Expand All @@ -59,7 +59,7 @@ See [`-[UIApplication canOpenURL:]`](https://developer.apple.com/documentation/u
### 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`,
Expand Down Expand Up @@ -94,34 +94,53 @@ for examples of other queries.

## 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
Expand All @@ -137,57 +156,46 @@ final Uri emailLaunchUri = Uri(
}),
);

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.

### Handling missing URL receivers
### URLs not handled by `Uri`

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.
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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we take a page from the React book and call this file: dangerous_url_launcher_string.dart or similar, so users that import this get scared?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did think about it; my hope is that having to find a separate import is already enough of a deterrent.

If that turns out not to be true and we see a lot of people filing issue because they blindly used this and ignored all the warnings about how it should rarely be used, we could deprecate that import file and add one with a scary name.


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.
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.

## 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 handling

## File scheme handling
`file:` scheme can be used on desktop platforms: `macOS`, `Linux` and `Windows`.
`file:` scheme can be used on desktop platforms: Windows, macOS, and Linux.

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
var filePath = '/path/to/file';
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`.
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,23 @@ void main() {
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);
});
}
74 changes: 33 additions & 41 deletions packages/url_launcher/url_launcher/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,67 +44,62 @@ class _MyHomePageState extends State<MyHomePage> {
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,
);
}
}
Expand All @@ -118,22 +113,19 @@ class _MyHomePageState extends State<MyHomePage> {
}

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),
Expand All @@ -160,9 +152,9 @@ class _MyHomePageState extends State<MyHomePage> {
? 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(() {
Expand All @@ -179,15 +171,15 @@ class _MyHomePageState extends State<MyHomePage> {
),
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(
Expand All @@ -203,7 +195,7 @@ class _MyHomePageState extends State<MyHomePage> {
_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'),
Expand Down
Loading