From 6e96168e9cf2bbab40c3700b358382befa92e90b Mon Sep 17 00:00:00 2001 From: Shinya Kumagai Date: Sat, 2 Dec 2023 00:08:03 +0900 Subject: [PATCH 1/4] Add support for launching a URL in a external browser to `flutter_custom_tabs_android` platform package --- .../customtabs/CustomTabsLauncher.java | 10 ++++-- .../flutter/plugins/customtabs/Messages.java | 2 +- .../example/lib/main.dart | 14 ++++++++ .../lib/flutter_custom_tabs_android.dart | 4 +-- .../lib/src/custom_tabs_plugin_android.dart | 13 ++++--- .../lib/src/messages.g.dart | 2 +- .../pigeons/messages.dart | 2 +- .../test/custom_tabs_plugin_android_test.dart | 34 ++++++++++++++----- 8 files changed, 62 insertions(+), 19 deletions(-) diff --git a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncher.java b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncher.java index f9e47814..4b2d923e 100644 --- a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncher.java +++ b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncher.java @@ -39,7 +39,7 @@ void setActivity(@Nullable Activity activity) { public void launchUrl( @NonNull String urlString, @NonNull Boolean prefersDeepLink, - @NonNull CustomTabsOptionsMessage options + @Nullable CustomTabsOptionsMessage options ) { final Activity activity = this.activity; if (activity == null) { @@ -51,8 +51,14 @@ public void launchUrl( return; } - final CustomTabsFactory factory = new CustomTabsFactory(activity); try { + if (options == null) { + final Intent intent = new Intent(Intent.ACTION_VIEW, uri); + activity.startActivity(intent); + return; + } + + final CustomTabsFactory factory = new CustomTabsFactory(activity); final CustomTabsIntent customTabsIntent = factory.createIntent(options); if (customTabsIntent.intent.hasExtra(EXTRA_INITIAL_ACTIVITY_HEIGHT_PX)) { customTabsIntent.intent.setData(uri); diff --git a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/Messages.java b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/Messages.java index d7f1c11e..e46beec7 100644 --- a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/Messages.java +++ b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/Messages.java @@ -867,7 +867,7 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface CustomTabsApi { - void launchUrl(@NonNull String urlString, @NonNull Boolean prefersDeepLink, @NonNull CustomTabsOptionsMessage options); + void launchUrl(@NonNull String urlString, @NonNull Boolean prefersDeepLink, @Nullable CustomTabsOptionsMessage options); void closeAllIfPossible(); diff --git a/flutter_custom_tabs_android/example/lib/main.dart b/flutter_custom_tabs_android/example/lib/main.dart index 36361a62..33c60ec4 100644 --- a/flutter_custom_tabs_android/example/lib/main.dart +++ b/flutter_custom_tabs_android/example/lib/main.dart @@ -55,6 +55,10 @@ class MyApp extends StatelessWidget { onPressed: () => _launchAndCloseManually(context), child: const Text('Show flutter.dev + close after 5 seconds'), ), + FilledButton.tonal( + onPressed: () => _launchInExternalBrowser(), + child: const Text('Show flutter.dev in external browser'), + ), ], ), ), @@ -175,3 +179,13 @@ Future _launchAndCloseManually(BuildContext context) async { debugPrint(e.toString()); } } + +Future _launchInExternalBrowser() async { + try { + await CustomTabsPlatform.instance.launch( + 'https://flutter.dev', + ); + } catch (e) { + debugPrint(e.toString()); + } +} \ No newline at end of file diff --git a/flutter_custom_tabs_android/lib/flutter_custom_tabs_android.dart b/flutter_custom_tabs_android/lib/flutter_custom_tabs_android.dart index e774cb4e..0ebb55a7 100644 --- a/flutter_custom_tabs_android/lib/flutter_custom_tabs_android.dart +++ b/flutter_custom_tabs_android/lib/flutter_custom_tabs_android.dart @@ -1,2 +1,2 @@ -export './src/types/types.dart'; -export './src/custom_tabs_plugin_android.dart'; +export 'src/types/types.dart'; +export 'src/custom_tabs_plugin_android.dart'; diff --git a/flutter_custom_tabs_android/lib/src/custom_tabs_plugin_android.dart b/flutter_custom_tabs_android/lib/src/custom_tabs_plugin_android.dart index d7872c3d..6247f553 100644 --- a/flutter_custom_tabs_android/lib/src/custom_tabs_plugin_android.dart +++ b/flutter_custom_tabs_android/lib/src/custom_tabs_plugin_android.dart @@ -27,13 +27,18 @@ class CustomTabsPluginAndroid extends CustomTabsPlatform { PlatformOptions? customTabsOptions, PlatformOptions? safariVCOptions, }) { - final options = (customTabsOptions is CustomTabsOptions) - ? customTabsOptions.toMessage() - : CustomTabsOptionsMessage(); + final CustomTabsOptionsMessage? message; + if (customTabsOptions == null) { + message = null; + } else { + message = (customTabsOptions is CustomTabsOptions) + ? customTabsOptions.toMessage() + : CustomTabsOptionsMessage(); + } return _hostApi.launchUrl( urlString, prefersDeepLink: prefersDeepLink, - options: options, + options: message, ); } diff --git a/flutter_custom_tabs_android/lib/src/messages.g.dart b/flutter_custom_tabs_android/lib/src/messages.g.dart index ba351249..2146abf0 100644 --- a/flutter_custom_tabs_android/lib/src/messages.g.dart +++ b/flutter_custom_tabs_android/lib/src/messages.g.dart @@ -346,7 +346,7 @@ class CustomTabsApi { static const MessageCodec pigeonChannelCodec = _CustomTabsApiCodec(); - Future launchUrl(String urlString, {required bool prefersDeepLink, required CustomTabsOptionsMessage options,}) async { + Future launchUrl(String urlString, {required bool prefersDeepLink, CustomTabsOptionsMessage? options,}) async { const String __pigeon_channelName = 'dev.flutter.pigeon.flutter_custom_tabs_android.CustomTabsApi.launchUrl'; final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, diff --git a/flutter_custom_tabs_android/pigeons/messages.dart b/flutter_custom_tabs_android/pigeons/messages.dart index bb6d2860..157bf4df 100644 --- a/flutter_custom_tabs_android/pigeons/messages.dart +++ b/flutter_custom_tabs_android/pigeons/messages.dart @@ -14,7 +14,7 @@ abstract class CustomTabsApi { void launchUrl( String urlString, { required bool prefersDeepLink, - required CustomTabsOptionsMessage options, + CustomTabsOptionsMessage? options, }); void closeAllIfPossible(); diff --git a/flutter_custom_tabs_android/test/custom_tabs_plugin_android_test.dart b/flutter_custom_tabs_android/test/custom_tabs_plugin_android_test.dart index 9d3ece18..92af9246 100644 --- a/flutter_custom_tabs_android/test/custom_tabs_plugin_android_test.dart +++ b/flutter_custom_tabs_android/test/custom_tabs_plugin_android_test.dart @@ -41,21 +41,37 @@ void main() { test('launch() invoke method "launch" with invalid options', () async { const url = 'http://example.com/'; const prefersDeepLink = true; + const options = _Options( + urlBarHidingEnabled: true, + ); api.setLaunchExpectations( url: url, prefersDeepLink: prefersDeepLink, + options: options, ); await customTabs.launch( url, prefersDeepLink: prefersDeepLink, - customTabsOptions: const _Options( - urlBarHidingEnabled: true, - ), + customTabsOptions: options, safariVCOptions: const _Options(), ); }); + test('launch() invoke method "launch" with no options', () async { + const url = 'http://example.com/'; + const prefersDeepLink = true; + api.setLaunchExpectations( + url: url, + prefersDeepLink: prefersDeepLink, + ); + + await customTabs.launch( + url, + prefersDeepLink: prefersDeepLink, + ); + }); + test('closeAllIfPossible() invoke method "closeAllIfPossible"', () async { await customTabs.closeAllIfPossible(); expect(api.closeAllIfPossibleCalled, isTrue); @@ -83,16 +99,18 @@ class _MockCustomTabsApi implements CustomTabsApi { Future launchUrl( String url, { required bool prefersDeepLink, - required CustomTabsOptionsMessage options, + CustomTabsOptionsMessage? options, }) async { expect(url, this.url); expect(prefersDeepLink, this.prefersDeepLink); - if (this.options is CustomTabsOptions) { - final message = (this.options as CustomTabsOptions).toMessage(); - expect(options.urlBarHidingEnabled, message.urlBarHidingEnabled); + if (this.options == null) { + expect(options, isNull); + } else if (this.options is CustomTabsOptions) { + final expected = (this.options as CustomTabsOptions).toMessage(); + expect(options?.urlBarHidingEnabled, expected.urlBarHidingEnabled); } else { - expect(options.urlBarHidingEnabled, isNull); + expect(options, isNotNull); } launchUrlCalled = true; } From 6b89d2e696c0241c019efab08058e22ecfc82a22 Mon Sep 17 00:00:00 2001 From: Shinya Kumagai Date: Sat, 2 Dec 2023 00:52:09 +0900 Subject: [PATCH 2/4] Allow passing HTTP request headers when launching external browser on Android --- .../plugins/customtabs/CustomTabsFactory.java | 39 ++- .../customtabs/CustomTabsLauncher.java | 14 +- .../plugins/customtabs/CustomTabsPlugin.java | 6 +- .../flutter/plugins/customtabs/Messages.java | 41 ++- .../lib/src/custom_tabs_plugin_android.dart | 2 +- .../lib/src/message_converters.dart | 1 + .../lib/src/messages.g.dart | 15 +- .../lib/src/types/custom_tabs_browser.dart | 12 +- .../lib/src/types/custom_tabs_options.dart | 8 + .../pigeons/messages.dart | 4 +- .../test/custom_tabs_plugin_android_test.dart | 2 +- .../test/types/custom_tabs_browser_test.dart | 13 + .../test/types/custom_tabs_options_test.dart | 23 ++ flutter_custom_tabs_ios/.swiftformat | 2 +- flutter_custom_tabs_ios/.swiftlint.yml | 2 +- flutter_custom_tabs_ios/example/lib/main.dart | 14 + .../ios/Classes/CustomTabsPlugin.swift | 19 +- .../ios/Classes/messages.g.swift | 308 +++++++++--------- .../lib/src/custom_tabs_plugin_ios.dart | 16 +- .../lib/src/messages.g.dart | 4 +- flutter_custom_tabs_ios/pigeons/messages.dart | 4 +- .../test/custom_tabs_plugin_ios_ios_test.dart | 36 +- 22 files changed, 374 insertions(+), 211 deletions(-) diff --git a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsFactory.java b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsFactory.java index b7f92eed..1166b034 100644 --- a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsFactory.java +++ b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsFactory.java @@ -9,12 +9,14 @@ import static com.github.droibit.flutter.plugins.customtabs.ResourceFactory.resolveDrawableIdentifier; import android.content.Context; +import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; import android.os.Bundle; import android.provider.Browser; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.browser.customtabs.CustomTabColorSchemeParams; import androidx.browser.customtabs.CustomTabsIntent; @@ -40,8 +42,28 @@ class CustomTabsFactory { this.context = context; } + @Nullable + Intent createExternalBrowserIntent(@Nullable CustomTabsOptionsMessage options) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + if (options == null) { + return intent; + } + + final CustomTabsBrowserConfigurationMessage browserOptions = options.getBrowser(); + if (browserOptions == null || !browserOptions.getPrefersExternalBrowser()) { + return null; + } + + final Map headers = browserOptions.getHeaders(); + if (headers != null) { + final Bundle bundleHeaders = extractBundle(headers); + intent.putExtra(Browser.EXTRA_HEADERS, bundleHeaders); + } + return intent; + } + @NonNull - CustomTabsIntent createIntent(@NonNull CustomTabsOptionsMessage options) { + CustomTabsIntent createCustomTabsIntent(@NonNull CustomTabsOptionsMessage options) { final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); final CustomTabsColorSchemesMessage colorSchemes = options.getColorSchemes(); if (colorSchemes != null) { @@ -92,7 +114,6 @@ CustomTabsIntent createIntent(@NonNull CustomTabsOptionsMessage options) { } applyBrowserConfiguration(customTabsIntent, browserConfiguration); return customTabsIntent; - } void applyColorSchemes( @@ -101,6 +122,7 @@ void applyColorSchemes( ) { final Long colorScheme = colorSchemes.getColorScheme(); if (colorScheme != null) { + builder.setColorScheme(colorScheme.intValue()); } @@ -210,10 +232,7 @@ private void applyBrowserConfiguration( ) { final Map headers = options.getHeaders(); if (headers != null) { - final Bundle bundleHeaders = new Bundle(); - for (Map.Entry header : headers.entrySet()) { - bundleHeaders.putString(header.getKey(), header.getValue()); - } + final Bundle bundleHeaders = extractBundle(headers); customTabsIntent.intent.putExtra(Browser.EXTRA_HEADERS, bundleHeaders); } @@ -238,4 +257,12 @@ private void applyBrowserConfiguration( ensureChromeCustomTabsPackage(customTabsIntent, context, fallback); } } + + private @NonNull Bundle extractBundle(@NonNull Map headers) { + final Bundle dest = new Bundle(headers.size()); + for (Map.Entry entry : headers.entrySet()) { + dest.putString(entry.getKey(), entry.getValue()); + } + return dest; + } } diff --git a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncher.java b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncher.java index 4b2d923e..de6b7d66 100644 --- a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncher.java +++ b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncher.java @@ -4,6 +4,7 @@ import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_INITIAL_ACTIVITY_HEIGHT_PX; import static androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION; +import static java.util.Objects.requireNonNull; import android.app.Activity; import android.app.ActivityManager; @@ -36,7 +37,7 @@ void setActivity(@Nullable Activity activity) { } @Override - public void launchUrl( + public void launch( @NonNull String urlString, @NonNull Boolean prefersDeepLink, @Nullable CustomTabsOptionsMessage options @@ -52,14 +53,15 @@ public void launchUrl( } try { - if (options == null) { - final Intent intent = new Intent(Intent.ACTION_VIEW, uri); - activity.startActivity(intent); + final CustomTabsFactory factory = new CustomTabsFactory(activity); + final Intent externalBrowserIntent = factory.createExternalBrowserIntent(options); + if (externalBrowserIntent != null) { + externalBrowserIntent.setData(uri); + activity.startActivity(externalBrowserIntent); return; } - final CustomTabsFactory factory = new CustomTabsFactory(activity); - final CustomTabsIntent customTabsIntent = factory.createIntent(options); + final CustomTabsIntent customTabsIntent = factory.createCustomTabsIntent(requireNonNull(options)); if (customTabsIntent.intent.hasExtra(EXTRA_INITIAL_ACTIVITY_HEIGHT_PX)) { customTabsIntent.intent.setData(uri); activity.startActivityForResult(customTabsIntent.intent, REQUEST_CODE_CUSTOM_TABS); diff --git a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsPlugin.java b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsPlugin.java index 38d1249d..5d3340eb 100644 --- a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsPlugin.java +++ b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsPlugin.java @@ -3,6 +3,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.github.droibit.flutter.plugins.customtabs.Messages.CustomTabsApi; + import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; @@ -13,7 +15,7 @@ public class CustomTabsPlugin implements FlutterPlugin, ActivityAware { @Override public void onAttachedToEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) { api = new CustomTabsLauncher(); - Messages.CustomTabsApi.setUp(binding.getBinaryMessenger(), api); + CustomTabsApi.setUp(binding.getBinaryMessenger(), api); } @Override @@ -22,7 +24,7 @@ public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding bin return; } - Messages.CustomTabsApi.setUp(binding.getBinaryMessenger(), null); + CustomTabsApi.setUp(binding.getBinaryMessenger(), null); api = null; } diff --git a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/Messages.java b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/Messages.java index e46beec7..669bab9d 100644 --- a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/Messages.java +++ b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/Messages.java @@ -376,6 +376,19 @@ ArrayList toList() { /** Generated class from Pigeon that represents data sent in messages. */ public static final class CustomTabsBrowserConfigurationMessage { + private @NonNull Boolean prefersExternalBrowser; + + public @NonNull Boolean getPrefersExternalBrowser() { + return prefersExternalBrowser; + } + + public void setPrefersExternalBrowser(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"prefersExternalBrowser\" is null."); + } + this.prefersExternalBrowser = setterArg; + } + private @Nullable Boolean prefersDefaultBrowser; public @Nullable Boolean getPrefersDefaultBrowser() { @@ -406,8 +419,18 @@ public void setHeaders(@Nullable Map setterArg) { this.headers = setterArg; } + /** Constructor is non-public to enforce null safety; use Builder. */ + CustomTabsBrowserConfigurationMessage() {} + public static final class Builder { + private @Nullable Boolean prefersExternalBrowser; + + public @NonNull Builder setPrefersExternalBrowser(@NonNull Boolean setterArg) { + this.prefersExternalBrowser = setterArg; + return this; + } + private @Nullable Boolean prefersDefaultBrowser; public @NonNull Builder setPrefersDefaultBrowser(@Nullable Boolean setterArg) { @@ -431,6 +454,7 @@ public static final class Builder { public @NonNull CustomTabsBrowserConfigurationMessage build() { CustomTabsBrowserConfigurationMessage pigeonReturn = new CustomTabsBrowserConfigurationMessage(); + pigeonReturn.setPrefersExternalBrowser(prefersExternalBrowser); pigeonReturn.setPrefersDefaultBrowser(prefersDefaultBrowser); pigeonReturn.setFallbackCustomTabs(fallbackCustomTabs); pigeonReturn.setHeaders(headers); @@ -440,7 +464,8 @@ public static final class Builder { @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList(3); + ArrayList toListResult = new ArrayList(4); + toListResult.add(prefersExternalBrowser); toListResult.add(prefersDefaultBrowser); toListResult.add(fallbackCustomTabs); toListResult.add(headers); @@ -449,11 +474,13 @@ ArrayList toList() { static @NonNull CustomTabsBrowserConfigurationMessage fromList(@NonNull ArrayList list) { CustomTabsBrowserConfigurationMessage pigeonResult = new CustomTabsBrowserConfigurationMessage(); - Object prefersDefaultBrowser = list.get(0); + Object prefersExternalBrowser = list.get(0); + pigeonResult.setPrefersExternalBrowser((Boolean) prefersExternalBrowser); + Object prefersDefaultBrowser = list.get(1); pigeonResult.setPrefersDefaultBrowser((Boolean) prefersDefaultBrowser); - Object fallbackCustomTabs = list.get(1); + Object fallbackCustomTabs = list.get(2); pigeonResult.setFallbackCustomTabs((List) fallbackCustomTabs); - Object headers = list.get(2); + Object headers = list.get(3); pigeonResult.setHeaders((Map) headers); return pigeonResult; } @@ -867,7 +894,7 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface CustomTabsApi { - void launchUrl(@NonNull String urlString, @NonNull Boolean prefersDeepLink, @Nullable CustomTabsOptionsMessage options); + void launch(@NonNull String urlString, @NonNull Boolean prefersDeepLink, @Nullable CustomTabsOptionsMessage options); void closeAllIfPossible(); @@ -880,7 +907,7 @@ static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable CustomTabs { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.flutter_custom_tabs_android.CustomTabsApi.launchUrl", getCodec()); + binaryMessenger, "dev.flutter.pigeon.flutter_custom_tabs_android.CustomTabsApi.launch", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -890,7 +917,7 @@ static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable CustomTabs Boolean prefersDeepLinkArg = (Boolean) args.get(1); CustomTabsOptionsMessage optionsArg = (CustomTabsOptionsMessage) args.get(2); try { - api.launchUrl(urlStringArg, prefersDeepLinkArg, optionsArg); + api.launch(urlStringArg, prefersDeepLinkArg, optionsArg); wrapped.add(0, null); } catch (Throwable exception) { diff --git a/flutter_custom_tabs_android/lib/src/custom_tabs_plugin_android.dart b/flutter_custom_tabs_android/lib/src/custom_tabs_plugin_android.dart index 6247f553..c9eb48e4 100644 --- a/flutter_custom_tabs_android/lib/src/custom_tabs_plugin_android.dart +++ b/flutter_custom_tabs_android/lib/src/custom_tabs_plugin_android.dart @@ -35,7 +35,7 @@ class CustomTabsPluginAndroid extends CustomTabsPlatform { ? customTabsOptions.toMessage() : CustomTabsOptionsMessage(); } - return _hostApi.launchUrl( + return _hostApi.launch( urlString, prefersDeepLink: prefersDeepLink, options: message, diff --git a/flutter_custom_tabs_android/lib/src/message_converters.dart b/flutter_custom_tabs_android/lib/src/message_converters.dart index 3f9491e8..fa3b53a6 100644 --- a/flutter_custom_tabs_android/lib/src/message_converters.dart +++ b/flutter_custom_tabs_android/lib/src/message_converters.dart @@ -40,6 +40,7 @@ extension CustomTabsBrowserConfigurationConverter on CustomTabsBrowserConfiguration { CustomTabsBrowserConfigurationMessage toMessage() { return CustomTabsBrowserConfigurationMessage( + prefersExternalBrowser: prefersExternalBrowser, prefersDefaultBrowser: prefersDefaultBrowser, fallbackCustomTabs: fallbackCustomTabs, headers: headers, diff --git a/flutter_custom_tabs_android/lib/src/messages.g.dart b/flutter_custom_tabs_android/lib/src/messages.g.dart index 2146abf0..9642f938 100644 --- a/flutter_custom_tabs_android/lib/src/messages.g.dart +++ b/flutter_custom_tabs_android/lib/src/messages.g.dart @@ -124,11 +124,14 @@ class CustomTabsAnimationsMessage { class CustomTabsBrowserConfigurationMessage { CustomTabsBrowserConfigurationMessage({ + required this.prefersExternalBrowser, this.prefersDefaultBrowser, this.fallbackCustomTabs, this.headers, }); + bool prefersExternalBrowser; + bool? prefersDefaultBrowser; List? fallbackCustomTabs; @@ -137,6 +140,7 @@ class CustomTabsBrowserConfigurationMessage { Object encode() { return [ + prefersExternalBrowser, prefersDefaultBrowser, fallbackCustomTabs, headers, @@ -146,9 +150,10 @@ class CustomTabsBrowserConfigurationMessage { static CustomTabsBrowserConfigurationMessage decode(Object result) { result as List; return CustomTabsBrowserConfigurationMessage( - prefersDefaultBrowser: result[0] as bool?, - fallbackCustomTabs: (result[1] as List?)?.cast(), - headers: (result[2] as Map?)?.cast(), + prefersExternalBrowser: result[0]! as bool, + prefersDefaultBrowser: result[1] as bool?, + fallbackCustomTabs: (result[2] as List?)?.cast(), + headers: (result[3] as Map?)?.cast(), ); } } @@ -346,8 +351,8 @@ class CustomTabsApi { static const MessageCodec pigeonChannelCodec = _CustomTabsApiCodec(); - Future launchUrl(String urlString, {required bool prefersDeepLink, CustomTabsOptionsMessage? options,}) async { - const String __pigeon_channelName = 'dev.flutter.pigeon.flutter_custom_tabs_android.CustomTabsApi.launchUrl'; + Future launch(String urlString, {required bool prefersDeepLink, CustomTabsOptionsMessage? options,}) async { + const String __pigeon_channelName = 'dev.flutter.pigeon.flutter_custom_tabs_android.CustomTabsApi.launch'; final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, diff --git a/flutter_custom_tabs_android/lib/src/types/custom_tabs_browser.dart b/flutter_custom_tabs_android/lib/src/types/custom_tabs_browser.dart index fb17a8b3..6042ef42 100644 --- a/flutter_custom_tabs_android/lib/src/types/custom_tabs_browser.dart +++ b/flutter_custom_tabs_android/lib/src/types/custom_tabs_browser.dart @@ -13,7 +13,14 @@ class CustomTabsBrowserConfiguration { this.prefersDefaultBrowser, this.fallbackCustomTabs, this.headers, - }); + }) : prefersExternalBrowser = false; + + @internal + const CustomTabsBrowserConfiguration.externalBrowser({ + required this.headers, + }) : prefersDefaultBrowser = null, + fallbackCustomTabs = null, + prefersExternalBrowser = true; /// A Boolean value that determines whether to prioritize the default browser that supports Custom Tabs over Chrome. final bool? prefersDefaultBrowser; @@ -23,4 +30,7 @@ class CustomTabsBrowserConfiguration { /// Extra HTTP request headers. final Map? headers; + + @internal + final bool prefersExternalBrowser; } diff --git a/flutter_custom_tabs_android/lib/src/types/custom_tabs_options.dart b/flutter_custom_tabs_android/lib/src/types/custom_tabs_options.dart index d799dffd..563da863 100644 --- a/flutter_custom_tabs_android/lib/src/types/custom_tabs_options.dart +++ b/flutter_custom_tabs_android/lib/src/types/custom_tabs_options.dart @@ -45,6 +45,14 @@ class CustomTabsOptions implements PlatformOptions { partial: configuration, ); + /// Creates a [CustomTabsOptions] instance with HTTP headers for an external browser. + CustomTabsOptions.externalBrowser({ + required Map headers, + }) : this( + browser: + CustomTabsBrowserConfiguration.externalBrowser(headers: headers), + ); + /// The visualization configuration. final CustomTabsColorSchemes? colorSchemes; diff --git a/flutter_custom_tabs_android/pigeons/messages.dart b/flutter_custom_tabs_android/pigeons/messages.dart index 157bf4df..1f3d2f77 100644 --- a/flutter_custom_tabs_android/pigeons/messages.dart +++ b/flutter_custom_tabs_android/pigeons/messages.dart @@ -11,7 +11,7 @@ import 'package:pigeon/pigeon.dart'; )) @HostApi() abstract class CustomTabsApi { - void launchUrl( + void launch( String urlString, { required bool prefersDeepLink, CustomTabsOptionsMessage? options, @@ -60,11 +60,13 @@ class CustomTabsAnimationsMessage { class CustomTabsBrowserConfigurationMessage { const CustomTabsBrowserConfigurationMessage({ + required this.prefersExternalBrowser, this.prefersDefaultBrowser, this.fallbackCustomTabs, this.headers, }); + final bool prefersExternalBrowser; final bool? prefersDefaultBrowser; final List? fallbackCustomTabs; final Map? headers; diff --git a/flutter_custom_tabs_android/test/custom_tabs_plugin_android_test.dart b/flutter_custom_tabs_android/test/custom_tabs_plugin_android_test.dart index 92af9246..572b1658 100644 --- a/flutter_custom_tabs_android/test/custom_tabs_plugin_android_test.dart +++ b/flutter_custom_tabs_android/test/custom_tabs_plugin_android_test.dart @@ -96,7 +96,7 @@ class _MockCustomTabsApi implements CustomTabsApi { } @override - Future launchUrl( + Future launch( String url, { required bool prefersDeepLink, CustomTabsOptionsMessage? options, diff --git a/flutter_custom_tabs_android/test/types/custom_tabs_browser_test.dart b/flutter_custom_tabs_android/test/types/custom_tabs_browser_test.dart index ef589269..54d27668 100644 --- a/flutter_custom_tabs_android/test/types/custom_tabs_browser_test.dart +++ b/flutter_custom_tabs_android/test/types/custom_tabs_browser_test.dart @@ -10,6 +10,7 @@ void main() { expect(actual.prefersDefaultBrowser, isNull); expect(actual.fallbackCustomTabs, isNull); expect(actual.headers, isNull); + expect(actual.prefersExternalBrowser, isFalse); }); test('toMessage() returns a message with complete options', () { @@ -25,5 +26,17 @@ void main() { expect(actual.prefersDefaultBrowser, configuration.prefersDefaultBrowser); expect(actual.fallbackCustomTabs, configuration.fallbackCustomTabs); expect(actual.headers, configuration.headers); + expect(actual.prefersExternalBrowser, isFalse); + }); + + test('toMessage() returns a message with external browser options', () { + const configuration = CustomTabsBrowserConfiguration.externalBrowser( + headers: {'key': 'value'}, + ); + final actual = configuration.toMessage(); + expect(actual.prefersDefaultBrowser, isNull); + expect(actual.fallbackCustomTabs, isNull); + expect(actual.headers, configuration.headers); + expect(actual.prefersExternalBrowser, isTrue); }); } diff --git a/flutter_custom_tabs_android/test/types/custom_tabs_options_test.dart b/flutter_custom_tabs_android/test/types/custom_tabs_options_test.dart index ded21c76..dc62d5a2 100644 --- a/flutter_custom_tabs_android/test/types/custom_tabs_options_test.dart +++ b/flutter_custom_tabs_android/test/types/custom_tabs_options_test.dart @@ -125,6 +125,7 @@ void main() { expectedBrowser.fallbackCustomTabs, ); expect(actualBrowser.headers, options.browser!.headers); + expect(actualBrowser.prefersExternalBrowser, isFalse); final expectedPartial = options.partial!; final actualPartial = actual.partial!; @@ -135,6 +136,28 @@ void main() { ); expect(actualPartial.cornerRadius, expectedPartial.cornerRadius); }); + + test('toMessage() returns a message with external browser options', () { + final options = CustomTabsOptions.externalBrowser(headers: const { + 'key': 'value', + }); + final actual = options.toMessage(); + expect(actual.colorSchemes, isNull); + expect(actual.urlBarHidingEnabled, isNull); + expect(actual.shareState, isNull); + expect(actual.showTitle, isNull); + expect(actual.instantAppsEnabled, isNull); + expect(actual.animations, isNull); + expect(actual.closeButton, isNull); + expect(actual.browser, isNotNull); + expect(actual.partial, isNull); + + final actualBrowser = actual.browser!; + expect(actualBrowser.prefersExternalBrowser, isTrue); + expect(actualBrowser.prefersDefaultBrowser, isNull); + expect(actualBrowser.fallbackCustomTabs, isNull); + expect(actualBrowser.headers, options.browser!.headers); + }); }); group('CustomTabsShareState', () { diff --git a/flutter_custom_tabs_ios/.swiftformat b/flutter_custom_tabs_ios/.swiftformat index 1cc7ac55..5626f57b 100644 --- a/flutter_custom_tabs_ios/.swiftformat +++ b/flutter_custom_tabs_ios/.swiftformat @@ -1,5 +1,5 @@ # file options ---exclude Carthage,Pods,.swiftpm,ios/Classes/Messages.swift +--exclude Carthage,Pods,.swiftpm,./ios/Classes/messages.g.swift # format options --allman false diff --git a/flutter_custom_tabs_ios/.swiftlint.yml b/flutter_custom_tabs_ios/.swiftlint.yml index 70415709..209bc220 100644 --- a/flutter_custom_tabs_ios/.swiftlint.yml +++ b/flutter_custom_tabs_ios/.swiftlint.yml @@ -136,7 +136,7 @@ analyzer_rules: included: - ios/Classes excluded: - - ios/Classes/Messages.swift + - ios/Classes/messages.g.swift line_length: - 200 # warning diff --git a/flutter_custom_tabs_ios/example/lib/main.dart b/flutter_custom_tabs_ios/example/lib/main.dart index 087d439c..bfb1debb 100644 --- a/flutter_custom_tabs_ios/example/lib/main.dart +++ b/flutter_custom_tabs_ios/example/lib/main.dart @@ -48,6 +48,10 @@ class MyApp extends StatelessWidget { onPressed: () => _launchAndCloseManually(context), child: const Text('Show flutter.dev + close after 5 seconds'), ), + FilledButton( + onPressed: () => _launchInExternalBrowser(), + child: const Text('Show flutter.dev in external browser'), + ), ], ), ), @@ -140,3 +144,13 @@ Future _launchAndCloseManually(BuildContext context) async { debugPrint(e.toString()); } } + +Future _launchInExternalBrowser() async { + try { + await CustomTabsPlatform.instance.launch( + 'https://flutter.dev', + ); + } catch (e) { + debugPrint(e.toString()); + } +} \ No newline at end of file diff --git a/flutter_custom_tabs_ios/ios/Classes/CustomTabsPlugin.swift b/flutter_custom_tabs_ios/ios/Classes/CustomTabsPlugin.swift index 81ef6ed3..ebd195eb 100644 --- a/flutter_custom_tabs_ios/ios/Classes/CustomTabsPlugin.swift +++ b/flutter_custom_tabs_ios/ios/Classes/CustomTabsPlugin.swift @@ -13,18 +13,18 @@ public class CustomTabsPlugin: NSObject, FlutterPlugin, CustomTabsApi { func launchURL( _ urlString: String, prefersDeepLink: Bool, - options: SafariViewControllerOptionsMessage, + options: SafariViewControllerOptionsMessage?, completion: @escaping (Result) -> Void ) { let url = URL(string: urlString)! if prefersDeepLink { UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { [weak self] opened in if !opened { - self?.presentSafariViewController(with: url, options: options, completion: completion) + self?.launchURL(url, options: options, completion: completion) } } } else { - presentSafariViewController(with: url, options: options, completion: completion) + launchURL(url, options: options, completion: completion) } } @@ -36,11 +36,18 @@ public class CustomTabsPlugin: NSObject, FlutterPlugin, CustomTabsApi { // MARK: - Private - private func presentSafariViewController( - with url: URL, - options: SafariViewControllerOptionsMessage, + private func launchURL( + _ url: URL, + options: SafariViewControllerOptionsMessage?, completion: @escaping (Result) -> Void ) { + guard let options else { + UIApplication.shared.open(url) { _ in + completion(.success(())) + } + return + } + if let topViewController = UIWindow.keyWindow?.topViewController() { let safariViewController = SFSafariViewController.make(url: url, options: options) dismissStack.append { [weak safariViewController] in diff --git a/flutter_custom_tabs_ios/ios/Classes/messages.g.swift b/flutter_custom_tabs_ios/ios/Classes/messages.g.swift index aff179e0..86355bc9 100644 --- a/flutter_custom_tabs_ios/ios/Classes/messages.g.swift +++ b/flutter_custom_tabs_ios/ios/Classes/messages.g.swift @@ -3,210 +3,208 @@ import Foundation #if os(iOS) - import Flutter +import Flutter #elseif os(macOS) - import FlutterMacOS +import FlutterMacOS #else - #error("Unsupported platform.") +#error("Unsupported platform.") #endif private func wrapResult(_ result: Any?) -> [Any?] { - [result] + return [result] } private func wrapError(_ error: Any) -> [Any?] { - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } + if let flutterError = error as? FlutterError { return [ - "\(error)", - "\(type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", + flutterError.code, + flutterError.message, + flutterError.details ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)" + ] } private func isNullish(_ value: Any?) -> Bool { - value is NSNull || value == nil + return value is NSNull || value == nil } private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? + if value is NSNull { return nil } + return value as! T? } /// Generated class from Pigeon that represents data sent in messages. struct SafariViewControllerOptionsMessage { - var preferredBarTintColor: String? - var preferredControlTintColor: String? - var barCollapsingEnabled: Bool? - var entersReaderIfAvailable: Bool? - var dismissButtonStyle: Int64? - var modalPresentationStyle: Int64? - var pageSheet: SheetPresentationControllerConfigurationMessage? - - static func fromList(_ list: [Any?]) -> SafariViewControllerOptionsMessage? { - let preferredBarTintColor: String? = nilOrValue(list[0]) - let preferredControlTintColor: String? = nilOrValue(list[1]) - let barCollapsingEnabled: Bool? = nilOrValue(list[2]) - let entersReaderIfAvailable: Bool? = nilOrValue(list[3]) - let dismissButtonStyle: Int64? = isNullish(list[4]) ? nil : (list[4] is Int64? ? list[4] as! Int64? : Int64(list[4] as! Int32)) - let modalPresentationStyle: Int64? = isNullish(list[5]) ? nil : (list[5] is Int64? ? list[5] as! Int64? : Int64(list[5] as! Int32)) - var pageSheet: SheetPresentationControllerConfigurationMessage? - if let pageSheetList: [Any?] = nilOrValue(list[6]) { - pageSheet = SheetPresentationControllerConfigurationMessage.fromList(pageSheetList) - } - - return SafariViewControllerOptionsMessage( - preferredBarTintColor: preferredBarTintColor, - preferredControlTintColor: preferredControlTintColor, - barCollapsingEnabled: barCollapsingEnabled, - entersReaderIfAvailable: entersReaderIfAvailable, - dismissButtonStyle: dismissButtonStyle, - modalPresentationStyle: modalPresentationStyle, - pageSheet: pageSheet - ) + var preferredBarTintColor: String? = nil + var preferredControlTintColor: String? = nil + var barCollapsingEnabled: Bool? = nil + var entersReaderIfAvailable: Bool? = nil + var dismissButtonStyle: Int64? = nil + var modalPresentationStyle: Int64? = nil + var pageSheet: SheetPresentationControllerConfigurationMessage? = nil + + static func fromList(_ list: [Any?]) -> SafariViewControllerOptionsMessage? { + let preferredBarTintColor: String? = nilOrValue(list[0]) + let preferredControlTintColor: String? = nilOrValue(list[1]) + let barCollapsingEnabled: Bool? = nilOrValue(list[2]) + let entersReaderIfAvailable: Bool? = nilOrValue(list[3]) + let dismissButtonStyle: Int64? = isNullish(list[4]) ? nil : (list[4] is Int64? ? list[4] as! Int64? : Int64(list[4] as! Int32)) + let modalPresentationStyle: Int64? = isNullish(list[5]) ? nil : (list[5] is Int64? ? list[5] as! Int64? : Int64(list[5] as! Int32)) + var pageSheet: SheetPresentationControllerConfigurationMessage? = nil + if let pageSheetList: [Any?] = nilOrValue(list[6]) { + pageSheet = SheetPresentationControllerConfigurationMessage.fromList(pageSheetList) } - func toList() -> [Any?] { - [ - preferredBarTintColor, - preferredControlTintColor, - barCollapsingEnabled, - entersReaderIfAvailable, - dismissButtonStyle, - modalPresentationStyle, - pageSheet?.toList(), - ] - } + return SafariViewControllerOptionsMessage( + preferredBarTintColor: preferredBarTintColor, + preferredControlTintColor: preferredControlTintColor, + barCollapsingEnabled: barCollapsingEnabled, + entersReaderIfAvailable: entersReaderIfAvailable, + dismissButtonStyle: dismissButtonStyle, + modalPresentationStyle: modalPresentationStyle, + pageSheet: pageSheet + ) + } + func toList() -> [Any?] { + return [ + preferredBarTintColor, + preferredControlTintColor, + barCollapsingEnabled, + entersReaderIfAvailable, + dismissButtonStyle, + modalPresentationStyle, + pageSheet?.toList(), + ] + } } /// Generated class from Pigeon that represents data sent in messages. struct SheetPresentationControllerConfigurationMessage { - var detents: [String?] - var largestUndimmedDetentIdentifier: String? - var prefersScrollingExpandsWhenScrolledToEdge: Bool? - var prefersGrabberVisible: Bool? - var prefersEdgeAttachedInCompactHeight: Bool? - var preferredCornerRadius: Double? - - static func fromList(_ list: [Any?]) -> SheetPresentationControllerConfigurationMessage? { - let detents = list[0] as! [String?] - let largestUndimmedDetentIdentifier: String? = nilOrValue(list[1]) - let prefersScrollingExpandsWhenScrolledToEdge: Bool? = nilOrValue(list[2]) - let prefersGrabberVisible: Bool? = nilOrValue(list[3]) - let prefersEdgeAttachedInCompactHeight: Bool? = nilOrValue(list[4]) - let preferredCornerRadius: Double? = nilOrValue(list[5]) - - return SheetPresentationControllerConfigurationMessage( - detents: detents, - largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, - prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, - prefersGrabberVisible: prefersGrabberVisible, - prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, - preferredCornerRadius: preferredCornerRadius - ) - } - - func toList() -> [Any?] { - [ - detents, - largestUndimmedDetentIdentifier, - prefersScrollingExpandsWhenScrolledToEdge, - prefersGrabberVisible, - prefersEdgeAttachedInCompactHeight, - preferredCornerRadius, - ] - } + var detents: [String?] + var largestUndimmedDetentIdentifier: String? = nil + var prefersScrollingExpandsWhenScrolledToEdge: Bool? = nil + var prefersGrabberVisible: Bool? = nil + var prefersEdgeAttachedInCompactHeight: Bool? = nil + var preferredCornerRadius: Double? = nil + + static func fromList(_ list: [Any?]) -> SheetPresentationControllerConfigurationMessage? { + let detents = list[0] as! [String?] + let largestUndimmedDetentIdentifier: String? = nilOrValue(list[1]) + let prefersScrollingExpandsWhenScrolledToEdge: Bool? = nilOrValue(list[2]) + let prefersGrabberVisible: Bool? = nilOrValue(list[3]) + let prefersEdgeAttachedInCompactHeight: Bool? = nilOrValue(list[4]) + let preferredCornerRadius: Double? = nilOrValue(list[5]) + + return SheetPresentationControllerConfigurationMessage( + detents: detents, + largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, + prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, + prefersGrabberVisible: prefersGrabberVisible, + prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, + preferredCornerRadius: preferredCornerRadius + ) + } + func toList() -> [Any?] { + return [ + detents, + largestUndimmedDetentIdentifier, + prefersScrollingExpandsWhenScrolledToEdge, + prefersGrabberVisible, + prefersEdgeAttachedInCompactHeight, + preferredCornerRadius, + ] + } } private class CustomTabsApiCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 128: - return SafariViewControllerOptionsMessage.fromList(readValue() as! [Any?]) - case 129: - return SheetPresentationControllerConfigurationMessage.fromList(readValue() as! [Any?]) - default: - return super.readValue(ofType: type) - } + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 128: + return SafariViewControllerOptionsMessage.fromList(self.readValue() as! [Any?]) + case 129: + return SheetPresentationControllerConfigurationMessage.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) } + } } private class CustomTabsApiCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? SafariViewControllerOptionsMessage { - super.writeByte(128) - super.writeValue(value.toList()) - } else if let value = value as? SheetPresentationControllerConfigurationMessage { - super.writeByte(129) - super.writeValue(value.toList()) - } else { - super.writeValue(value) - } + override func writeValue(_ value: Any) { + if let value = value as? SafariViewControllerOptionsMessage { + super.writeByte(128) + super.writeValue(value.toList()) + } else if let value = value as? SheetPresentationControllerConfigurationMessage { + super.writeByte(129) + super.writeValue(value.toList()) + } else { + super.writeValue(value) } + } } private class CustomTabsApiCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - CustomTabsApiCodecReader(data: data) - } + override func reader(with data: Data) -> FlutterStandardReader { + return CustomTabsApiCodecReader(data: data) + } - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - CustomTabsApiCodecWriter(data: data) - } + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return CustomTabsApiCodecWriter(data: data) + } } class CustomTabsApiCodec: FlutterStandardMessageCodec { - static let shared = CustomTabsApiCodec(readerWriter: CustomTabsApiCodecReaderWriter()) + static let shared = CustomTabsApiCodec(readerWriter: CustomTabsApiCodecReaderWriter()) } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol CustomTabsApi { - func launchURL(_ urlString: String, prefersDeepLink: Bool, options: SafariViewControllerOptionsMessage, completion: @escaping (Result) -> Void) - func closeAllIfPossible() throws + func launchURL(_ urlString: String, prefersDeepLink: Bool, options: SafariViewControllerOptionsMessage?, completion: @escaping (Result) -> Void) + func closeAllIfPossible() throws } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -enum CustomTabsApiSetup { - /// The codec used by CustomTabsApi. - static var codec: FlutterStandardMessageCodec { CustomTabsApiCodec.shared } - /// Sets up an instance of `CustomTabsApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: CustomTabsApi?) { - let launchUrlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_custom_tabs_ios.CustomTabsApi.launchUrl", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - launchUrlChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let urlStringArg = args[0] as! String - let prefersDeepLinkArg = args[1] as! Bool - let optionsArg = args[2] as! SafariViewControllerOptionsMessage - api.launchURL(urlStringArg, prefersDeepLink: prefersDeepLinkArg, options: optionsArg) { result in - switch result { - case .success: - reply(wrapResult(nil)) - case let .failure(error): - reply(wrapError(error)) - } - } - } - } else { - launchUrlChannel.setMessageHandler(nil) +class CustomTabsApiSetup { + /// The codec used by CustomTabsApi. + static var codec: FlutterStandardMessageCodec { CustomTabsApiCodec.shared } + /// Sets up an instance of `CustomTabsApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: CustomTabsApi?) { + let launchChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_custom_tabs_ios.CustomTabsApi.launch", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + launchChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let urlStringArg = args[0] as! String + let prefersDeepLinkArg = args[1] as! Bool + let optionsArg: SafariViewControllerOptionsMessage? = nilOrValue(args[2]) + api.launchURL(urlStringArg, prefersDeepLink: prefersDeepLinkArg, options: optionsArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } } - let closeAllIfPossibleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_custom_tabs_ios.CustomTabsApi.closeAllIfPossible", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - closeAllIfPossibleChannel.setMessageHandler { _, reply in - do { - try api.closeAllIfPossible() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - closeAllIfPossibleChannel.setMessageHandler(nil) + } + } else { + launchChannel.setMessageHandler(nil) + } + let closeAllIfPossibleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_custom_tabs_ios.CustomTabsApi.closeAllIfPossible", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + closeAllIfPossibleChannel.setMessageHandler { _, reply in + do { + try api.closeAllIfPossible() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) } + } + } else { + closeAllIfPossibleChannel.setMessageHandler(nil) } + } } diff --git a/flutter_custom_tabs_ios/lib/src/custom_tabs_plugin_ios.dart b/flutter_custom_tabs_ios/lib/src/custom_tabs_plugin_ios.dart index 1b469e54..45ec1978 100644 --- a/flutter_custom_tabs_ios/lib/src/custom_tabs_plugin_ios.dart +++ b/flutter_custom_tabs_ios/lib/src/custom_tabs_plugin_ios.dart @@ -27,13 +27,19 @@ class CustomTabsPluginIOS extends CustomTabsPlatform { PlatformOptions? customTabsOptions, PlatformOptions? safariVCOptions, }) { - final options = (safariVCOptions is SafariViewControllerOptions) - ? safariVCOptions.toMessage() - : SafariViewControllerOptionsMessage(); - return _hostApi.launchUrl( + final SafariViewControllerOptionsMessage? message; + if (safariVCOptions == null) { + message = null; + } else { + message = (safariVCOptions is SafariViewControllerOptions) + ? safariVCOptions.toMessage() + : SafariViewControllerOptionsMessage(); + } + + return _hostApi.launch( urlString, prefersDeepLink: prefersDeepLink, - options: options, + options: message, ); } diff --git a/flutter_custom_tabs_ios/lib/src/messages.g.dart b/flutter_custom_tabs_ios/lib/src/messages.g.dart index d934ce56..4d67b84e 100644 --- a/flutter_custom_tabs_ios/lib/src/messages.g.dart +++ b/flutter_custom_tabs_ios/lib/src/messages.g.dart @@ -152,8 +152,8 @@ class CustomTabsApi { static const MessageCodec pigeonChannelCodec = _CustomTabsApiCodec(); - Future launchUrl(String urlString, {required bool prefersDeepLink, required SafariViewControllerOptionsMessage options,}) async { - const String __pigeon_channelName = 'dev.flutter.pigeon.flutter_custom_tabs_ios.CustomTabsApi.launchUrl'; + Future launch(String urlString, {required bool prefersDeepLink, SafariViewControllerOptionsMessage? options,}) async { + const String __pigeon_channelName = 'dev.flutter.pigeon.flutter_custom_tabs_ios.CustomTabsApi.launch'; final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, diff --git a/flutter_custom_tabs_ios/pigeons/messages.dart b/flutter_custom_tabs_ios/pigeons/messages.dart index 79ab8f4c..0f4c9b9b 100644 --- a/flutter_custom_tabs_ios/pigeons/messages.dart +++ b/flutter_custom_tabs_ios/pigeons/messages.dart @@ -8,10 +8,10 @@ import 'package:pigeon/pigeon.dart'; abstract class CustomTabsApi { @async @SwiftFunction('launchURL(_:prefersDeepLink:options:)') - void launchUrl( + void launch( String urlString, { required bool prefersDeepLink, - required SafariViewControllerOptionsMessage options, + SafariViewControllerOptionsMessage? options, }); void closeAllIfPossible(); diff --git a/flutter_custom_tabs_ios/test/custom_tabs_plugin_ios_ios_test.dart b/flutter_custom_tabs_ios/test/custom_tabs_plugin_ios_ios_test.dart index 35711eee..d305ba90 100644 --- a/flutter_custom_tabs_ios/test/custom_tabs_plugin_ios_ios_test.dart +++ b/flutter_custom_tabs_ios/test/custom_tabs_plugin_ios_ios_test.dart @@ -41,17 +41,32 @@ void main() { test('launch() invoke method "launch" with invalid options', () async { const url = 'http://example.com/'; const prefersDeepLink = false; + const options = _Options( + barCollapsingEnabled: true, + ); api.setLaunchExpectations( url: url, prefersDeepLink: prefersDeepLink, + options: options, ); await customTabs.launch( url, prefersDeepLink: prefersDeepLink, customTabsOptions: const _Options(), - safariVCOptions: const _Options( - barCollapsingEnabled: true, - ), + safariVCOptions: options, + ); + }); + + test('launch() invoke method "launch" with no options', () async { + const url = 'http://example.com/'; + const prefersDeepLink = false; + api.setLaunchExpectations( + url: url, + prefersDeepLink: prefersDeepLink, + ); + await customTabs.launch( + url, + prefersDeepLink: prefersDeepLink, ); }); @@ -79,19 +94,22 @@ class _MockCustomTabsApi implements CustomTabsApi { } @override - Future launchUrl( + Future launch( String url, { required bool prefersDeepLink, - required SafariViewControllerOptionsMessage options, + SafariViewControllerOptionsMessage? options, }) async { expect(url, this.url); expect(prefersDeepLink, this.prefersDeepLink); - if (this.options is SafariViewControllerOptions) { - final message = (this.options as SafariViewControllerOptions).toMessage(); - expect(options.barCollapsingEnabled, message.barCollapsingEnabled); + if (this.options == null) { + expect(options, isNull); + } else if (this.options is SafariViewControllerOptions) { + final expected = + (this.options as SafariViewControllerOptions).toMessage(); + expect(options?.barCollapsingEnabled, expected.barCollapsingEnabled); } else { - expect(options.barCollapsingEnabled, isNull); + expect(options, isNotNull); } launchUrlCalled = true; } From eb1c2950f0a73783f538fba1c1b6f9045c60018a Mon Sep 17 00:00:00 2001 From: Shinya Kumagai Date: Tue, 5 Dec 2023 21:14:20 +0900 Subject: [PATCH 3/4] Add support for launching a URL in an external browser --- flutter_custom_tabs/example/lib/main.dart | 15 +++++++++++++++ flutter_custom_tabs/lib/src/launcher.dart | 14 +++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/flutter_custom_tabs/example/lib/main.dart b/flutter_custom_tabs/example/lib/main.dart index d90d3238..34ddb428 100644 --- a/flutter_custom_tabs/example/lib/main.dart +++ b/flutter_custom_tabs/example/lib/main.dart @@ -71,6 +71,10 @@ class MyApp extends StatelessWidget { onPressed: () => _launchAndCloseManually(context), child: const Text('Show flutter.dev + close after 5 seconds'), ), + FilledButton( + onPressed: () => _launchInExternalBrowser(), + child: const Text('Show flutter.dev in external browser'), + ), ], ), ), @@ -282,3 +286,14 @@ Future _launchAndCloseManually(BuildContext context) async { debugPrint(e.toString()); } } + +Future _launchInExternalBrowser() async { + try { + await launchUrl( + Uri.parse('https://flutter.dev'), + prefersDeepLink: false, + ); + } catch (e) { + debugPrint(e.toString()); + } +} diff --git a/flutter_custom_tabs/lib/src/launcher.dart b/flutter_custom_tabs/lib/src/launcher.dart index 64644f23..5c4068ce 100644 --- a/flutter_custom_tabs/lib/src/launcher.dart +++ b/flutter_custom_tabs/lib/src/launcher.dart @@ -11,7 +11,9 @@ import 'package:flutter_custom_tabs_platform_interface/flutter_custom_tabs_platf /// - On iOS, the appearance and behavior of [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) can be customized using the [safariVCOptions] parameter. /// - For web, customization options are not available. /// -/// Example: +/// If [customTabsOptions] or [safariVCOptions] are `null`, the URL will be launched in an external browser on mobile platforms. +/// +/// Example of launching Custom Tabs: /// /// ```dart /// final theme = ...; @@ -39,6 +41,16 @@ import 'package:flutter_custom_tabs_platform_interface/flutter_custom_tabs_platf /// // An exception is thrown if browser app is not installed on Android device. /// } /// ``` +/// +/// Example of launching an external browser: +/// +/// ```dart +/// try { +/// await launchUrl(Uri.parse('https://flutter.dev')); +/// } catch (e) { +/// // An exception is thrown if browser app is not installed on Android device. +/// } +/// ``` Future launchUrl( Uri url, { bool prefersDeepLink = false, From 3330e75c1d40ea4a3659fabb552b34320534b61f Mon Sep 17 00:00:00 2001 From: Shinya Kumagai Date: Wed, 6 Dec 2023 14:17:03 +0900 Subject: [PATCH 4/4] Enhance README in `flutter_custom_tabs` --- flutter_custom_tabs/README.md | 37 +++++++++++++++---- flutter_custom_tabs/example/lib/main.dart | 4 +- .../example/lib/main.dart | 4 +- flutter_custom_tabs_ios/example/lib/main.dart | 4 +- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/flutter_custom_tabs/README.md b/flutter_custom_tabs/README.md index 08cbec01..fa254a21 100644 --- a/flutter_custom_tabs/README.md +++ b/flutter_custom_tabs/README.md @@ -2,7 +2,11 @@ [![pub package](https://img.shields.io/pub/v/flutter_custom_tabs.svg)](https://pub.dartlang.org/packages/flutter_custom_tabs) A Flutter plugin for mobile apps to launch a URL in Custom Tabs. -This plugin allows you to add the browser experience that Custom Tabs provides to your mobile apps. +The plugin allows you to add the browser experience that Custom Tabs provides to your mobile apps. + +In version 2.0, the plugin expands the support for launching a URL in mobile apps: +- Launch a URL in an external browser. +- Launch a deep link URL. | | Android | iOS | Web | |-------------|---------|-------|-------| @@ -24,7 +28,7 @@ dependencies: - Android Gradle Plugin v7.4.0 and above. - Kotlin v1.7.0 and above. -```diff +```groovy // your-project/android/build.gradle buildscript { ext.kotlin_version = '1.7.0' // and above if explicitly depending on Kotlin. @@ -52,7 +56,7 @@ void _launchURL(BuildContext context) async { final theme = Theme.of(context); try { await launchUrl( - Uri.parse('https://flutter.dev'), + Uri.parse('https://flutter.dev'), customTabsOptions: CustomTabsOptions( colorSchemes: CustomTabsColorSchemes.defaults( toolbarColor: theme.colorScheme.surface, @@ -85,8 +89,8 @@ See the example app for more complex examples. This package supports a wide range of Custom Tabs customizations, but we have experimentally introduced a lightweight URL launch for users who don't need as much in v2.0.0. -> [!TIP] -> On Android, **the lightweight version** prioritizes launching the default browser that supports Custom Tabs over Chrome. +> [!NOTE] +> On Android, **the lightweight version** prefers launching the default browser that supports Custom Tabs over Chrome. ```dart import 'package:flutter/material.dart'; @@ -136,12 +140,13 @@ Support status in `flutter_custom_tabs`: ## Advanced Usage ### Deep Linking -Supports launching deep link URLs. +Supports launching a deep link URL. (If a native app that responds to the deep link URL is installed, it will directly launch it. otherwise, it will launch a custom tab.) ```dart import 'package:flutter/material.dart'; import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; +// or import 'package:flutter_custom_tabs/flutter_custom_tabs_lite.dart'; Future _launchDeepLinkURL(BuildContext context) async { final theme = Theme.of(context); @@ -161,8 +166,22 @@ Future _launchDeepLinkURL(BuildContext context) async { } ``` +## Launch in an external browser +By default, if no mobile platform-specific options are specified, a URL will be launched in an external browser. + +> [!TIP] +> Android: `CustomTabsOptions.externalBrowser` supports HTTP request headers. + +```dart +import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; + +Future _launchInExternalBrowser() async { + await launchUrl(Uri.parse('https://flutter.dev')); +} +``` + ### Show as a bottom sheet -You can launch URLs in Custom Tabs as a bottom sheet. +You can launch a URL in Custom Tabs as a bottom sheet. Requirements: - Android: Chrome v107 and above or [other browsers](https://developer.chrome.com/docs/android/custom-tabs/browser-support/#setinitialactivityheightpx) @@ -204,7 +223,7 @@ Future _launchURLInBottomSheet(BuildContext context) async { ``` ### Prefer the default browser over Chrome -On Android, the default browser to launch is Chrome, which supports all Custom Tabs features. +On Android, the plugin defaults to launching Chrome, which supports all Custom Tabs features. You can prioritize launching the default browser on the device that supports Custom Tabs over Chrome. > [!NOTE] @@ -212,6 +231,8 @@ You can prioritize launching the default browser on the device that supports Cus > - See: [Custom Tabs Browser Support](https://developer.chrome.com/docs/android/custom-tabs/browser-support/). ```dart +import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; + Future _launchURLInDefaultBrowserOnAndroid() async { await launchUrl( Uri.parse('https://flutter.dev'), diff --git a/flutter_custom_tabs/example/lib/main.dart b/flutter_custom_tabs/example/lib/main.dart index 34ddb428..194660c4 100644 --- a/flutter_custom_tabs/example/lib/main.dart +++ b/flutter_custom_tabs/example/lib/main.dart @@ -52,7 +52,7 @@ class MyApp extends StatelessWidget { child: const Text('Show flutter.dev (lite ver)'), ), FilledButton( - onPressed: () => _launchDeepLinkingURL(context), + onPressed: () => _launchDeepLinkURL(context), child: const Text('Deep link to platform maps'), ), FilledButton( @@ -149,7 +149,7 @@ Future _launchUrlLite(BuildContext context) async { } } -Future _launchDeepLinkingURL(BuildContext context) async { +Future _launchDeepLinkURL(BuildContext context) async { final theme = Theme.of(context); final uri = Platform.isIOS ? 'https://maps.apple.com/?q=tokyo+station' diff --git a/flutter_custom_tabs_android/example/lib/main.dart b/flutter_custom_tabs_android/example/lib/main.dart index 33c60ec4..3a1b8ed5 100644 --- a/flutter_custom_tabs_android/example/lib/main.dart +++ b/flutter_custom_tabs_android/example/lib/main.dart @@ -48,7 +48,7 @@ class MyApp extends StatelessWidget { child: const Text('Show flutter.dev in bottom sheet'), ), FilledButton.tonal( - onPressed: () => _launchDeepLinkingURL(context), + onPressed: () => _launchDeepLinkURL(context), child: const Text('Deep link to Google Maps'), ), FilledButton.tonal( @@ -138,7 +138,7 @@ Future _launchURLInBottomSheet(BuildContext context) async { } } -Future _launchDeepLinkingURL(BuildContext context) async { +Future _launchDeepLinkURL(BuildContext context) async { final theme = Theme.of(context); try { await CustomTabsPlatform.instance.launch( diff --git a/flutter_custom_tabs_ios/example/lib/main.dart b/flutter_custom_tabs_ios/example/lib/main.dart index bfb1debb..120ce76c 100644 --- a/flutter_custom_tabs_ios/example/lib/main.dart +++ b/flutter_custom_tabs_ios/example/lib/main.dart @@ -41,7 +41,7 @@ class MyApp extends StatelessWidget { child: const Text('Show flutter.dev in bottom sheet'), ), FilledButton( - onPressed: () => _launchDeepLinkingURL(context), + onPressed: () => _launchDeepLinkURL(context), child: const Text('Deep link to Apple Maps'), ), FilledButton( @@ -107,7 +107,7 @@ Future _launchURLInBottomSheet(BuildContext context) async { } } -Future _launchDeepLinkingURL(BuildContext context) async { +Future _launchDeepLinkURL(BuildContext context) async { final theme = Theme.of(context); try { await CustomTabsPlatform.instance.launch(