[url_launcher] Add an `inAppBrowserView` mode in implementations (#5211)

Implementation package portion of https://github.com/flutter/packages/pull/5155

This adds:
- Android support for the 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.
  - `inAppBrowserView` will still automatically fall back to `inAppBrowserView` in cases where it's not supported. (In the future, we might want to tune that based on feedback. We could instead have three modes: the webview-only mode we now have, the dynamic mode we now have iff the user requested `platformDefault`, and a new Android Custom Tabs-only if it was explicitly requested which would fail if it doesn't work.)
- iOS support for treating `inAppBrowserView` as identical to `inAppWebView`, since in practice that's what its `inAppWebView` mode has always been.
- Support on all platforms for the new `supportsMode` and `supportsCloseForMode` support query methods.

Fixes https://github.com/flutter/flutter/issues/134208
diff --git a/packages/url_launcher/url_launcher_android/CHANGELOG.md b/packages/url_launcher/url_launcher_android/CHANGELOG.md
index b9270e6..cf0da2f 100644
--- a/packages/url_launcher/url_launcher_android/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_android/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 6.2.0
+
+* Adds support for `inAppBrowserView` as a separate launch mode option from
+  `inAppWebView` mode. `inAppBrowserView` is the preferred in-app mode for most uses,
+  but does not support `closeInAppWebView`.
+* Implements `supportsMode` and `supportsCloseForMode`.
+
 ## 6.1.1
 
 * Updates annotations lib to 1.7.0.
diff --git a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/Messages.java b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/Messages.java
index eab0d87..f2294f0 100644
--- a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/Messages.java
+++ b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/Messages.java
@@ -1,7 +1,7 @@
 // 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.
-// Autogenerated from Pigeon (v10.0.0), do not edit directly.
+// Autogenerated from Pigeon (v10.1.4), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
 package io.flutter.plugins.urllauncher;
@@ -190,9 +190,15 @@
     /** Opens the URL externally, returning true if successful. */
     @NonNull
     Boolean launchUrl(@NonNull String url, @NonNull Map<String, String> headers);
-    /** Opens the URL in an in-app WebView, returning true if it opens successfully. */
+    /**
+     * Opens the URL in an in-app Custom Tab or WebView, returning true if it opens successfully.
+     */
     @NonNull
-    Boolean openUrlInWebView(@NonNull String url, @NonNull WebViewOptions options);
+    Boolean openUrlInApp(
+        @NonNull String url, @NonNull Boolean allowCustomTab, @NonNull WebViewOptions options);
+
+    @NonNull
+    Boolean supportsCustomTabs();
     /** Closes the view opened by [openUrlInSafariViewController]. */
     void closeWebView();
 
