Skip to content

Commit c6821f9

Browse files
[url_launcher] Add an inAppBrowserView mode (flutter#5155)
`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#134208
1 parent 3cc6e26 commit c6821f9

File tree

14 files changed

+145
-47
lines changed

14 files changed

+145
-47
lines changed

packages/url_launcher/url_launcher/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
## 6.2.0
2+
3+
* Adds `supportsLaunchMode` for checking whether the current platform supports a
4+
given launch mode, to allow clients that will only work with specific modes
5+
to avoid fallback to a different mode.
6+
* Adds `supportsCloseForLaunchMode` to allow checking programatically if a
7+
launched URL will be able to be closed. Previously the documented behvaior was
8+
that it worked only with the `inAppWebView` launch mode, but this is no longer
9+
true on all platforms with the addition of `inAppBrowserView`.
10+
* Updates the documention for `launchUrl` to clarify that clients should not
11+
rely on any specific behavior of the `platformDefault` launch mode. Changes
12+
to the handling of `platformDefault`, such as Android's recent change from
13+
`inAppWebView` to the new `inAppBrowserView`, are not considered breaking.
14+
* Updates minimum supported SDK version to Flutter 3.13.
15+
116
## 6.1.14
217

318
* Updates documentation to mention support for Android Custom Tabs.

packages/url_launcher/url_launcher/README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ See [`-[UIApplication canOpenURL:]`](https://developer.apple.com/documentation/u
6666

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

7274
Example:
@@ -85,6 +87,10 @@ Example:
8587
<action android:name="android.intent.action.VIEW" />
8688
<data android:scheme="tel" />
8789
</intent>
90+
<!-- If your application checks for inAppBrowserView launch mode support -->
91+
<intent>
92+
<action android:name="android.support.customtabs.action.CustomTabsService" />
93+
</intent>
8894
</queries>
8995
```
9096

@@ -210,10 +216,16 @@ if (!await launchUrl(uri)) {
210216
If you need to access files outside of your application's sandbox, you will need to have the necessary
211217
[entitlements](https://docs.flutter.dev/desktop#entitlements-and-the-app-sandbox).
212218

213-
## Browser vs in-app Handling
219+
## Browser vs in-app handling
214220

215221
On some platforms, web URLs can be launched either in an in-app web view, or
216222
in the default browser. The default behavior depends on the platform (see
217223
[`launchUrl`](https://pub.dev/documentation/url_launcher/latest/url_launcher/launchUrl.html)
218224
for details), but a specific mode can be used on supported platforms by
219-
passing a `LaunchMode`.
225+
passing a `PreferredLaunchMode`.
226+
227+
Platforms that do no support a requested `PreferredLaunchMode` will
228+
automatically fall back to a supported mode (usually `platformDefault`). If
229+
your application needs to avoid that fallback behavior, however, you can check
230+
if the current platform supports a given mode with `supportsLaunchMode` before
231+
calling `launchUrl`.

packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
<action android:name="android.intent.action.VIEW" />
2121
<data android:scheme="tel" />
2222
</intent>
23+
<!-- If your application checks for inAppBrowserView launch mode support -->
24+
<intent>
25+
<action android:name="android.support.customtabs.action.CustomTabsService" />
26+
</intent>
2327
<!--#enddocregion android-queries-->
2428
<!-- The "https" scheme is only required for integration tests of this package.
2529
It shouldn't be needed in most actual apps, or show up in the README! -->

packages/url_launcher/url_launcher/example/lib/main.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,13 @@ class _MyHomePageState extends State<MyHomePage> {
6262
}
6363
}
6464

65-
Future<void> _launchInWebViewOrVC(Uri url) async {
65+
Future<void> _launchInBrowserView(Uri url) async {
66+
if (!await launchUrl(url, mode: LaunchMode.inAppBrowserView)) {
67+
throw Exception('Could not launch $url');
68+
}
69+
}
70+
71+
Future<void> _launchInWebView(Uri url) async {
6672
if (!await launchUrl(url, mode: LaunchMode.inAppWebView)) {
6773
throw Exception('Could not launch $url');
6874
}
@@ -99,15 +105,15 @@ class _MyHomePageState extends State<MyHomePage> {
99105
}
100106
}
101107

102-
Future<void> _launchUniversalLinkIos(Uri url) async {
108+
Future<void> _launchUniversalLinkIOS(Uri url) async {
103109
final bool nativeAppLaunchSucceeded = await launchUrl(
104110
url,
105111
mode: LaunchMode.externalNonBrowserApplication,
106112
);
107113
if (!nativeAppLaunchSucceeded) {
108114
await launchUrl(
109115
url,
110-
mode: LaunchMode.inAppWebView,
116+
mode: LaunchMode.inAppBrowserView,
111117
);
112118
}
113119
}
@@ -173,7 +179,7 @@ class _MyHomePageState extends State<MyHomePage> {
173179
const Padding(padding: EdgeInsets.all(16.0)),
174180
ElevatedButton(
175181
onPressed: () => setState(() {
176-
_launched = _launchInWebViewOrVC(toLaunch);
182+
_launched = _launchInBrowserView(toLaunch);
177183
}),
178184
child: const Text('Launch in app'),
179185
),
@@ -198,15 +204,15 @@ class _MyHomePageState extends State<MyHomePage> {
198204
const Padding(padding: EdgeInsets.all(16.0)),
199205
ElevatedButton(
200206
onPressed: () => setState(() {
201-
_launched = _launchUniversalLinkIos(toLaunch);
207+
_launched = _launchUniversalLinkIOS(toLaunch);
202208
}),
203209
child: const Text(
204210
'Launch a universal link in a native app, fallback to Safari.(Youtube)'),
205211
),
206212
const Padding(padding: EdgeInsets.all(16.0)),
207213
ElevatedButton(
208214
onPressed: () => setState(() {
209-
_launched = _launchInWebViewOrVC(toLaunch);
215+
_launched = _launchInWebView(toLaunch);
210216
Timer(const Duration(seconds: 5), () {
211217
closeInAppWebView();
212218
});

packages/url_launcher/url_launcher/example/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ description: Demonstrates how to use the url_launcher plugin.
33
publish_to: none
44

55
environment:
6-
sdk: ">=3.0.0 <4.0.0"
7-
flutter: ">=3.10.0"
6+
sdk: ">=3.1.0 <4.0.0"
7+
flutter: ">=3.13.0"
88

99
dependencies:
1010
flutter:

packages/url_launcher/url_launcher/lib/src/link.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ class DefaultLinkDelegate extends StatelessWidget {
126126
success = await launchUrl(
127127
url,
128128
mode: _useWebView
129-
? LaunchMode.inAppWebView
129+
? LaunchMode.inAppBrowserView
130130
: LaunchMode.externalApplication,
131131
);
132132
} on PlatformException {

packages/url_launcher/url_launcher/lib/src/type_conversion.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ PreferredLaunchMode convertLaunchMode(LaunchMode mode) {
2222
switch (mode) {
2323
case LaunchMode.platformDefault:
2424
return PreferredLaunchMode.platformDefault;
25+
case LaunchMode.inAppBrowserView:
26+
return PreferredLaunchMode.inAppBrowserView;
2527
case LaunchMode.inAppWebView:
2628
return PreferredLaunchMode.inAppWebView;
2729
case LaunchMode.externalApplication:

packages/url_launcher/url_launcher/lib/src/types.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ enum LaunchMode {
1414
/// implementation.
1515
platformDefault,
1616

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

20+
/// Loads the URL in an in-app web view (e.g., Android Custom Tabs, SFSafariViewController).
21+
inAppBrowserView,
22+
2023
/// Passes the URL to the OS to be handled by another application.
2124
externalApplication,
2225

packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ Future<bool> launchUrlString(
2525
WebViewConfiguration webViewConfiguration = const WebViewConfiguration(),
2626
String? webOnlyWindowName,
2727
}) async {
28-
if (mode == LaunchMode.inAppWebView &&
28+
if ((mode == LaunchMode.inAppWebView ||
29+
mode == LaunchMode.inAppBrowserView) &&
2930
!(urlString.startsWith('https:') || urlString.startsWith('http:'))) {
3031
throw ArgumentError.value(urlString, 'urlString',
3132
'To use an in-app web view, you must provide an http(s) URL.');

packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,13 @@ import 'type_conversion.dart';
1111

1212
/// Passes [url] to the underlying platform for handling.
1313
///
14-
/// [mode] support varies significantly by platform:
15-
/// - [LaunchMode.platformDefault] is supported on all platforms:
16-
/// - On iOS and Android, this treats web URLs as
17-
/// [LaunchMode.inAppWebView], and all other URLs as
18-
/// [LaunchMode.externalApplication].
19-
/// - On Windows, macOS, and Linux this behaves like
20-
/// [LaunchMode.externalApplication].
21-
/// - On web, this uses `webOnlyWindowName` for web URLs, and behaves like
22-
/// [LaunchMode.externalApplication] for any other content.
23-
/// - [LaunchMode.inAppWebView] is currently only supported on iOS and
24-
/// Android. If a non-web URL is passed with this mode, an [ArgumentError]
25-
/// will be thrown.
26-
/// - [LaunchMode.externalApplication] is supported on all platforms.
27-
/// On iOS, this should be used in cases where sharing the cookies of the
28-
/// user's browser is important, such as SSO flows, since Safari View
29-
/// Controller does not share the browser's context.
30-
/// - [LaunchMode.externalNonBrowserApplication] is supported on iOS 10+.
31-
/// This setting is used to require universal links to open in a non-browser
32-
/// application.
14+
/// [mode] support varies significantly by platform. Clients can use
15+
/// [supportsLaunchMode] to query for support, but platforms will fall back to
16+
/// other modes if the requested mode is not supported, so checking is not
17+
/// required. The default behavior of [LaunchMode.platformDefault] is up to each
18+
/// platform, and its behavior for a given platform may change over time as new
19+
/// modes are supported, so clients that want a specific mode should request it
20+
/// rather than rely on any currently observed default behavior.
3321
///
3422
/// For web, [webOnlyWindowName] specifies a target for the launch. This
3523
/// supports the standard special link target names. For example:
@@ -45,7 +33,8 @@ Future<bool> launchUrl(
4533
WebViewConfiguration webViewConfiguration = const WebViewConfiguration(),
4634
String? webOnlyWindowName,
4735
}) async {
48-
if (mode == LaunchMode.inAppWebView &&
36+
if ((mode == LaunchMode.inAppWebView ||
37+
mode == LaunchMode.inAppBrowserView) &&
4938
!(url.scheme == 'https' || url.scheme == 'http')) {
5039
throw ArgumentError.value(url, 'url',
5140
'To use an in-app web view, you must provide an http(s) URL.');
@@ -81,8 +70,26 @@ Future<bool> canLaunchUrl(Uri url) async {
8170
/// Closes the current in-app web view, if one was previously opened by
8271
/// [launchUrl].
8372
///
84-
/// If [launchUrl] was never called with [LaunchMode.inAppWebView], then this
85-
/// call will have no effect.
73+
/// This works only if [supportsCloseForLaunchMode] returns true for the mode
74+
/// that was used by [launchUrl].
8675
Future<void> closeInAppWebView() async {
8776
return UrlLauncherPlatform.instance.closeWebView();
8877
}
78+
79+
/// Returns true if [mode] is supported by the current platform implementation.
80+
///
81+
/// Calling [launchUrl] with an unsupported mode will fall back to a supported
82+
/// mode, so calling this method is only necessary for cases where the caller
83+
/// needs to know which mode will be used.
84+
Future<bool> supportsLaunchMode(PreferredLaunchMode mode) {
85+
return UrlLauncherPlatform.instance.supportsMode(mode);
86+
}
87+
88+
/// Returns true if [closeInAppWebView] is supported for [mode] in the current
89+
/// platform implementation.
90+
///
91+
/// If this returns false, [closeInAppWebView] will not work when launching
92+
/// URLs with [mode].
93+
Future<bool> supportsCloseForLaunchMode(PreferredLaunchMode mode) {
94+
return UrlLauncherPlatform.instance.supportsMode(mode);
95+
}

packages/url_launcher/url_launcher/pubspec.yaml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ description: Flutter plugin for launching a URL. Supports
33
web, phone, SMS, and email schemes.
44
repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
6-
version: 6.1.14
6+
version: 6.2.0
77

88
environment:
9-
sdk: ">=3.0.0 <4.0.0"
10-
flutter: ">=3.10.0"
9+
sdk: ">=3.1.0 <4.0.0"
10+
flutter: ">=3.13.0"
1111

1212
flutter:
1313
plugin:
@@ -28,15 +28,15 @@ flutter:
2828
dependencies:
2929
flutter:
3030
sdk: flutter
31-
url_launcher_android: ^6.0.13
32-
url_launcher_ios: ^6.0.13
31+
url_launcher_android: ^6.2.0
32+
url_launcher_ios: ^6.2.0
3333
# Allow either the pure-native or Dart/native hybrid versions of the desktop
3434
# implementations, as both are compatible.
35-
url_launcher_linux: ">=2.0.0 <4.0.0"
36-
url_launcher_macos: ">=2.0.0 <4.0.0"
37-
url_launcher_platform_interface: ^2.1.0
38-
url_launcher_web: ^2.0.0
39-
url_launcher_windows: ">=2.0.0 <4.0.0"
35+
url_launcher_linux: ^3.1.0
36+
url_launcher_macos: ^3.1.0
37+
url_launcher_platform_interface: ^2.2.0
38+
url_launcher_web: ^2.2.0
39+
url_launcher_windows: ^3.1.0
4040

4141
dev_dependencies:
4242
flutter_test:

packages/url_launcher/url_launcher/test/link_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ void main() {
8686
mock
8787
..setLaunchExpectations(
8888
url: 'http://example.com/foobar',
89-
launchMode: PreferredLaunchMode.inAppWebView,
89+
launchMode: PreferredLaunchMode.inAppBrowserView,
9090
universalLinksOnly: false,
9191
enableJavaScript: true,
9292
enableDomStorage: true,

packages/url_launcher/url_launcher/test/mocks/mock_url_launcher_platform.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,16 @@ class MockUrlLauncher extends Fake
107107
Future<void> closeWebView() async {
108108
closeWebViewCalled = true;
109109
}
110+
111+
@override
112+
Future<bool> supportsMode(PreferredLaunchMode mode) async {
113+
launchMode = mode;
114+
return response!;
115+
}
116+
117+
@override
118+
Future<bool> supportsCloseForMode(PreferredLaunchMode mode) async {
119+
launchMode = mode;
120+
return response!;
121+
}
110122
}

packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,40 @@ void main() {
248248
expect(await launchUrl(emailLaunchUrl), isTrue);
249249
});
250250
});
251+
252+
group('supportsLaunchMode', () {
253+
test('handles returning true', () async {
254+
const PreferredLaunchMode mode = PreferredLaunchMode.inAppBrowserView;
255+
mock.setResponse(true);
256+
257+
expect(await supportsLaunchMode(mode), true);
258+
expect(mock.launchMode, mode);
259+
});
260+
261+
test('handles returning false', () async {
262+
const PreferredLaunchMode mode = PreferredLaunchMode.inAppBrowserView;
263+
mock.setResponse(false);
264+
265+
expect(await supportsLaunchMode(mode), false);
266+
expect(mock.launchMode, mode);
267+
});
268+
});
269+
270+
group('supportsCloseForLaunchMode', () {
271+
test('handles returning true', () async {
272+
const PreferredLaunchMode mode = PreferredLaunchMode.inAppBrowserView;
273+
mock.setResponse(true);
274+
275+
expect(await supportsCloseForLaunchMode(mode), true);
276+
expect(mock.launchMode, mode);
277+
});
278+
279+
test('handles returning false', () async {
280+
const PreferredLaunchMode mode = PreferredLaunchMode.inAppBrowserView;
281+
mock.setResponse(false);
282+
283+
expect(await supportsCloseForLaunchMode(mode), false);
284+
expect(mock.launchMode, mode);
285+
});
286+
});
251287
}

0 commit comments

Comments
 (0)