Skip to content

Commit

Permalink
[url_launcher] Add a workaround for Uri encoding
Browse files Browse the repository at this point in the history
`Uri`'s constructor doesn't handle query parameters correctly for
non-http(s) schemes, so the `mailto` example in the README is
misleading. This adds a new utility method to do query string
construction correctly, and updates the README to show using it and
warning about the need to use it in general.

If/when `Uri` is fixed to handle generic URI query parameters correctly,
this utility method can be deprecated.

Fixes flutter/flutter#75552
Fixes flutter/flutter#73717
  • Loading branch information
stuartmorgan committed Apr 21, 2021
1 parent 6398441 commit 562afed
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 17 deletions.
8 changes: 7 additions & 1 deletion packages/url_launcher/url_launcher/CHANGELOG.md
@@ -1,6 +1,12 @@
## 6.1.0

* Add a utility method for query encoding when constructing
URIs, and update the README accordingly, to work around limitations
in `Uri` behavior.

## 6.0.3

* Updat README notes about URL schemes on iOS
* Update README notes about URL schemes on iOS

## 6.0.2

Expand Down
38 changes: 25 additions & 13 deletions packages/url_launcher/url_launcher/README.md
Expand Up @@ -10,10 +10,10 @@ To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml fil

## Installation

### iOS
### iOS
Add any URL schemes passed to `canLaunch` as `LSApplicationQueriesSchemes` entries in your Info.plist file.

Example:
Example:
```
<key>LSApplicationQueriesSchemes</key>
<array>
Expand Down Expand Up @@ -73,25 +73,37 @@ apps installed, so can't open `tel:` or `mailto:` links.

### Encoding URLs

URLs must be properly encoded, especially when including spaces or other special characters. This can be done using the [`Uri` class](https://api.dart.dev/stable/2.7.1/dart-core/Uri-class.html):
URLs must be properly encoded, especially when including spaces or other special
characters. This can be done using the
[`Uri` class](https://api.dart.dev/stable/2.7.1/dart-core/Uri-class.html).
For example:
```dart
import 'dart:core';
import 'package:url_launcher/url_launcher.dart';
final Uri _emailLaunchUri = Uri(
final Uri emailLaunchUri = Uri(
scheme: 'mailto',
path: 'smith@example.com',
queryParameters: {
query: encodeQueryParameters(<String, String>{
'subject': 'Example Subject & Symbols are allowed!'
}
}),
);
// ...
launch(emailLaunchUri.toString());
```

**Warning**: For any scheme other than `http` or `https`, you should use this
package's utility method for query parameters:

// mailto:smith@example.com?subject=Example+Subject+%26+Symbols+are+allowed%21
launch(_emailLaunchUri.toString());
```dart
Uri(
// ...
query: encodeQueryParameters(yourParameters),
);
```

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

A particular mobile device may not be able to receive all supported URL schemes.
Expand All @@ -113,4 +125,4 @@ By default, Android opens up a browser when handling URLs. You can pass
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.
else is redirected to the app handler.
Expand Up @@ -8,7 +8,6 @@

void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry,
"UrlLauncherPlugin");
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}
18 changes: 18 additions & 0 deletions packages/url_launcher/url_launcher/lib/url_launcher.dart
Expand Up @@ -9,6 +9,24 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';

/// Encodes [params] as a query parameter string suitable to be passed as the
/// 'query' parameter to a [Uri] constructor.
///
/// This exists to work around the fact that the 'queryParameters' argument of
/// the [Uri] constructor does encoding as HTML form parameters rather than
/// generic URI query parameters, and thus does not work correctly for schemes
/// other than http(s). See https://github.com/dart-lang/sdk/issues/43838 for
/// details.
String? encodeQueryParameters(Map<String, String> params) {
if (params.isEmpty) {
return null;
}
return params.entries
.map((MapEntry<String, String> e) =>
'${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
.join('&');
}

/// Parses the specified URL string and delegates handling of it to the
/// underlying platform.
///
Expand Down
2 changes: 1 addition & 1 deletion packages/url_launcher/url_launcher/pubspec.yaml
Expand Up @@ -2,7 +2,7 @@ name: url_launcher
description: Flutter plugin for launching a URL. Supports
web, phone, SMS, and email schemes.
homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher
version: 6.0.3
version: 6.1.0

flutter:
plugin:
Expand Down
54 changes: 54 additions & 0 deletions packages/url_launcher/url_launcher/test/url_launcher_test.dart
Expand Up @@ -282,4 +282,58 @@ void main() {
expect(binding.renderView.automaticSystemUiAdjustment, true);
});
});

group('encodeQueryParameters', () {
test('handles empty dictionary', () async {
final String? result = encodeQueryParameters(<String, String>{});

expect(result, isNull);
});

test('handles parameters without special characters', () async {
final Map<String, String> parameters = <String, String>{
'key1': 'value1',
'key2': 'value2',
};
final String? result = encodeQueryParameters(parameters);

expect(Uri.splitQueryString(result!), parameters);
});

test('handles & correctly', () async {
final Map<String, String> parameters = <String, String>{
'key1': 'this & that',
'key&': 'foo & bar',
};
final String? result = encodeQueryParameters(parameters);

expect(Uri.splitQueryString(result!), parameters);
// There should be exactly one unencoded & in the string, joining the
// two parameters.
expect(result.split('&').length, 2);
});

test('handles spaces correctly', () async {
final Map<String, String> parameters = <String, String>{
'a key': 'a value',
};
final String? result = encodeQueryParameters(parameters);

expect(Uri.splitQueryString(result!), parameters);
// Spaces should be encoded as %20, not +.
expect(result.contains('+'), isFalse);
expect(result.contains('%20'), isTrue);
});

test('handles + correctly', () async {
final Map<String, String> parameters = <String, String>{
'key+': 'value+',
};
final String? result = encodeQueryParameters(parameters);

expect(Uri.splitQueryString(result!), parameters);
// + should be encoded.
expect(result.contains('+'), isFalse);
});
});
}

0 comments on commit 562afed

Please sign in to comment.