Skip to content

Commit

Permalink
[url_launcher] Add an inAppBrowserView mode (#5155)
Browse files Browse the repository at this point in the history
`url_launcher_android` recently switched from an in-app webview to an Android Custom Tab (when supported), which was intended to be an in-place upgrade. However, this broke `closeInAppWebView`, and that couldn't be fixed directly because Android Custom Tab has no mechanism for programatic close. To address the regression, this adds a new `inAppBrowserView` launch mode which is distinct from `inAppWebView`, so that use cases that require programatic close can specifically request `inAppWebView` instead. The default for web links is the new `inAppBrowserView` since that gives better results in most cases.

Since whether `closeInAppWebView` will work in any given case is now non-trivial (on iOS, both in-app modes are supported, but on Android it's only the web view mode), this adds a new support API to query it, in keeping with the relatively new guidance of https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#api-support-queries. It also adds API to query for support for being able to use specific launch modes, since there wasn't a good way to understand which modes worked in general on different platforms.

Since there are new APIs, this adds support for those APIs to all of our implementations to ensure that they give accurate responses.

Fixes flutter/flutter#134208
  • Loading branch information
stuartmorgan authored Oct 24, 2023
1 parent 3cc6e26 commit c6821f9
Show file tree
Hide file tree
Showing 14 changed files with 145 additions and 47 deletions.
15 changes: 15 additions & 0 deletions packages/url_launcher/url_launcher/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## 6.2.0

* Adds `supportsLaunchMode` for checking whether the current platform supports a
given launch mode, to allow clients that will only work with specific modes
to avoid fallback to a different mode.
* Adds `supportsCloseForLaunchMode` to allow checking programatically if a
launched URL will be able to be closed. Previously the documented behvaior was
that it worked only with the `inAppWebView` launch mode, but this is no longer
true on all platforms with the addition of `inAppBrowserView`.
* Updates the documention for `launchUrl` to clarify that clients should not
rely on any specific behavior of the `platformDefault` launch mode. Changes
to the handling of `platformDefault`, such as Android's recent change from
`inAppWebView` to the new `inAppBrowserView`, are not considered breaking.
* Updates minimum supported SDK version to Flutter 3.13.

## 6.1.14

* Updates documentation to mention support for Android Custom Tabs.
Expand Down
18 changes: 15 additions & 3 deletions packages/url_launcher/url_launcher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ See [`-[UIApplication canOpenURL:]`](https://developer.apple.com/documentation/u

Add any URL schemes passed to `canLaunchUrl` as `<queries>` entries in your
`AndroidManifest.xml`, otherwise it will return false in most cases starting
on Android 11 (API 30) or higher. A `<queries>`
on Android 11 (API 30) or higher. Checking for
`supportsLaunchMode(PreferredLaunchMode.inAppBrowserView)` also requires
a `<queries>` entry to return anything but false. A `<queries>`
element must be added to your manifest as a child of the root element.

Example:
Expand All @@ -85,6 +87,10 @@ Example:
<action android:name="android.intent.action.VIEW" />
<data android:scheme="tel" />
</intent>
<!-- If your application checks for inAppBrowserView launch mode support -->
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
```

Expand Down Expand Up @@ -210,10 +216,16 @@ if (!await launchUrl(uri)) {
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
## 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`.
passing a `PreferredLaunchMode`.

Platforms that do no support a requested `PreferredLaunchMode` will
automatically fall back to a supported mode (usually `platformDefault`). If
your application needs to avoid that fallback behavior, however, you can check
if the current platform supports a given mode with `supportsLaunchMode` before
calling `launchUrl`.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
<action android:name="android.intent.action.VIEW" />
<data android:scheme="tel" />
</intent>
<!-- If your application checks for inAppBrowserView launch mode support -->
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
<!--#enddocregion android-queries-->
<!-- The "https" scheme is only required for integration tests of this package.
It shouldn't be needed in most actual apps, or show up in the README! -->
Expand Down
18 changes: 12 additions & 6 deletions packages/url_launcher/url_launcher/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ class _MyHomePageState extends State<MyHomePage> {
}
}

Future<void> _launchInWebViewOrVC(Uri url) async {
Future<void> _launchInBrowserView(Uri url) async {
if (!await launchUrl(url, mode: LaunchMode.inAppBrowserView)) {
throw Exception('Could not launch $url');
}
}

Future<void> _launchInWebView(Uri url) async {
if (!await launchUrl(url, mode: LaunchMode.inAppWebView)) {
throw Exception('Could not launch $url');
}
Expand Down Expand Up @@ -99,15 +105,15 @@ class _MyHomePageState extends State<MyHomePage> {
}
}

Future<void> _launchUniversalLinkIos(Uri url) async {
Future<void> _launchUniversalLinkIOS(Uri url) async {
final bool nativeAppLaunchSucceeded = await launchUrl(
url,
mode: LaunchMode.externalNonBrowserApplication,
);
if (!nativeAppLaunchSucceeded) {
await launchUrl(
url,
mode: LaunchMode.inAppWebView,
mode: LaunchMode.inAppBrowserView,
);
}
}
Expand Down Expand Up @@ -173,7 +179,7 @@ class _MyHomePageState extends State<MyHomePage> {
const Padding(padding: EdgeInsets.all(16.0)),
ElevatedButton(
onPressed: () => setState(() {
_launched = _launchInWebViewOrVC(toLaunch);
_launched = _launchInBrowserView(toLaunch);
}),
child: const Text('Launch in app'),
),
Expand All @@ -198,15 +204,15 @@ class _MyHomePageState extends State<MyHomePage> {
const Padding(padding: EdgeInsets.all(16.0)),
ElevatedButton(
onPressed: () => setState(() {
_launched = _launchUniversalLinkIos(toLaunch);
_launched = _launchUniversalLinkIOS(toLaunch);
}),
child: const Text(
'Launch a universal link in a native app, fallback to Safari.(Youtube)'),
),
const Padding(padding: EdgeInsets.all(16.0)),
ElevatedButton(
onPressed: () => setState(() {
_launched = _launchInWebViewOrVC(toLaunch);
_launched = _launchInWebView(toLaunch);
Timer(const Duration(seconds: 5), () {
closeInAppWebView();
});
Expand Down
4 changes: 2 additions & 2 deletions packages/url_launcher/url_launcher/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ description: Demonstrates how to use the url_launcher plugin.
publish_to: none

environment:
sdk: ">=3.0.0 <4.0.0"
flutter: ">=3.10.0"
sdk: ">=3.1.0 <4.0.0"
flutter: ">=3.13.0"

dependencies:
flutter:
Expand Down
2 changes: 1 addition & 1 deletion packages/url_launcher/url_launcher/lib/src/link.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class DefaultLinkDelegate extends StatelessWidget {
success = await launchUrl(
url,
mode: _useWebView
? LaunchMode.inAppWebView
? LaunchMode.inAppBrowserView
: LaunchMode.externalApplication,
);
} on PlatformException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ PreferredLaunchMode convertLaunchMode(LaunchMode mode) {
switch (mode) {
case LaunchMode.platformDefault:
return PreferredLaunchMode.platformDefault;
case LaunchMode.inAppBrowserView:
return PreferredLaunchMode.inAppBrowserView;
case LaunchMode.inAppWebView:
return PreferredLaunchMode.inAppWebView;
case LaunchMode.externalApplication:
Expand Down
5 changes: 4 additions & 1 deletion packages/url_launcher/url_launcher/lib/src/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ enum LaunchMode {
/// implementation.
platformDefault,

/// Loads the URL in an in-app web view (e.g., Android Custom Tabs, Safari View Controller).
/// Loads the URL in an in-app web view (e.g., Android WebView).
inAppWebView,

/// Loads the URL in an in-app web view (e.g., Android Custom Tabs, SFSafariViewController).
inAppBrowserView,

/// Passes the URL to the OS to be handled by another application.
externalApplication,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ Future<bool> launchUrlString(
WebViewConfiguration webViewConfiguration = const WebViewConfiguration(),
String? webOnlyWindowName,
}) async {
if (mode == LaunchMode.inAppWebView &&
if ((mode == LaunchMode.inAppWebView ||
mode == LaunchMode.inAppBrowserView) &&
!(urlString.startsWith('https:') || urlString.startsWith('http:'))) {
throw ArgumentError.value(urlString, 'urlString',
'To use an in-app web view, you must provide an http(s) URL.');
Expand Down
51 changes: 29 additions & 22 deletions packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,13 @@ import 'type_conversion.dart';

/// Passes [url] to the underlying platform for handling.
///
/// [mode] support varies significantly by platform:
/// - [LaunchMode.platformDefault] is supported on all platforms:
/// - On iOS and Android, this treats web URLs as
/// [LaunchMode.inAppWebView], and all other URLs as
/// [LaunchMode.externalApplication].
/// - On Windows, macOS, and Linux this behaves like
/// [LaunchMode.externalApplication].
/// - On web, this uses `webOnlyWindowName` for web URLs, and behaves like
/// [LaunchMode.externalApplication] for any other content.
/// - [LaunchMode.inAppWebView] is currently only supported on iOS and
/// Android. If a non-web URL is passed with this mode, an [ArgumentError]
/// will be thrown.
/// - [LaunchMode.externalApplication] is supported on all platforms.
/// On iOS, this should be used in cases where sharing the cookies of the
/// user's browser is important, such as SSO flows, since Safari View
/// Controller does not share the browser's context.
/// - [LaunchMode.externalNonBrowserApplication] is supported on iOS 10+.
/// This setting is used to require universal links to open in a non-browser
/// application.
/// [mode] support varies significantly by platform. Clients can use
/// [supportsLaunchMode] to query for support, but platforms will fall back to
/// other modes if the requested mode is not supported, so checking is not
/// required. The default behavior of [LaunchMode.platformDefault] is up to each
/// platform, and its behavior for a given platform may change over time as new
/// modes are supported, so clients that want a specific mode should request it
/// rather than rely on any currently observed default behavior.
///
/// For web, [webOnlyWindowName] specifies a target for the launch. This
/// supports the standard special link target names. For example:
Expand All @@ -45,7 +33,8 @@ Future<bool> launchUrl(
WebViewConfiguration webViewConfiguration = const WebViewConfiguration(),
String? webOnlyWindowName,
}) async {
if (mode == LaunchMode.inAppWebView &&
if ((mode == LaunchMode.inAppWebView ||
mode == LaunchMode.inAppBrowserView) &&
!(url.scheme == 'https' || url.scheme == 'http')) {
throw ArgumentError.value(url, 'url',
'To use an in-app web view, you must provide an http(s) URL.');
Expand Down Expand Up @@ -81,8 +70,26 @@ Future<bool> canLaunchUrl(Uri url) async {
/// Closes the current in-app web view, if one was previously opened by
/// [launchUrl].
///
/// If [launchUrl] was never called with [LaunchMode.inAppWebView], then this
/// call will have no effect.
/// This works only if [supportsCloseForLaunchMode] returns true for the mode
/// that was used by [launchUrl].
Future<void> closeInAppWebView() async {
return UrlLauncherPlatform.instance.closeWebView();
}

/// Returns true if [mode] is supported by the current platform implementation.
///
/// Calling [launchUrl] with an unsupported mode will fall back to a supported
/// mode, so calling this method is only necessary for cases where the caller
/// needs to know which mode will be used.
Future<bool> supportsLaunchMode(PreferredLaunchMode mode) {
return UrlLauncherPlatform.instance.supportsMode(mode);
}

/// Returns true if [closeInAppWebView] is supported for [mode] in the current
/// platform implementation.
///
/// If this returns false, [closeInAppWebView] will not work when launching
/// URLs with [mode].
Future<bool> supportsCloseForLaunchMode(PreferredLaunchMode mode) {
return UrlLauncherPlatform.instance.supportsMode(mode);
}
20 changes: 10 additions & 10 deletions packages/url_launcher/url_launcher/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ description: Flutter plugin for launching a URL. Supports
web, phone, SMS, and email schemes.
repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
version: 6.1.14
version: 6.2.0

environment:
sdk: ">=3.0.0 <4.0.0"
flutter: ">=3.10.0"
sdk: ">=3.1.0 <4.0.0"
flutter: ">=3.13.0"

flutter:
plugin:
Expand All @@ -28,15 +28,15 @@ flutter:
dependencies:
flutter:
sdk: flutter
url_launcher_android: ^6.0.13
url_launcher_ios: ^6.0.13
url_launcher_android: ^6.2.0
url_launcher_ios: ^6.2.0
# Allow either the pure-native or Dart/native hybrid versions of the desktop
# implementations, as both are compatible.
url_launcher_linux: ">=2.0.0 <4.0.0"
url_launcher_macos: ">=2.0.0 <4.0.0"
url_launcher_platform_interface: ^2.1.0
url_launcher_web: ^2.0.0
url_launcher_windows: ">=2.0.0 <4.0.0"
url_launcher_linux: ^3.1.0
url_launcher_macos: ^3.1.0
url_launcher_platform_interface: ^2.2.0
url_launcher_web: ^2.2.0
url_launcher_windows: ^3.1.0

dev_dependencies:
flutter_test:
Expand Down
2 changes: 1 addition & 1 deletion packages/url_launcher/url_launcher/test/link_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ void main() {
mock
..setLaunchExpectations(
url: 'http://example.com/foobar',
launchMode: PreferredLaunchMode.inAppWebView,
launchMode: PreferredLaunchMode.inAppBrowserView,
universalLinksOnly: false,
enableJavaScript: true,
enableDomStorage: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,16 @@ class MockUrlLauncher extends Fake
Future<void> closeWebView() async {
closeWebViewCalled = true;
}

@override
Future<bool> supportsMode(PreferredLaunchMode mode) async {
launchMode = mode;
return response!;
}

@override
Future<bool> supportsCloseForMode(PreferredLaunchMode mode) async {
launchMode = mode;
return response!;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,40 @@ void main() {
expect(await launchUrl(emailLaunchUrl), isTrue);
});
});

group('supportsLaunchMode', () {
test('handles returning true', () async {
const PreferredLaunchMode mode = PreferredLaunchMode.inAppBrowserView;
mock.setResponse(true);

expect(await supportsLaunchMode(mode), true);
expect(mock.launchMode, mode);
});

test('handles returning false', () async {
const PreferredLaunchMode mode = PreferredLaunchMode.inAppBrowserView;
mock.setResponse(false);

expect(await supportsLaunchMode(mode), false);
expect(mock.launchMode, mode);
});
});

group('supportsCloseForLaunchMode', () {
test('handles returning true', () async {
const PreferredLaunchMode mode = PreferredLaunchMode.inAppBrowserView;
mock.setResponse(true);

expect(await supportsCloseForLaunchMode(mode), true);
expect(mock.launchMode, mode);
});

test('handles returning false', () async {
const PreferredLaunchMode mode = PreferredLaunchMode.inAppBrowserView;
mock.setResponse(false);

expect(await supportsCloseForLaunchMode(mode), false);
expect(mock.launchMode, mode);
});
});
}

0 comments on commit c6821f9

Please sign in to comment.