@@ -205,7 +211,9 @@
       {
         BasicMessageChannel<Object> channel =
             new BasicMessageChannel<>(
-                binaryMessenger, "dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl", getCodec());
+                binaryMessenger,
+                "dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.canLaunchUrl",
+                getCodec());
         if (api != null) {
           channel.setMessageHandler(
               (message, reply) -> {
@@ -228,7 +236,9 @@
       {
         BasicMessageChannel<Object> channel =
             new BasicMessageChannel<>(
-                binaryMessenger, "dev.flutter.pigeon.UrlLauncherApi.launchUrl", getCodec());
+                binaryMessenger,
+                "dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.launchUrl",
+                getCodec());
         if (api != null) {
           channel.setMessageHandler(
               (message, reply) -> {
@@ -252,16 +262,19 @@
       {
         BasicMessageChannel<Object> channel =
             new BasicMessageChannel<>(
-                binaryMessenger, "dev.flutter.pigeon.UrlLauncherApi.openUrlInWebView", getCodec());
+                binaryMessenger,
+                "dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.openUrlInApp",
+                getCodec());
         if (api != null) {
           channel.setMessageHandler(
               (message, reply) -> {
                 ArrayList<Object> wrapped = new ArrayList<Object>();
                 ArrayList<Object> args = (ArrayList<Object>) message;
                 String urlArg = (String) args.get(0);
-                WebViewOptions optionsArg = (WebViewOptions) args.get(1);
+                Boolean allowCustomTabArg = (Boolean) args.get(1);
+                WebViewOptions optionsArg = (WebViewOptions) args.get(2);
                 try {
-                  Boolean output = api.openUrlInWebView(urlArg, optionsArg);
+                  Boolean output = api.openUrlInApp(urlArg, allowCustomTabArg, optionsArg);
                   wrapped.add(0, output);
                 } catch (Throwable exception) {
                   ArrayList<Object> wrappedError = wrapError(exception);
@@ -276,7 +289,32 @@
       {
         BasicMessageChannel<Object> channel =
             new BasicMessageChannel<>(
-                binaryMessenger, "dev.flutter.pigeon.UrlLauncherApi.closeWebView", getCodec());
+                binaryMessenger,
+                "dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.supportsCustomTabs",
+                getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                ArrayList<Object> wrapped = new ArrayList<Object>();
+                try {
+                  Boolean output = api.supportsCustomTabs();
+                  wrapped.add(0, output);
+                } catch (Throwable exception) {
+                  ArrayList<Object> wrappedError = wrapError(exception);
+                  wrapped = wrappedError;
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger,
+                "dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.closeWebView",
+                getCodec());
         if (api != null) {
           channel.setMessageHandler(
               (message, reply) -> {
diff --git a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java
index 8ee9bff..028338c 100644
--- a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java
+++ b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java
@@ -16,9 +16,11 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.browser.customtabs.CustomTabsClient;
 import androidx.browser.customtabs.CustomTabsIntent;
 import io.flutter.plugins.urllauncher.Messages.UrlLauncherApi;
 import io.flutter.plugins.urllauncher.Messages.WebViewOptions;
+import java.util.Collections;
 import java.util.Locale;
 import java.util.Map;
 
@@ -95,14 +97,16 @@
   }
 
   @Override
-  public @NonNull Boolean openUrlInWebView(@NonNull String url, @NonNull WebViewOptions options) {
+  public @NonNull Boolean openUrlInApp(
+      @NonNull String url, @NonNull Boolean allowCustomTab, @NonNull WebViewOptions options) {
     ensureActivity();
     assert activity != null;
 
     Bundle headersBundle = extractBundle(options.getHeaders());
 
-    // Try to launch using Custom Tabs if they have the necessary functionality.
-    if (!containsRestrictedHeader(options.getHeaders())) {
+    // Try to launch using Custom Tabs if they have the necessary functionality, unless the caller
+    // specifically requested a web view.
+    if (allowCustomTab && !containsRestrictedHeader(options.getHeaders())) {
       Uri uri = Uri.parse(url);
       if (openCustomTab(activity, uri, headersBundle)) {
         return true;
@@ -131,6 +135,11 @@
     applicationContext.sendBroadcast(new Intent(WebViewActivity.ACTION_CLOSE));
   }
 
+  @Override
+  public @NonNull Boolean supportsCustomTabs() {
+    return CustomTabsClient.getPackageName(applicationContext, Collections.emptyList()) != null;
+  }
+
   private static boolean openCustomTab(
       @NonNull Context context, @NonNull Uri uri, @NonNull Bundle headersBundle) {
     CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
diff --git a/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/UrlLauncherTest.java b/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/UrlLauncherTest.java
index b8bb3b4..3bffbc6 100644
--- a/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/UrlLauncherTest.java
+++ b/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/UrlLauncherTest.java
@@ -130,7 +130,7 @@
   }
 
   @Test
-  public void openWebView_opensUrl_inWebView() {
+  public void openUrlInApp_opensUrlInWebViewIfNecessary() {
     Activity activity = mock(Activity.class);
     UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
     api.setActivity(activity);
@@ -141,8 +141,9 @@
     headers.put("key", "value");
 
     boolean result =
-        api.openUrlInWebView(
+        api.openUrlInApp(
             url,
+            true,
             new Messages.WebViewOptions.Builder()
                 .setEnableJavaScript(enableJavaScript)
                 .setEnableDomStorage(enableDomStorage)
@@ -162,15 +163,39 @@
   }
 
   @Test
-  public void openWebView_opensUrl_inCustomTabs() {
+  public void openWebView_opensUrlInWebViewIfRequested() {
     Activity activity = mock(Activity.class);
     UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
     api.setActivity(activity);
     String url = "https://flutter.dev";
 
     boolean result =
-        api.openUrlInWebView(
+        api.openUrlInApp(
             url,
+            false,
+            new Messages.WebViewOptions.Builder()
+                .setEnableJavaScript(false)
+                .setEnableDomStorage(false)
+                .setHeaders(new HashMap<>())
+                .build());
+
+    final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+    verify(activity).startActivity(intentCaptor.capture());
+    assertTrue(result);
+    assertEquals(url, intentCaptor.getValue().getExtras().getString(WebViewActivity.URL_EXTRA));
+  }
+
+  @Test
+  public void openWebView_opensUrlInCustomTabs() {
+    Activity activity = mock(Activity.class);
+    UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
+    api.setActivity(activity);
+    String url = "https://flutter.dev";
+
+    boolean result =
+        api.openUrlInApp(
+            url,
+            true,
             new Messages.WebViewOptions.Builder()
                 .setEnableJavaScript(false)
                 .setEnableDomStorage(false)
@@ -185,7 +210,7 @@
   }
 
   @Test
-  public void openWebView_opensUrl_inCustomTabs_withCORSAllowedHeader() {
+  public void openWebView_opensUrlInCustomTabsWithCORSAllowedHeader() {
     Activity activity = mock(Activity.class);
     UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
     api.setActivity(activity);
@@ -195,8 +220,9 @@
     headers.put(headerKey, "text/plain");
 
     boolean result =
-        api.openUrlInWebView(
+        api.openUrlInApp(
             url,
+            true,
             new Messages.WebViewOptions.Builder()
                 .setEnableJavaScript(false)
                 .setEnableDomStorage(false)
@@ -214,7 +240,7 @@
   }
 
   @Test
-  public void openWebView_fallsbackTo_inWebView() {
+  public void openWebView_fallsBackToWebViewIfCustomTabFails() {
     Activity activity = mock(Activity.class);
     UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
     api.setActivity(activity);
@@ -224,8 +250,9 @@
         .startActivity(any(), isNull()); // for custom tabs intent
 
     boolean result =
-        api.openUrlInWebView(
+        api.openUrlInApp(
             url,
+            true,
             new Messages.WebViewOptions.Builder()
                 .setEnableJavaScript(false)
                 .setEnableDomStorage(false)
@@ -251,8 +278,9 @@
     HashMap<String, String> headers = new HashMap<>();
     headers.put("key", "value");
 
-    api.openUrlInWebView(
+    api.openUrlInApp(
         "https://flutter.dev",
+        true,
         new Messages.WebViewOptions.Builder()
             .setEnableJavaScript(enableJavaScript)
             .setEnableDomStorage(false)
@@ -277,8 +305,9 @@
     headers.put(key1, "value");
     headers.put(key2, "value2");
 
-    api.openUrlInWebView(
+    api.openUrlInApp(
         "https://flutter.dev",
+        true,
         new Messages.WebViewOptions.Builder()
             .setEnableJavaScript(false)
             .setEnableDomStorage(false)
@@ -303,8 +332,9 @@
     HashMap<String, String> headers = new HashMap<>();
     headers.put("key", "value");
 
-    api.openUrlInWebView(
+    api.openUrlInApp(
         "https://flutter.dev",
+        true,
         new Messages.WebViewOptions.Builder()
             .setEnableJavaScript(false)
             .setEnableDomStorage(enableDomStorage)
@@ -327,8 +357,9 @@
         assertThrows(
             Messages.FlutterError.class,
             () ->
-                api.openUrlInWebView(
+                api.openUrlInApp(
                     "https://flutter.dev",
+                    true,
                     new Messages.WebViewOptions.Builder()
                         .setEnableJavaScript(false)
                         .setEnableDomStorage(false)
@@ -350,8 +381,9 @@
         .startActivity(any()); // for webview intent
 
     boolean result =
-        api.openUrlInWebView(
+        api.openUrlInApp(
             "https://flutter.dev",
+            true,
             new Messages.WebViewOptions.Builder()
                 .setEnableJavaScript(false)
                 .setEnableDomStorage(false)
diff --git a/packages/url_launcher/url_launcher_android/example/lib/main.dart b/packages/url_launcher/url_launcher_android/example/lib/main.dart
index df28069..36c32f2 100644
--- a/packages/url_launcher/url_launcher_android/example/lib/main.dart
+++ b/packages/url_launcher/url_launcher_android/example/lib/main.dart
@@ -39,6 +39,7 @@
 class _MyHomePageState extends State<MyHomePage> {
   final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance;
   bool _hasCallSupport = false;
+  bool _hasCustomTabSupport = false;
   Future<void>? _launched;
   String _phone = '';
 
@@ -51,73 +52,77 @@
         _hasCallSupport = result;
       });
     });
+    // Check for Android Custom Tab support.
+    launcher
+        .supportsMode(PreferredLaunchMode.inAppBrowserView)
+        .then((bool result) {
+      setState(() {
+        _hasCustomTabSupport = result;
+      });
+    });
   }
 
   Future<void> _launchInBrowser(String url) async {
-    if (!await launcher.launch(
+    if (!await launcher.launchUrl(
       url,
-      useSafariVC: false,
-      useWebView: false,
-      enableJavaScript: false,
-      enableDomStorage: false,
-      universalLinksOnly: false,
-      headers: <String, String>{},
+      const LaunchOptions(mode: PreferredLaunchMode.externalApplication),
+    )) {
+      throw Exception('Could not launch $url');
+    }
+  }
+
+  Future<void> _launchInCustomTab(String url) async {
+    if (!await launcher.launchUrl(
+      url,
+      const LaunchOptions(mode: PreferredLaunchMode.inAppBrowserView),
     )) {
       throw Exception('Could not launch $url');
     }
   }
 
   Future<void> _launchInWebView(String url) async {
-    if (!await launcher.launch(
+    if (!await launcher.launchUrl(
       url,
-      useSafariVC: true,
-      useWebView: true,
-      enableJavaScript: false,
-      enableDomStorage: false,
-      universalLinksOnly: false,
-      headers: <String, String>{},
+      const LaunchOptions(mode: PreferredLaunchMode.inAppWebView),
     )) {
       throw Exception('Could not launch $url');
     }
   }
 
   Future<void> _launchInWebViewWithCustomHeaders(String url) async {
-    if (!await launcher.launch(
+    if (!await launcher.launchUrl(
       url,
-      useSafariVC: true,
-      useWebView: true,
-      enableJavaScript: false,
-      enableDomStorage: false,
-      universalLinksOnly: false,
-      headers: <String, String>{'my_header_key': 'my_header_value'},
+      const LaunchOptions(
+          mode: PreferredLaunchMode.inAppWebView,
+          webViewConfiguration: InAppWebViewConfiguration(
+            headers: <String, String>{'my_header_key': 'my_header_value'},
+          )),
     )) {
       throw Exception('Could not launch $url');
     }
   }
 
-  Future<void> _launchInWebViewWithJavaScript(String url) async {
-    if (!await launcher.launch(
+  Future<void> _launchInWebViewWithoutJavaScript(String url) async {
+    if (!await launcher.launchUrl(
       url,
-      useSafariVC: true,
-      useWebView: true,
-      enableJavaScript: true,
-      enableDomStorage: false,
-      universalLinksOnly: false,
-      headers: <String, String>{},
+      const LaunchOptions(
+          mode: PreferredLaunchMode.inAppWebView,
+          webViewConfiguration: InAppWebViewConfiguration(
+            enableJavaScript: false,
+          )),
     )) {
       throw Exception('Could not launch $url');
     }
   }
 
-  Future<void> _launchInWebViewWithDomStorage(String url) async {
-    if (!await launcher.launch(
+  Future<void> _launchInWebViewWithoutDomStorage(String url) async {
+    if (!await launcher.launchUrl(
       url,
-      useSafariVC: true,
-      useWebView: true,
-      enableJavaScript: false,
-      enableDomStorage: true,
-      universalLinksOnly: false,
-      headers: <String, String>{},
+      const LaunchOptions(
+          mode: PreferredLaunchMode.inAppWebView,
+          webViewConfiguration: InAppWebViewConfiguration(
+            enableDomStorage: false,
+          )),
     )) {
       throw Exception('Could not launch $url');
     }
@@ -133,22 +138,12 @@
 
   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.
+    // Just using 'tel:$phoneNumber' would create invalid URLs in some cases.
     final Uri launchUri = Uri(
       scheme: 'tel',
       path: phoneNumber,
     );
-    await launcher.launch(
-      launchUri.toString(),
-      useSafariVC: false,
-      useWebView: false,
-      enableJavaScript: false,
-      enableDomStorage: false,
-      universalLinksOnly: true,
-      headers: <String, String>{},
-    );
+    await launcher.launchUrl(launchUri.toString(), const LaunchOptions());
   }
 
   @override
@@ -187,35 +182,44 @@
                 child: Text(toLaunch),
               ),
               ElevatedButton(
-                onPressed: () => setState(() {
-                  _launched = _launchInBrowser(toLaunch);
-                }),
+                onPressed: _hasCustomTabSupport
+                    ? () => setState(() {
+                          _launched = _launchInBrowser(toLaunch);
+                        })
+                    : null,
                 child: const Text('Launch in browser'),
               ),
               const Padding(padding: EdgeInsets.all(16.0)),
               ElevatedButton(
                 onPressed: () => setState(() {
+                  _launched = _launchInCustomTab(toLaunch);
+                }),
+                child: const Text('Launch in Android Custom Tab'),
+              ),
+              const Padding(padding: EdgeInsets.all(16.0)),
+              ElevatedButton(
+                onPressed: () => setState(() {
                   _launched = _launchInWebView(toLaunch);
                 }),
-                child: const Text('Launch in app'),
+                child: const Text('Launch in web view'),
               ),
               ElevatedButton(
                 onPressed: () => setState(() {
                   _launched = _launchInWebViewWithCustomHeaders(toLaunch);
                 }),
-                child: const Text('Launch in app (Custom headers)'),
+                child: const Text('Launch in web view (Custom headers)'),
               ),
               ElevatedButton(
                 onPressed: () => setState(() {
-                  _launched = _launchInWebViewWithJavaScript(toLaunch);
+                  _launched = _launchInWebViewWithoutJavaScript(toLaunch);
                 }),
-                child: const Text('Launch in app (JavaScript ON)'),
+                child: const Text('Launch in web view (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 web view (DOM storage OFF)'),
               ),
               const Padding(padding: EdgeInsets.all(16.0)),
               ElevatedButton(
@@ -225,7 +229,7 @@
                     launcher.closeWebView();
                   });
                 }),
-                child: const Text('Launch in app + close after 5 seconds'),
+                child: const Text('Launch in web view + close after 5 seconds'),
               ),
               const Padding(padding: EdgeInsets.all(16.0)),
               FutureBuilder<void>(future: _launched, builder: _launchStatus),
diff --git a/packages/url_launcher/url_launcher_android/example/pubspec.yaml b/packages/url_launcher/url_launcher_android/example/pubspec.yaml
index e8aee17..d234884 100644
--- a/packages/url_launcher/url_launcher_android/example/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_android/example/pubspec.yaml
@@ -16,7 +16,7 @@
     # The example app is bundled with the plugin so we use a path dependency on
     # the parent directory to use the current plugin's version.
     path: ../
-  url_launcher_platform_interface: ^2.0.3
+  url_launcher_platform_interface: ^2.2.0
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/url_launcher/url_launcher_android/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_android/lib/src/messages.g.dart
index 9aed8f7..9d6ce26 100644
--- a/packages/url_launcher/url_launcher_android/lib/src/messages.g.dart
+++ b/packages/url_launcher/url_launcher_android/lib/src/messages.g.dart
@@ -1,7 +1,7 @@
 // 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.
-// Autogenerated from Pigeon (v10.0.0), do not edit directly.
+// Autogenerated from Pigeon (v10.1.4), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
 
@@ -79,7 +79,8 @@
   /// Returns true if the URL can definitely be launched.
   Future<bool> canLaunchUrl(String arg_url) async {
     final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
-        'dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl', codec,
+        'dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.canLaunchUrl',
+        codec,
         binaryMessenger: _binaryMessenger);
     final List<Object?>? replyList =
         await channel.send(<Object?>[arg_url]) as List<Object?>?;
@@ -108,7 +109,8 @@
   Future<bool> launchUrl(
       String arg_url, Map<String?, String?> arg_headers) async {
     final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
-        'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec,
+        'dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.launchUrl',
+        codec,
         binaryMessenger: _binaryMessenger);
     final List<Object?>? replyList =
         await channel.send(<Object?>[arg_url, arg_headers]) as List<Object?>?;
@@ -133,15 +135,44 @@
     }
   }
 
-  /// Opens the URL in an in-app WebView, returning true if it opens
-  /// successfully.
-  Future<bool> openUrlInWebView(
-      String arg_url, WebViewOptions arg_options) async {
+  /// Opens the URL in an in-app Custom Tab or WebView, returning true if it
+  /// opens successfully.
+  Future<bool> openUrlInApp(String arg_url, bool arg_allowCustomTab,
+      WebViewOptions arg_options) async {
     final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
-        'dev.flutter.pigeon.UrlLauncherApi.openUrlInWebView', codec,
+        'dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.openUrlInApp',
+        codec,
         binaryMessenger: _binaryMessenger);
     final List<Object?>? replyList =
-        await channel.send(<Object?>[arg_url, arg_options]) as List<Object?>?;
+        await channel.send(<Object?>[arg_url, arg_allowCustomTab, arg_options])
+            as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else if (replyList[0] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyList[0] as bool?)!;
+    }
+  }
+
+  Future<bool> supportsCustomTabs() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.supportsCustomTabs',
+        codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
     if (replyList == null) {
       throw PlatformException(
         code: 'channel-error',
@@ -166,7 +197,8 @@
   /// Closes the view opened by [openUrlInSafariViewController].
   Future<void> closeWebView() async {
     final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
-        'dev.flutter.pigeon.UrlLauncherApi.closeWebView', codec,
+        'dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.closeWebView',
+        codec,
         binaryMessenger: _binaryMessenger);
     final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
     if (replyList == null) {
diff --git a/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart
index 7b53b85..f121084 100644
--- a/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart
+++ b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart
@@ -49,8 +49,6 @@
     return _hostApi.closeWebView();
   }
 
-  // TODO(stuartmorgan): Implement launchUrl, and make this a passthrough
-  // to launchUrl. See also https://github.com/flutter/flutter/issues/66721
   @override
   Future<bool> launch(
     String url, {
@@ -62,16 +60,57 @@
     required Map<String, String> headers,
     String? webOnlyWindowName,
   }) async {
+    return launchUrl(
+        url,
+        LaunchOptions(
+            mode: useWebView
+                ? PreferredLaunchMode.inAppWebView
+                : PreferredLaunchMode.externalApplication,
+            webViewConfiguration: InAppWebViewConfiguration(
+                enableDomStorage: enableDomStorage,
+                enableJavaScript: enableJavaScript,
+                headers: headers)));
+  }
+
+  @override
+  Future<bool> launchUrl(String url, LaunchOptions options) async {
+    final bool inApp;
+    switch (options.mode) {
+      case PreferredLaunchMode.inAppWebView:
+      case PreferredLaunchMode.inAppBrowserView:
+        inApp = true;
+        break;
+      case PreferredLaunchMode.externalApplication:
+      case PreferredLaunchMode.externalNonBrowserApplication:
+        // TODO(stuartmorgan): Add full support for
+        // externalNonBrowsingApplication; see
+        // https://github.com/flutter/flutter/issues/66721.
+        // Currently it's treated the same as externalApplication.
+        inApp = false;
+        break;
+      case PreferredLaunchMode.platformDefault:
+      // Intentionally treat any new values as platformDefault; see comment in
+      // supportsMode.
+      // ignore: no_default_cases
+      default:
+        // By default, open web URLs in the application.
+        inApp = url.startsWith('http:') || url.startsWith('https:');
+        break;
+    }
+
     final bool succeeded;
-    if (useWebView) {
-      succeeded = await _hostApi.openUrlInWebView(
+    if (inApp) {
+      succeeded = await _hostApi.openUrlInApp(
           url,
+          // Prefer custom tabs unless a webview was specifically requested.
+          options.mode != PreferredLaunchMode.inAppWebView,
           WebViewOptions(
-              enableJavaScript: enableJavaScript,
-              enableDomStorage: enableDomStorage,
-              headers: headers));
+              enableJavaScript: options.webViewConfiguration.enableJavaScript,
+              enableDomStorage: options.webViewConfiguration.enableDomStorage,
+              headers: options.webViewConfiguration.headers));
     } else {
-      succeeded = await _hostApi.launchUrl(url, headers);
+      succeeded =
+          await _hostApi.launchUrl(url, options.webViewConfiguration.headers);
     }
     // TODO(stuartmorgan): Remove this special handling as part of a
     // breaking change to rework failure handling across all platform. The
@@ -84,6 +123,29 @@
     return succeeded;
   }
 
+  @override
+  Future<bool> supportsMode(PreferredLaunchMode mode) async {
+    switch (mode) {
+      case PreferredLaunchMode.platformDefault:
+      case PreferredLaunchMode.inAppWebView:
+      case PreferredLaunchMode.externalApplication:
+        return true;
+      case PreferredLaunchMode.inAppBrowserView:
+        return _hostApi.supportsCustomTabs();
+      // Default is a desired behavior here since support for new modes is
+      // always opt-in, and the enum lives in a different package, so silently
+      // adding "false" for new values is the correct behavior.
+      // ignore: no_default_cases
+      default:
+        return false;
+    }
+  }
+
+  @override
+  Future<bool> supportsCloseForMode(PreferredLaunchMode mode) async {
+    return mode == PreferredLaunchMode.inAppWebView;
+  }
+
   // Returns the part of [url] up to the first ':', or an empty string if there
   // is no ':'. This deliberately does not use [Uri] to extract the scheme
   // so that it works on strings that aren't actually valid URLs, since Android
diff --git a/packages/url_launcher/url_launcher_android/pigeons/messages.dart b/packages/url_launcher/url_launcher_android/pigeons/messages.dart
index 84e507d..d718441 100644
--- a/packages/url_launcher/url_launcher_android/pigeons/messages.dart
+++ b/packages/url_launcher/url_launcher_android/pigeons/messages.dart
@@ -33,9 +33,11 @@
   /// Opens the URL externally, returning true if successful.
   bool launchUrl(String url, Map<String, String> headers);
 
-  /// Opens the URL in an in-app WebView, returning true if it opens
-  /// successfully.
-  bool openUrlInWebView(String url, WebViewOptions options);
+  /// Opens the URL in an in-app Custom Tab or WebView, returning true if it
+  /// opens successfully.
+  bool openUrlInApp(String url, bool allowCustomTab, WebViewOptions options);
+
+  bool supportsCustomTabs();
 
   /// Closes the view opened by [openUrlInSafariViewController].
   void closeWebView();
diff --git a/packages/url_launcher/url_launcher_android/pubspec.yaml b/packages/url_launcher/url_launcher_android/pubspec.yaml
index 09e865d..5b5d06e 100644
--- a/packages/url_launcher/url_launcher_android/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_android/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Android implementation of the url_launcher plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
-version: 6.1.1
+version: 6.2.0
 environment:
   sdk: ">=2.19.0 <4.0.0"
   flutter: ">=3.7.0"
@@ -19,7 +19,7 @@
 dependencies:
   flutter:
     sdk: flutter
-  url_launcher_platform_interface: ^2.0.3
+  url_launcher_platform_interface: ^2.2.0
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart
index 3b3d012..2b331cb 100644
--- a/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart
+++ b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart
@@ -52,7 +52,7 @@
     });
   });
 
-  group('launch without webview', () {
+  group('legacy launch without webview', () {
     test('calls through', () async {
       final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
       final bool launched = await launcher.launch(
@@ -88,7 +88,7 @@
       final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
       await expectLater(
           launcher.launch(
-            'noactivity://',
+            'https://noactivity',
             useSafariVC: false,
             useWebView: false,
             enableJavaScript: false,
@@ -116,7 +116,7 @@
     });
   });
 
-  group('launch with webview', () {
+  group('legacy launch with webview', () {
     test('calls through', () async {
       final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
       final bool launched = await launcher.launch(
@@ -130,6 +130,7 @@
       );
       expect(launched, true);
       expect(api.usedWebView, true);
+      expect(api.allowedCustomTab, false);
       expect(api.passedWebViewOptions?.enableDomStorage, false);
       expect(api.passedWebViewOptions?.enableJavaScript, false);
       expect(api.passedWebViewOptions?.headers, isEmpty);
@@ -169,7 +170,7 @@
       final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
       await expectLater(
           launcher.launch(
-            'noactivity://scheme',
+            'https://noactivity',
             useSafariVC: false,
             useWebView: true,
             enableJavaScript: false,
@@ -197,12 +198,198 @@
     });
   });
 
-  group('closeWebView', () {
+  group('launch without webview', () {
     test('calls through', () async {
       final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
-      await launcher.closeWebView();
+      final bool launched = await launcher.launchUrl(
+        'http://example.com/',
+        const LaunchOptions(mode: PreferredLaunchMode.externalApplication),
+      );
+      expect(launched, true);
+      expect(api.usedWebView, false);
+      expect(api.passedWebViewOptions?.headers, isEmpty);
+    });
 
-      expect(api.closed, true);
+    test('passes headers', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      await launcher.launchUrl(
+        'http://example.com/',
+        const LaunchOptions(
+            mode: PreferredLaunchMode.externalApplication,
+            webViewConfiguration: InAppWebViewConfiguration(
+                headers: <String, String>{'key': 'value'})),
+      );
+      expect(api.passedWebViewOptions?.headers.length, 1);
+      expect(api.passedWebViewOptions?.headers['key'], 'value');
+    });
+
+    test('passes through no-activity exception', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      await expectLater(
+          launcher.launchUrl('https://noactivity', const LaunchOptions()),
+          throwsA(isA<PlatformException>()));
+    });
+
+    test('throws if there is no handling activity', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      await expectLater(
+          launcher.launchUrl('unknown://scheme', const LaunchOptions()),
+          throwsA(isA<PlatformException>().having(
+              (PlatformException e) => e.code, 'code', 'ACTIVITY_NOT_FOUND')));
+    });
+  });
+
+  group('launch with webview', () {
+    test('calls through', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      final bool launched = await launcher.launchUrl('http://example.com/',
+          const LaunchOptions(mode: PreferredLaunchMode.inAppWebView));
+      expect(launched, true);
+      expect(api.usedWebView, true);
+      expect(api.allowedCustomTab, false);
+      expect(api.passedWebViewOptions?.enableDomStorage, true);
+      expect(api.passedWebViewOptions?.enableJavaScript, true);
+      expect(api.passedWebViewOptions?.headers, isEmpty);
+    });
+
+    test('passes enableJavaScript to webview', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      await launcher.launchUrl(
+          'http://example.com/',
+          const LaunchOptions(
+              mode: PreferredLaunchMode.inAppWebView,
+              webViewConfiguration:
+                  InAppWebViewConfiguration(enableJavaScript: false)));
+
+      expect(api.passedWebViewOptions?.enableJavaScript, false);
+    });
+
+    test('passes enableDomStorage to webview', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      await launcher.launchUrl(
+          'http://example.com/',
+          const LaunchOptions(
+              mode: PreferredLaunchMode.inAppWebView,
+              webViewConfiguration:
+                  InAppWebViewConfiguration(enableDomStorage: false)));
+
+      expect(api.passedWebViewOptions?.enableDomStorage, false);
+    });
+
+    test('passes through no-activity exception', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      await expectLater(
+          launcher.launchUrl('https://noactivity',
+              const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)),
+          throwsA(isA<PlatformException>()));
+    });
+
+    test('throws if there is no handling activity', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      await expectLater(
+          launcher.launchUrl('unknown://scheme',
+              const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)),
+          throwsA(isA<PlatformException>().having(
+              (PlatformException e) => e.code, 'code', 'ACTIVITY_NOT_FOUND')));
+    });
+  });
+
+  group('launch with custom tab', () {
+    test('calls through', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      final bool launched = await launcher.launchUrl('http://example.com/',
+          const LaunchOptions(mode: PreferredLaunchMode.inAppBrowserView));
+      expect(launched, true);
+      expect(api.usedWebView, true);
+      expect(api.allowedCustomTab, true);
+    });
+  });
+
+  group('launch with platform default', () {
+    test('uses custom tabs for http', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      final bool launched = await launcher.launchUrl(
+          'http://example.com/', const LaunchOptions());
+      expect(launched, true);
+      expect(api.usedWebView, true);
+      expect(api.allowedCustomTab, true);
+    });
+
+    test('uses custom tabs for https', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      final bool launched = await launcher.launchUrl(
+          'https://example.com/', const LaunchOptions());
+      expect(launched, true);
+      expect(api.usedWebView, true);
+      expect(api.allowedCustomTab, true);
+    });
+
+    test('uses external for other schemes', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      final bool launched = await launcher.launchUrl(
+          'supportedcustomscheme://example.com/', const LaunchOptions());
+      expect(launched, true);
+      expect(api.usedWebView, false);
+    });
+  });
+
+  group('supportsMode', () {
+    test('returns true for platformDefault', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      expect(await launcher.supportsMode(PreferredLaunchMode.platformDefault),
+          true);
+    });
+
+    test('returns true for external application', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      expect(
+          await launcher.supportsMode(PreferredLaunchMode.externalApplication),
+          true);
+    });
+
+    test('returns true for in app web view', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      expect(
+          await launcher.supportsMode(PreferredLaunchMode.inAppWebView), true);
+    });
+
+    test('returns true for in app browser view when available', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      api.hasCustomTabSupport = true;
+      expect(await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView),
+          true);
+    });
+
+    test('returns false for in app browser view when not available', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      api.hasCustomTabSupport = false;
+      expect(await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView),
+          false);
+    });
+  });
+
+  group('supportsCloseForMode', () {
+    test('returns true for in app web view', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      expect(
+          await launcher.supportsCloseForMode(PreferredLaunchMode.inAppWebView),
+          true);
+    });
+
+    test('returns false for other modes', () async {
+      final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api);
+      expect(
+          await launcher
+              .supportsCloseForMode(PreferredLaunchMode.externalApplication),
+          false);
+      expect(
+          await launcher.supportsCloseForMode(
+              PreferredLaunchMode.externalNonBrowserApplication),
+          false);
+      expect(
+          await launcher
+              .supportsCloseForMode(PreferredLaunchMode.inAppBrowserView),
+          false);
     });
   });
 }
@@ -211,8 +398,10 @@
 ///
 /// See _launch for the behaviors.
 class _FakeUrlLauncherApi implements UrlLauncherApi {
+  bool hasCustomTabSupport = true;
   WebViewOptions? passedWebViewOptions;
   bool? usedWebView;
+  bool? allowedCustomTab;
   bool? closed;
 
   /// A domain that will be treated as having no handler, even for http(s).
@@ -237,20 +426,29 @@
   }
 
   @override
-  Future<bool> openUrlInWebView(String url, WebViewOptions options) async {
+  Future<bool> openUrlInApp(
+      String url, bool allowCustomTab, WebViewOptions options) async {
     passedWebViewOptions = options;
     usedWebView = true;
+    allowedCustomTab = allowCustomTab;
     return _launch(url);
   }
 
+  @override
+  Future<bool> supportsCustomTabs() async {
+    return hasCustomTabSupport;
+  }
+
   bool _launch(String url) {
     final String scheme = url.split(':')[0];
     switch (scheme) {
       case 'http':
       case 'https':
+      case 'supportedcustomscheme':
+        if (url.endsWith('noactivity')) {
+          throw PlatformException(code: 'NO_ACTIVITY');
+        }
         return !url.contains(specialHandlerDomain);
-      case 'noactivity':
-        throw PlatformException(code: 'NO_ACTIVITY');
       default:
         return false;
     }
diff --git a/packages/url_launcher/url_launcher_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md
index ad52522..ae63012 100644
--- a/packages/url_launcher/url_launcher_ios/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 6.2.0
+
+* Implements `supportsMode` and `supportsCloseForMode`.
+
 ## 6.1.5
 
 * Adds pub topics to package metadata.
diff --git a/packages/url_launcher/url_launcher_ios/example/pubspec.yaml b/packages/url_launcher/url_launcher_ios/example/pubspec.yaml
index d755c55..3daaa9a 100644
--- a/packages/url_launcher/url_launcher_ios/example/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_ios/example/pubspec.yaml
@@ -16,7 +16,7 @@
     # The example app is bundled with the plugin so we use a path dependency on
     # the parent directory to use the current plugin's version.
     path: ../
-  url_launcher_platform_interface: ^2.0.3
+  url_launcher_platform_interface: ^2.2.0
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart
index 2f0e9f4..6696978 100644
--- a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart
+++ b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart
@@ -45,11 +45,79 @@
     required bool universalLinksOnly,
     required Map<String, String> headers,
     String? webOnlyWindowName,
-  }) {
+  }) async {
+    final PreferredLaunchMode mode;
     if (useSafariVC) {
+      mode = PreferredLaunchMode.inAppBrowserView;
+    } else if (universalLinksOnly) {
+      mode = PreferredLaunchMode.externalNonBrowserApplication;
+    } else {
+      mode = PreferredLaunchMode.externalApplication;
+    }
+    return launchUrl(
+        url,
+        LaunchOptions(
+            mode: mode,
+            webViewConfiguration: InAppWebViewConfiguration(
+                enableDomStorage: enableDomStorage,
+                enableJavaScript: enableJavaScript,
+                headers: headers)));
+  }
+
+  @override
+  Future<bool> launchUrl(String url, LaunchOptions options) async {
+    final bool inApp;
+    switch (options.mode) {
+      case PreferredLaunchMode.inAppWebView:
+      case PreferredLaunchMode.inAppBrowserView:
+        // The iOS implementation doesn't distinguish between these two modes;
+        // both are treated as inAppBrowserView.
+        inApp = true;
+        break;
+      case PreferredLaunchMode.externalApplication:
+      case PreferredLaunchMode.externalNonBrowserApplication:
+        inApp = false;
+        break;
+      case PreferredLaunchMode.platformDefault:
+      // Intentionally treat any new values as platformDefault; support for any
+      // new mode requires intentional opt-in, otherwise falling back is the
+      // documented behavior.
+      // ignore: no_default_cases
+      default:
+        // By default, open web URLs in the application.
+        inApp = url.startsWith('http:') || url.startsWith('https:');
+        break;
+    }
+
+    if (inApp) {
       return _hostApi.openUrlInSafariViewController(url);
     } else {
-      return _hostApi.launchUrl(url, universalLinksOnly);
+      return _hostApi.launchUrl(url,
+          options.mode == PreferredLaunchMode.externalNonBrowserApplication);
     }
   }
+
+  @override
+  Future<bool> supportsMode(PreferredLaunchMode mode) async {
+    switch (mode) {
+      case PreferredLaunchMode.platformDefault:
+      case PreferredLaunchMode.inAppWebView:
+      case PreferredLaunchMode.inAppBrowserView:
+      case PreferredLaunchMode.externalApplication:
+      case PreferredLaunchMode.externalNonBrowserApplication:
+        return true;
+      // Default is a desired behavior here since support for new modes is
+      // always opt-in, and the enum lives in a different package, so silently
+      // adding "false" for new values is the correct behavior.
+      // ignore: no_default_cases
+      default:
+        return false;
+    }
+  }
+
+  @override
+  Future<bool> supportsCloseForMode(PreferredLaunchMode mode) async {
+    return mode == PreferredLaunchMode.inAppWebView ||
+        mode == PreferredLaunchMode.inAppBrowserView;
+  }
 }
diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml
index 8b45ed7..6047568 100644
--- a/packages/url_launcher/url_launcher_ios/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_ios/pubspec.yaml
@@ -2,7 +2,7 @@
 description: iOS implementation of the url_launcher plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_ios
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
-version: 6.1.5
+version: 6.2.0
 
 environment:
   sdk: ">=2.19.0 <4.0.0"
@@ -19,7 +19,7 @@
 dependencies:
   flutter:
     sdk: flutter
-  url_launcher_platform_interface: ^2.0.3
+  url_launcher_platform_interface: ^2.2.0
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart
index 9274173..bacea31 100644
--- a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart
+++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart
@@ -11,36 +11,37 @@
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
 
-  group('UrlLauncherIOS', () {
-    late _FakeUrlLauncherApi api;
+  late _FakeUrlLauncherApi api;
 
-    setUp(() {
-      api = _FakeUrlLauncherApi();
-    });
+  setUp(() {
+    api = _FakeUrlLauncherApi();
+  });
 
-    test('registers instance', () {
-      UrlLauncherIOS.registerWith();
-      expect(UrlLauncherPlatform.instance, isA<UrlLauncherIOS>());
-    });
+  test('registers instance', () {
+    UrlLauncherIOS.registerWith();
+    expect(UrlLauncherPlatform.instance, isA<UrlLauncherIOS>());
+  });
 
-    test('canLaunch success', () async {
+  group('canLaunch', () {
+    test('handles success', () async {
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       expect(await launcher.canLaunch('http://example.com/'), true);
     });
 
-    test('canLaunch failure', () async {
+    test('handles failure', () async {
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       expect(await launcher.canLaunch('unknown://scheme'), false);
     });
 
-    test('canLaunch invalid URL passes the PlatformException through',
-        () async {
+    test('passes invalid URL PlatformException through', () async {
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       await expectLater(launcher.canLaunch('invalid://u r l'),
           throwsA(isA<PlatformException>()));
     });
+  });
 
-    test('launch success', () async {
+  group('legacy launch', () {
+    test('handles success', () async {
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       expect(
           await launcher.launch(
@@ -56,7 +57,7 @@
       expect(api.passedUniversalLinksOnly, false);
     });
 
-    test('launch failure', () async {
+    test('handles failure', () async {
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       expect(
           await launcher.launch(
@@ -72,7 +73,7 @@
       expect(api.passedUniversalLinksOnly, false);
     });
 
-    test('launch invalid URL passes the PlatformException through', () async {
+    test('passes invalid URL PlatformException through', () async {
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       await expectLater(
           launcher.launch(
@@ -87,7 +88,7 @@
           throwsA(isA<PlatformException>()));
     });
 
-    test('launch force SafariVC', () async {
+    test('force SafariVC is handled', () async {
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       expect(
           await launcher.launch(
@@ -103,7 +104,7 @@
       expect(api.usedSafariViewController, true);
     });
 
-    test('launch universal links only', () async {
+    test('universal links only is handled', () async {
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       expect(
           await launcher.launch(
@@ -119,7 +120,7 @@
       expect(api.passedUniversalLinksOnly, true);
     });
 
-    test('launch force SafariVC to false', () async {
+    test('disallowing SafariVC is handled', () async {
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
       expect(
           await launcher.launch(
@@ -134,11 +135,171 @@
           true);
       expect(api.usedSafariViewController, false);
     });
+  });
 
-    test('closeWebView default behavior', () async {
+  test('closeWebView calls through', () async {
+    final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+    await launcher.closeWebView();
+    expect(api.closed, true);
+  });
+
+  group('launch without webview', () {
+    test('calls through', () async {
       final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
-      await launcher.closeWebView();
-      expect(api.closed, true);
+      final bool launched = await launcher.launchUrl(
+        'http://example.com/',
+        const LaunchOptions(mode: PreferredLaunchMode.externalApplication),
+      );
+      expect(launched, true);
+      expect(api.usedSafariViewController, false);
+    });
+
+    test('passes invalid URL PlatformException through', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      await expectLater(
+          launcher.launchUrl('invalid://u r l', const LaunchOptions()),
+          throwsA(isA<PlatformException>()));
+    });
+  });
+
+  group('launch with Safari view controller', () {
+    test('calls through with inAppWebView', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      final bool launched = await launcher.launchUrl('http://example.com/',
+          const LaunchOptions(mode: PreferredLaunchMode.inAppWebView));
+      expect(launched, true);
+      expect(api.usedSafariViewController, true);
+    });
+
+    test('calls through with inAppBrowserView', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      final bool launched = await launcher.launchUrl('http://example.com/',
+          const LaunchOptions(mode: PreferredLaunchMode.inAppBrowserView));
+      expect(launched, true);
+      expect(api.usedSafariViewController, true);
+    });
+
+    test('passes invalid URL PlatformException through', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      await expectLater(
+          launcher.launchUrl('invalid://u r l',
+              const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)),
+          throwsA(isA<PlatformException>()));
+    });
+  });
+
+  group('launch with universal links', () {
+    test('calls through', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      final bool launched = await launcher.launchUrl(
+        'http://example.com/',
+        const LaunchOptions(
+            mode: PreferredLaunchMode.externalNonBrowserApplication),
+      );
+      expect(launched, true);
+      expect(api.usedSafariViewController, false);
+      expect(api.passedUniversalLinksOnly, true);
+    });
+
+    test('passes invalid URL PlatformException through', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      await expectLater(
+          launcher.launchUrl(
+              'invalid://u r l',
+              const LaunchOptions(
+                  mode: PreferredLaunchMode.externalNonBrowserApplication)),
+          throwsA(isA<PlatformException>()));
+    });
+  });
+
+  group('launch with platform default', () {
+    test('uses Safari view controller for http', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      final bool launched = await launcher.launchUrl(
+          'http://example.com/', const LaunchOptions());
+      expect(launched, true);
+      expect(api.usedSafariViewController, true);
+    });
+
+    test('uses Safari view controller for https', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      final bool launched = await launcher.launchUrl(
+          'https://example.com/', const LaunchOptions());
+      expect(launched, true);
+      expect(api.usedSafariViewController, true);
+    });
+
+    test('uses standard external for other schemes', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      final bool launched = await launcher.launchUrl(
+          'supportedcustomscheme://example.com/', const LaunchOptions());
+      expect(launched, true);
+      expect(api.usedSafariViewController, false);
+      expect(api.passedUniversalLinksOnly, false);
+    });
+  });
+
+  group('supportsMode', () {
+    test('returns true for platformDefault', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      expect(await launcher.supportsMode(PreferredLaunchMode.platformDefault),
+          true);
+    });
+
+    test('returns true for external application', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      expect(
+          await launcher.supportsMode(PreferredLaunchMode.externalApplication),
+          true);
+    });
+
+    test('returns true for external non-browser application', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      expect(
+          await launcher
+              .supportsMode(PreferredLaunchMode.externalNonBrowserApplication),
+          true);
+    });
+
+    test('returns true for in app web view', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      expect(
+          await launcher.supportsMode(PreferredLaunchMode.inAppWebView), true);
+    });
+
+    test('returns true for in app browser view', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      expect(await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView),
+          true);
+    });
+  });
+
+  group('supportsCloseForMode', () {
+    test('returns true for in app web view', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      expect(
+          await launcher.supportsCloseForMode(PreferredLaunchMode.inAppWebView),
+          true);
+    });
+
+    test('returns true for in app browser view', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      expect(
+          await launcher
+              .supportsCloseForMode(PreferredLaunchMode.inAppBrowserView),
+          true);
+    });
+
+    test('returns false for other modes', () async {
+      final UrlLauncherIOS launcher = UrlLauncherIOS(api: api);
+      expect(
+          await launcher
+              .supportsCloseForMode(PreferredLaunchMode.externalApplication),
+          false);
+      expect(
+          await launcher.supportsCloseForMode(
+              PreferredLaunchMode.externalNonBrowserApplication),
+          false);
     });
   });
 }
@@ -179,6 +340,7 @@
     switch (scheme) {
       case 'http':
       case 'https':
+      case 'supportedcustomscheme':
         return true;
       case 'invalid':
         throw PlatformException(code: 'argument_error');
diff --git a/packages/url_launcher/url_launcher_linux/CHANGELOG.md b/packages/url_launcher/url_launcher_linux/CHANGELOG.md
index a7f3087..538fb77 100644
--- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 3.1.0
+
+* Implements `supportsMode` and `supportsCloseForMode`.
+
 ## 3.0.6
 
 * Adds pub topics to package metadata.
diff --git a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml
index 0dd4a4c..92aff2e 100644
--- a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml
@@ -16,7 +16,7 @@
     # The example app is bundled with the plugin so we use a path dependency on
     # the parent directory to use the current plugin's version.
     path: ../
-  url_launcher_platform_interface: ^2.0.0
+  url_launcher_platform_interface: ^2.2.0
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart b/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart
index 87ef314..286ac92 100644
--- a/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart
+++ b/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart
@@ -51,4 +51,16 @@
       },
     ).then((bool? value) => value ?? false);
   }
+
+  @override
+  Future<bool> supportsMode(PreferredLaunchMode mode) async {
+    return mode == PreferredLaunchMode.platformDefault ||
+        mode == PreferredLaunchMode.externalApplication;
+  }
+
+  @override
+  Future<bool> supportsCloseForMode(PreferredLaunchMode mode) async {
+    // No supported mode is closeable.
+    return false;
+  }
 }
diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml
index 2649bee..315ecd2 100644
--- a/packages/url_launcher/url_launcher_linux/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Linux implementation of the url_launcher plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_linux
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
-version: 3.0.6
+version: 3.1.0
 
 environment:
   sdk: ">=2.19.0 <4.0.0"
@@ -19,7 +19,7 @@
 dependencies:
   flutter:
     sdk: flutter
-  url_launcher_platform_interface: ^2.0.3
+  url_launcher_platform_interface: ^2.2.0
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart b/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart
index 4e62cc4..c7e6c8e 100644
--- a/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart
+++ b/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart
@@ -10,7 +10,7 @@
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
 
-  group('$UrlLauncherLinux', () {
+  group('UrlLauncherLinux', () {
     const MethodChannel channel =
         MethodChannel('plugins.flutter.io/url_launcher_linux');
     final List<MethodCall> log = <MethodCall>[];
@@ -142,6 +142,47 @@
 
       expect(launched, false);
     });
+
+    group('supportsMode', () {
+      test('returns true for platformDefault', () async {
+        final UrlLauncherLinux launcher = UrlLauncherLinux();
+        expect(await launcher.supportsMode(PreferredLaunchMode.platformDefault),
+            true);
+      });
+
+      test('returns true for external application', () async {
+        final UrlLauncherLinux launcher = UrlLauncherLinux();
+        expect(
+            await launcher
+                .supportsMode(PreferredLaunchMode.externalApplication),
+            true);
+      });
+
+      test('returns false for other modes', () async {
+        final UrlLauncherLinux launcher = UrlLauncherLinux();
+        expect(
+            await launcher.supportsMode(
+                PreferredLaunchMode.externalNonBrowserApplication),
+            false);
+        expect(
+            await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView),
+            false);
+        expect(await launcher.supportsMode(PreferredLaunchMode.inAppWebView),
+            false);
+      });
+    });
+
+    test('supportsCloseForMode returns false', () async {
+      final UrlLauncherLinux launcher = UrlLauncherLinux();
+      expect(
+          await launcher
+              .supportsCloseForMode(PreferredLaunchMode.platformDefault),
+          false);
+      expect(
+          await launcher
+              .supportsCloseForMode(PreferredLaunchMode.externalApplication),
+          false);
+    });
   });
 }
 
diff --git a/packages/url_launcher/url_launcher_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md
index e2f5c68..ac8a05a 100644
--- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 3.1.0
+
+* Implements `supportsMode` and `supportsCloseForMode`.
+
 ## 3.0.7
 
 * Adds pub topics to package metadata.
diff --git a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml
index 0ad2390..3c98f47 100644
--- a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml
@@ -16,7 +16,7 @@
     # The example app is bundled with the plugin so we use a path dependency on
     # the parent directory to use the current plugin's version.
     path: ../
-  url_launcher_platform_interface: ^2.0.0
+  url_launcher_platform_interface: ^2.2.0
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart b/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart
index 55c07b7..1d22973 100644
--- a/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart
+++ b/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart
@@ -57,6 +57,18 @@
     return result.value;
   }
 
+  @override
+  Future<bool> supportsMode(PreferredLaunchMode mode) async {
+    return mode == PreferredLaunchMode.platformDefault ||
+        mode == PreferredLaunchMode.externalApplication;
+  }
+
+  @override
+  Future<bool> supportsCloseForMode(PreferredLaunchMode mode) async {
+    // No supported mode is closeable.
+    return false;
+  }
+
   Exception _getInvalidUrlException(String url) {
     // TODO(stuartmorgan): Make this an actual ArgumentError. This should be
     // coordinated across all platforms as a breaking change to have them all
diff --git a/packages/url_launcher/url_launcher_macos/pubspec.yaml b/packages/url_launcher/url_launcher_macos/pubspec.yaml
index 23d5f6a..ba7066c 100644
--- a/packages/url_launcher/url_launcher_macos/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_macos/pubspec.yaml
@@ -2,7 +2,7 @@
 description: macOS implementation of the url_launcher plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_macos
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
-version: 3.0.7
+version: 3.1.0
 
 environment:
   sdk: ">=2.19.0 <4.0.0"
@@ -20,7 +20,7 @@
 dependencies:
   flutter:
     sdk: flutter
-  url_launcher_platform_interface: ^2.0.3
+  url_launcher_platform_interface: ^2.2.0
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart b/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart
index a7147af..e9cc3c6 100644
--- a/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart
+++ b/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart
@@ -106,6 +106,47 @@
             throwsA(isA<PlatformException>()));
       });
     });
+
+    group('supportsMode', () {
+      test('returns true for platformDefault', () async {
+        final UrlLauncherMacOS launcher = UrlLauncherMacOS(api: api);
+        expect(await launcher.supportsMode(PreferredLaunchMode.platformDefault),
+            true);
+      });
+
+      test('returns true for external application', () async {
+        final UrlLauncherMacOS launcher = UrlLauncherMacOS(api: api);
+        expect(
+            await launcher
+                .supportsMode(PreferredLaunchMode.externalApplication),
+            true);
+      });
+
+      test('returns false for other modes', () async {
+        final UrlLauncherMacOS launcher = UrlLauncherMacOS(api: api);
+        expect(
+            await launcher.supportsMode(
+                PreferredLaunchMode.externalNonBrowserApplication),
+            false);
+        expect(
+            await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView),
+            false);
+        expect(await launcher.supportsMode(PreferredLaunchMode.inAppWebView),
+            false);
+      });
+    });
+
+    test('supportsCloseForMode returns false', () async {
+      final UrlLauncherMacOS launcher = UrlLauncherMacOS(api: api);
+      expect(
+          await launcher
+              .supportsCloseForMode(PreferredLaunchMode.platformDefault),
+          false);
+      expect(
+          await launcher
+              .supportsCloseForMode(PreferredLaunchMode.externalApplication),
+          false);
+    });
   });
 }
 
diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md
index 836afdc..76979c9 100644
--- a/packages/url_launcher/url_launcher_web/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.2.0
+
+* Implements `supportsMode` and `supportsCloseForMode`.
+
 ## 2.1.0
 
 * Adds `launchUrl` implementation.
diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart
index c909186..994e3b2 100644
--- a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart
+++ b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart
@@ -8,6 +8,7 @@
 import 'package:integration_test/integration_test.dart';
 import 'package:mockito/annotations.dart';
 import 'package:mockito/mockito.dart';
+import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
 import 'package:url_launcher_web/url_launcher_web.dart';
 
 import 'url_launcher_web_test.mocks.dart';
@@ -218,5 +219,30 @@
         });
       });
     });
+
+    group('supportsMode', () {
+      testWidgets('returns true for platformDefault', (WidgetTester _) async {
+        expect(plugin.supportsMode(PreferredLaunchMode.platformDefault),
+            completion(isTrue));
+      });
+
+      testWidgets('returns false for other modes', (WidgetTester _) async {
+        expect(plugin.supportsMode(PreferredLaunchMode.externalApplication),
+            completion(isFalse));
+        expect(
+            plugin.supportsMode(
+                PreferredLaunchMode.externalNonBrowserApplication),
+            completion(isFalse));
+        expect(plugin.supportsMode(PreferredLaunchMode.inAppBrowserView),
+            completion(isFalse));
+        expect(plugin.supportsMode(PreferredLaunchMode.inAppWebView),
+            completion(isFalse));
+      });
+    });
+
+    testWidgets('supportsCloseForMode returns false', (WidgetTester _) async {
+      expect(plugin.supportsCloseForMode(PreferredLaunchMode.platformDefault),
+          completion(isFalse));
+    });
   });
 }
diff --git a/packages/url_launcher/url_launcher_web/example/pubspec.yaml b/packages/url_launcher/url_launcher_web/example/pubspec.yaml
index 9915164..d096423 100644
--- a/packages/url_launcher/url_launcher_web/example/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_web/example/pubspec.yaml
@@ -16,6 +16,6 @@
   integration_test:
     sdk: flutter
   mockito: 5.4.1
-  url_launcher_platform_interface: ^2.0.3
+  url_launcher_platform_interface: ^2.2.0
   url_launcher_web:
     path: ../
diff --git a/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart
index bf96bf2..0dd1012 100644
--- a/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart
+++ b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart
@@ -111,4 +111,17 @@
     final String? windowName = options.webOnlyWindowName;
     return openNewWindow(url, webOnlyWindowName: windowName) != null;
   }
+
+  @override
+  Future<bool> supportsMode(PreferredLaunchMode mode) async {
+    // Web doesn't allow any control over the destination beyond
+    // webOnlyWindowName, so don't claim support for any mode beyond default.
+    return mode == PreferredLaunchMode.platformDefault;
+  }
+
+  @override
+  Future<bool> supportsCloseForMode(PreferredLaunchMode mode) async {
+    // No supported mode is closeable.
+    return false;
+  }
 }
diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml
index 95dd915..b70a094 100644
--- a/packages/url_launcher/url_launcher_web/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_web/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Web platform implementation of url_launcher
 repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_web
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
-version: 2.1.0
+version: 2.2.0
 
 environment:
   sdk: ">=3.1.0 <4.0.0"
@@ -21,7 +21,7 @@
     sdk: flutter
   flutter_web_plugins:
     sdk: flutter
-  url_launcher_platform_interface: ^2.1.0
+  url_launcher_platform_interface: ^2.2.0
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md
index 933cc10..b63c9e1 100644
--- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 3.1.0
+
+* Implements `supportsMode` and `supportsCloseForMode`.
+
 ## 3.0.8
 
 * Adds pub topics to package metadata.
diff --git a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml
index 77106bc..08bf314 100644
--- a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml
@@ -9,7 +9,7 @@
 dependencies:
   flutter:
     sdk: flutter
-  url_launcher_platform_interface: ^2.0.0
+  url_launcher_platform_interface: ^2.2.0
   url_launcher_windows:
     # When depending on this package from a real application you should use:
     #   url_launcher_windows: ^x.y.z
diff --git a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart
index 41c403e..790a451 100644
--- a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart
+++ b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart
@@ -45,4 +45,16 @@
     // Failure is handled via a PlatformException from `launchUrl`.
     return true;
   }
+
+  @override
+  Future<bool> supportsMode(PreferredLaunchMode mode) async {
+    return mode == PreferredLaunchMode.platformDefault ||
+        mode == PreferredLaunchMode.externalApplication;
+  }
+
+  @override
+  Future<bool> supportsCloseForMode(PreferredLaunchMode mode) async {
+    // No supported mode is closeable.
+    return false;
+  }
 }
diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml
index 6b17b9d..118d928 100644
--- a/packages/url_launcher/url_launcher_windows/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Windows implementation of the url_launcher plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_windows
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
-version: 3.0.8
+version: 3.1.0
 
 environment:
   sdk: ">=2.19.0 <4.0.0"
@@ -19,7 +19,7 @@
 dependencies:
   flutter:
     sdk: flutter
-  url_launcher_platform_interface: ^2.0.3
+  url_launcher_platform_interface: ^2.2.0
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart b/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart
index 7f48f64..0be939b 100644
--- a/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart
+++ b/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart
@@ -77,6 +77,45 @@
       expect(api.argument, 'http://example.com/');
     });
   });
+
+  group('supportsMode', () {
+    test('returns true for platformDefault', () async {
+      final UrlLauncherWindows launcher = UrlLauncherWindows(api: api);
+      expect(await launcher.supportsMode(PreferredLaunchMode.platformDefault),
+          true);
+    });
+
+    test('returns true for external application', () async {
+      final UrlLauncherWindows launcher = UrlLauncherWindows(api: api);
+      expect(
+          await launcher.supportsMode(PreferredLaunchMode.externalApplication),
+          true);
+    });
+
+    test('returns false for other modes', () async {
+      final UrlLauncherWindows launcher = UrlLauncherWindows(api: api);
+      expect(
+          await launcher
+              .supportsMode(PreferredLaunchMode.externalNonBrowserApplication),
+          false);
+      expect(await launcher.supportsMode(PreferredLaunchMode.inAppBrowserView),
+          false);
+      expect(
+          await launcher.supportsMode(PreferredLaunchMode.inAppWebView), false);
+    });
+  });
+
+  test('supportsCloseForMode returns false', () async {
+    final UrlLauncherWindows launcher = UrlLauncherWindows(api: api);
+    expect(
+        await launcher
+            .supportsCloseForMode(PreferredLaunchMode.platformDefault),
+        false);
+    expect(
+        await launcher
+            .supportsCloseForMode(PreferredLaunchMode.externalApplication),
+        false);
+  });
 }
 
 class _FakeUrlLauncherApi implements UrlLauncherApi {