diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 0d8e243799e..e1fe66e08a9 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.10.0 + +* Adds support for the Payment Request API with `AndroidWebViewController.isWebViewFeatureSupported` and `AndroidWebViewController.setPaymentRequestEnabled`. + ## 4.9.1 * Updates kotlin version to 2.2.0 to enable gradle 8.11 support. diff --git a/packages/webview_flutter/webview_flutter_android/README.md b/packages/webview_flutter/webview_flutter_android/README.md index 214b50c079b..060a0fb7e37 100644 --- a/packages/webview_flutter/webview_flutter_android/README.md +++ b/packages/webview_flutter/webview_flutter_android/README.md @@ -52,6 +52,37 @@ Java: import io.flutter.plugins.webviewflutter.WebViewFlutterAndroidExternalApi; ``` +## Enable Payment Request in WebView + +The Payment Request API can be enabled by calling `AndroidWebViewController.setPaymentRequestEnabled` after +checking `AndroidWebViewController.isWebViewFeatureSupported`. + + +```dart +final bool paymentRequestEnabled = await androidController + .isWebViewFeatureSupported(WebViewFeatureType.paymentRequest); + +if (paymentRequestEnabled) { + await androidController.setPaymentRequestEnabled(true); +} +``` + +Add intent filters to your AndroidManifest.xml to discover and invoke Android payment apps using system intents: + +```xml + + + + + + + + + + + +``` + ## Fullscreen Video To display a video as fullscreen, an app must manually handle the notification that the current page diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/AndroidWebkitLibrary.g.kt b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/AndroidWebkitLibrary.g.kt index 58fc0ed2924..3a9932d3f4f 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/AndroidWebkitLibrary.g.kt +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/AndroidWebkitLibrary.g.kt @@ -567,6 +567,18 @@ abstract class AndroidWebkitLibraryPigeonProxyApiRegistrar(val binaryMessenger: */ abstract fun getPigeonApiCertificate(): PigeonApiCertificate + /** + * An implementation of [PigeonApiWebSettingsCompat] used to add a new Dart instance of + * `WebSettingsCompat` to the Dart `InstanceManager`. + */ + abstract fun getPigeonApiWebSettingsCompat(): PigeonApiWebSettingsCompat + + /** + * An implementation of [PigeonApiWebViewFeature] used to add a new Dart instance of + * `WebViewFeature` to the Dart `InstanceManager`. + */ + abstract fun getPigeonApiWebViewFeature(): PigeonApiWebViewFeature + fun setUp() { AndroidWebkitLibraryPigeonInstanceManagerApi.setUpMessageHandlers( binaryMessenger, instanceManager) @@ -598,6 +610,9 @@ abstract class AndroidWebkitLibraryPigeonProxyApiRegistrar(val binaryMessenger: binaryMessenger, getPigeonApiSslCertificateDName()) PigeonApiSslCertificate.setUpMessageHandlers(binaryMessenger, getPigeonApiSslCertificate()) PigeonApiCertificate.setUpMessageHandlers(binaryMessenger, getPigeonApiCertificate()) + PigeonApiWebSettingsCompat.setUpMessageHandlers( + binaryMessenger, getPigeonApiWebSettingsCompat()) + PigeonApiWebViewFeature.setUpMessageHandlers(binaryMessenger, getPigeonApiWebViewFeature()) } fun tearDown() { @@ -623,6 +638,8 @@ abstract class AndroidWebkitLibraryPigeonProxyApiRegistrar(val binaryMessenger: PigeonApiSslCertificateDName.setUpMessageHandlers(binaryMessenger, null) PigeonApiSslCertificate.setUpMessageHandlers(binaryMessenger, null) PigeonApiCertificate.setUpMessageHandlers(binaryMessenger, null) + PigeonApiWebSettingsCompat.setUpMessageHandlers(binaryMessenger, null) + PigeonApiWebViewFeature.setUpMessageHandlers(binaryMessenger, null) } } @@ -727,6 +744,10 @@ private class AndroidWebkitLibraryPigeonProxyApiBaseCodec( registrar.getPigeonApiSslCertificate().pigeon_newInstance(value) {} } else if (value is java.security.cert.Certificate) { registrar.getPigeonApiCertificate().pigeon_newInstance(value) {} + } else if (value is androidx.webkit.WebSettingsCompat) { + registrar.getPigeonApiWebSettingsCompat().pigeon_newInstance(value) {} + } else if (value is androidx.webkit.WebViewFeature) { + registrar.getPigeonApiWebViewFeature().pigeon_newInstance(value) {} } when { @@ -6310,3 +6331,159 @@ abstract class PigeonApiCertificate( } } } +/** + * Compatibility version of `WebSettings`. + * + * See https://developer.android.com/reference/kotlin/androidx/webkit/WebSettingsCompat. + */ +@Suppress("UNCHECKED_CAST") +abstract class PigeonApiWebSettingsCompat( + open val pigeonRegistrar: AndroidWebkitLibraryPigeonProxyApiRegistrar +) { + abstract fun setPaymentRequestEnabled(webSettings: android.webkit.WebSettings, enabled: Boolean) + + companion object { + @Suppress("LocalVariableName") + fun setUpMessageHandlers(binaryMessenger: BinaryMessenger, api: PigeonApiWebSettingsCompat?) { + val codec = api?.pigeonRegistrar?.codec ?: AndroidWebkitLibraryPigeonCodec() + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.webview_flutter_android.WebSettingsCompat.setPaymentRequestEnabled", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val webSettingsArg = args[0] as android.webkit.WebSettings + val enabledArg = args[1] as Boolean + val wrapped: List = + try { + api.setPaymentRequestEnabled(webSettingsArg, enabledArg) + listOf(null) + } catch (exception: Throwable) { + AndroidWebkitLibraryPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } + + @Suppress("LocalVariableName", "FunctionName") + /** Creates a Dart instance of WebSettingsCompat and attaches it to [pigeon_instanceArg]. */ + fun pigeon_newInstance( + pigeon_instanceArg: androidx.webkit.WebSettingsCompat, + callback: (Result) -> Unit + ) { + if (pigeonRegistrar.ignoreCallsToDart) { + callback( + Result.failure( + AndroidWebKitError("ignore-calls-error", "Calls to Dart are being ignored.", ""))) + } else if (pigeonRegistrar.instanceManager.containsInstance(pigeon_instanceArg)) { + callback(Result.success(Unit)) + } else { + val pigeon_identifierArg = + pigeonRegistrar.instanceManager.addHostCreatedInstance(pigeon_instanceArg) + val binaryMessenger = pigeonRegistrar.binaryMessenger + val codec = pigeonRegistrar.codec + val channelName = + "dev.flutter.pigeon.webview_flutter_android.WebSettingsCompat.pigeon_newInstance" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(pigeon_identifierArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback( + Result.failure( + AndroidWebKitError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback( + Result.failure(AndroidWebkitLibraryPigeonUtils.createConnectionError(channelName))) + } + } + } + } +} +/** + * Utility class for checking which WebView Support Library features are supported on the device. + * + * See https://developer.android.com/reference/kotlin/androidx/webkit/WebViewFeature. + */ +@Suppress("UNCHECKED_CAST") +abstract class PigeonApiWebViewFeature( + open val pigeonRegistrar: AndroidWebkitLibraryPigeonProxyApiRegistrar +) { + abstract fun isFeatureSupported(feature: String): Boolean + + companion object { + @Suppress("LocalVariableName") + fun setUpMessageHandlers(binaryMessenger: BinaryMessenger, api: PigeonApiWebViewFeature?) { + val codec = api?.pigeonRegistrar?.codec ?: AndroidWebkitLibraryPigeonCodec() + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.webview_flutter_android.WebViewFeature.isFeatureSupported", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val featureArg = args[0] as String + val wrapped: List = + try { + listOf(api.isFeatureSupported(featureArg)) + } catch (exception: Throwable) { + AndroidWebkitLibraryPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } + + @Suppress("LocalVariableName", "FunctionName") + /** Creates a Dart instance of WebViewFeature and attaches it to [pigeon_instanceArg]. */ + fun pigeon_newInstance( + pigeon_instanceArg: androidx.webkit.WebViewFeature, + callback: (Result) -> Unit + ) { + if (pigeonRegistrar.ignoreCallsToDart) { + callback( + Result.failure( + AndroidWebKitError("ignore-calls-error", "Calls to Dart are being ignored.", ""))) + } else if (pigeonRegistrar.instanceManager.containsInstance(pigeon_instanceArg)) { + callback(Result.success(Unit)) + } else { + val pigeon_identifierArg = + pigeonRegistrar.instanceManager.addHostCreatedInstance(pigeon_instanceArg) + val binaryMessenger = pigeonRegistrar.binaryMessenger + val codec = pigeonRegistrar.codec + val channelName = + "dev.flutter.pigeon.webview_flutter_android.WebViewFeature.pigeon_newInstance" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(pigeon_identifierArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback( + Result.failure( + AndroidWebKitError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback( + Result.failure(AndroidWebkitLibraryPigeonUtils.createConnectionError(channelName))) + } + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ProxyApiRegistrar.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ProxyApiRegistrar.java index 671ecf425f4..84e025a7a0c 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ProxyApiRegistrar.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ProxyApiRegistrar.java @@ -251,4 +251,16 @@ public void setContext(@NonNull Context context) { public FlutterAssetManager getFlutterAssetManager() { return flutterAssetManager; } + + @NonNull + @Override + public PigeonApiWebViewFeature getPigeonApiWebViewFeature() { + return new WebViewFeatureProxyApi(this); + } + + @NonNull + @Override + public PigeonApiWebSettingsCompat getPigeonApiWebSettingsCompat() { + return new WebSettingsCompatProxyApi(this); + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsCompatProxyApi.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsCompatProxyApi.java new file mode 100644 index 00000000000..c949d106f36 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsCompatProxyApi.java @@ -0,0 +1,32 @@ +// 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. + +package io.flutter.plugins.webviewflutter; + +import android.annotation.SuppressLint; +import android.webkit.WebSettings; +import androidx.annotation.NonNull; +import androidx.webkit.WebSettingsCompat; + +/** + * Proxy API implementation for {@link WebSettingsCompat}. + * + *

This class may handle instantiating and adding native object instances that are attached to a + * Dart instance or handle method calls on the associated native class or an instance of the class. + */ +public class WebSettingsCompatProxyApi extends PigeonApiWebSettingsCompat { + public WebSettingsCompatProxyApi(@NonNull ProxyApiRegistrar pigeonRegistrar) { + super(pigeonRegistrar); + } + + /** + * This method should only be called if {@link WebViewFeatureProxyApi#isFeatureSupported(String)} + * with PAYMENT_REQUEST returns true. + */ + @SuppressLint("RequiresFeature") + @Override + public void setPaymentRequestEnabled(@NonNull WebSettings webSettings, boolean enabled) { + WebSettingsCompat.setPaymentRequestEnabled(webSettings, enabled); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFeatureProxyApi.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFeatureProxyApi.java new file mode 100644 index 00000000000..e1a58ad35dd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFeatureProxyApi.java @@ -0,0 +1,25 @@ +// 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. + +package io.flutter.plugins.webviewflutter; + +import androidx.annotation.NonNull; +import androidx.webkit.WebViewFeature; + +/** + * Proxy API implementation for {@link WebViewFeature}. + * + *

This class may handle instantiating and adding native object instances that are attached to a + * Dart instance or handle method calls on the associated native class or an instance of the class. + */ +public class WebViewFeatureProxyApi extends PigeonApiWebViewFeature { + public WebViewFeatureProxyApi(@NonNull ProxyApiRegistrar pigeonRegistrar) { + super(pigeonRegistrar); + } + + @Override + public boolean isFeatureSupported(@NonNull String feature) { + return WebViewFeature.isFeatureSupported(feature); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebSettingsCompatTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebSettingsCompatTest.java new file mode 100644 index 00000000000..2219fa555f6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebSettingsCompatTest.java @@ -0,0 +1,39 @@ +// 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. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import android.webkit.WebSettings; +import androidx.webkit.WebSettingsCompat; +import androidx.webkit.WebViewFeature; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class WebSettingsCompatTest { + @Test + public void setPaymentRequestEnabled() { + final PigeonApiWebSettingsCompat api = + new TestProxyApiRegistrar().getPigeonApiWebSettingsCompat(); + + final WebSettings webSettings = mock(WebSettings.class); + + try (MockedStatic mockedStatic = mockStatic(WebSettingsCompat.class)) { + try (MockedStatic mockedWebViewFeature = mockStatic(WebViewFeature.class)) { + mockedWebViewFeature + .when(() -> WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) + .thenReturn(true); + api.setPaymentRequestEnabled(webSettings, true); + mockedStatic.verify(() -> WebSettingsCompat.setPaymentRequestEnabled(webSettings, true)); + } catch (Exception e) { + fail(e.toString()); + } + } catch (Exception e) { + fail(e.toString()); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFeatureTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFeatureTest.java new file mode 100644 index 00000000000..a30b13a9b7c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFeatureTest.java @@ -0,0 +1,54 @@ +// 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. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mockStatic; + +import androidx.webkit.WebViewFeature; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class WebViewFeatureTest { + @Test + public void isFeatureSupported() { + final PigeonApiWebViewFeature api = new TestProxyApiRegistrar().getPigeonApiWebViewFeature(); + + try (MockedStatic mockedStatic = mockStatic(WebViewFeature.class)) { + mockedStatic + .when(() -> WebViewFeature.isFeatureSupported("PAYMENT_REQUEST")) + .thenReturn(true); + + boolean result = api.isFeatureSupported("PAYMENT_REQUEST"); + + assertTrue(result); + + mockedStatic.verify(() -> WebViewFeature.isFeatureSupported("PAYMENT_REQUEST")); + } catch (Exception e) { + fail(e.toString()); + } + } + + @Test + public void isFeatureSupportedReturnsFalse() { + final PigeonApiWebViewFeature api = new TestProxyApiRegistrar().getPigeonApiWebViewFeature(); + + try (MockedStatic mockedStatic = mockStatic(WebViewFeature.class)) { + mockedStatic + .when(() -> WebViewFeature.isFeatureSupported("UNSUPPORTED_FEATURE")) + .thenReturn(false); + + boolean result = api.isFeatureSupported("UNSUPPORTED_FEATURE"); + + assertFalse(result); + + mockedStatic.verify(() -> WebViewFeature.isFeatureSupported("UNSUPPORTED_FEATURE")); + } catch (Exception e) { + fail(e.toString()); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index 6608c7bebbd..6f877b989f4 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -350,6 +350,7 @@ enum MenuOptions { basicAuthentication, javaScriptAlert, viewportMeta, + checkWebViewPaymentRequestFeatureEnabled, } class SampleMenu extends StatelessWidget { @@ -407,6 +408,8 @@ class SampleMenu extends StatelessWidget { _onJavaScriptAlertExample(context); case MenuOptions.viewportMeta: _onViewportMetaExample(); + case MenuOptions.checkWebViewPaymentRequestFeatureEnabled: + _onWebViewFeatureExample(context); } }, itemBuilder: (BuildContext context) => >[ @@ -483,6 +486,10 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.viewportMeta, child: Text('Viewport meta example'), ), + const PopupMenuItem( + value: MenuOptions.checkWebViewPaymentRequestFeatureEnabled, + child: Text('WebView Feature Example'), + ), ], ); } @@ -783,6 +790,24 @@ class SampleMenu extends StatelessWidget { Future _onViewportMetaExample() { return webViewController.loadHtmlString(kViewportMetaPage); } + + Future _onWebViewFeatureExample(BuildContext context) async { + final AndroidWebViewController androidController = + webViewController as AndroidWebViewController; + // #docregion payment_request_example + final bool paymentRequestEnabled = await androidController + .isWebViewFeatureSupported(WebViewFeatureType.paymentRequest); + + if (paymentRequestEnabled) { + await androidController.setPaymentRequestEnabled(true); + } + // #enddocregion payment_request_example + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Payment Request API supported: $paymentRequestEnabled'), + )); + } + } } class NavigationControls extends StatelessWidget { diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart index e32520c358b..12c460f4129 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart @@ -26,6 +26,8 @@ class AndroidWebViewProxy { this.instanceCookieManager = _instanceCookieManager, this.instanceFlutterAssetManager = _instanceFlutterAssetManager, this.instanceWebStorage = _instanceWebStorage, + this.isWebViewFeatureSupported = WebViewFeature.isFeatureSupported, + this.setPaymentRequestEnabled = WebSettingsCompat.setPaymentRequestEnabled, }); /// Constructs [WebView]. @@ -168,6 +170,12 @@ class AndroidWebViewProxy { /// Calls to [WebStorage.instance]. final WebStorage Function() instanceWebStorage; + /// Calls to [WebViewFeature.isFeatureSupported]. + final Future Function(String) isWebViewFeatureSupported; + + /// Calls to [WebSettingsCompat.setPaymentRequestEnabled]. + final Future Function(WebSettings, bool) setPaymentRequestEnabled; + static CookieManager _instanceCookieManager() => CookieManager.instance; static FlutterAssetManager _instanceFlutterAssetManager() => diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webkit.g.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webkit.g.dart index 2675271ce50..12f7d30aa5f 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webkit.g.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webkit.g.dart @@ -196,6 +196,10 @@ class PigeonInstanceManager { pigeon_instanceManager: instanceManager); Certificate.pigeon_setUpMessageHandlers( pigeon_instanceManager: instanceManager); + WebSettingsCompat.pigeon_setUpMessageHandlers( + pigeon_instanceManager: instanceManager); + WebViewFeature.pigeon_setUpMessageHandlers( + pigeon_instanceManager: instanceManager); return instanceManager; } @@ -8349,3 +8353,228 @@ class Certificate extends PigeonInternalProxyApiBaseClass { ); } } + +/// Compatibility version of `WebSettings`. +/// +/// See https://developer.android.com/reference/kotlin/androidx/webkit/WebSettingsCompat. +class WebSettingsCompat extends PigeonInternalProxyApiBaseClass { + /// Constructs [WebSettingsCompat] without creating the associated native object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies for an [PigeonInstanceManager]. + @protected + WebSettingsCompat.pigeon_detached({ + super.pigeon_binaryMessenger, + super.pigeon_instanceManager, + }); + + late final _PigeonInternalProxyApiBaseCodec + _pigeonVar_codecWebSettingsCompat = + _PigeonInternalProxyApiBaseCodec(pigeon_instanceManager); + + static void pigeon_setUpMessageHandlers({ + bool pigeon_clearHandlers = false, + BinaryMessenger? pigeon_binaryMessenger, + PigeonInstanceManager? pigeon_instanceManager, + WebSettingsCompat Function()? pigeon_newInstance, + }) { + final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = + _PigeonInternalProxyApiBaseCodec( + pigeon_instanceManager ?? PigeonInstanceManager.instance); + final BinaryMessenger? binaryMessenger = pigeon_binaryMessenger; + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.webview_flutter_android.WebSettingsCompat.pigeon_newInstance', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (pigeon_clearHandlers) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebSettingsCompat.pigeon_newInstance was null.'); + final List args = (message as List?)!; + final int? arg_pigeon_instanceIdentifier = (args[0] as int?); + assert(arg_pigeon_instanceIdentifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebSettingsCompat.pigeon_newInstance was null, expected non-null int.'); + try { + (pigeon_instanceManager ?? PigeonInstanceManager.instance) + .addHostCreatedInstance( + pigeon_newInstance?.call() ?? + WebSettingsCompat.pigeon_detached( + pigeon_binaryMessenger: pigeon_binaryMessenger, + pigeon_instanceManager: pigeon_instanceManager, + ), + arg_pigeon_instanceIdentifier!, + ); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } + + static Future setPaymentRequestEnabled( + WebSettings webSettings, + bool enabled, { + BinaryMessenger? pigeon_binaryMessenger, + PigeonInstanceManager? pigeon_instanceManager, + }) async { + final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = + _PigeonInternalProxyApiBaseCodec( + pigeon_instanceManager ?? PigeonInstanceManager.instance); + final BinaryMessenger? pigeonVar_binaryMessenger = pigeon_binaryMessenger; + const String pigeonVar_channelName = + 'dev.flutter.pigeon.webview_flutter_android.WebSettingsCompat.setPaymentRequestEnabled'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([webSettings, enabled]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + @override + WebSettingsCompat pigeon_copy() { + return WebSettingsCompat.pigeon_detached( + pigeon_binaryMessenger: pigeon_binaryMessenger, + pigeon_instanceManager: pigeon_instanceManager, + ); + } +} + +/// Utility class for checking which WebView Support Library features are supported on the device. +/// +/// See https://developer.android.com/reference/kotlin/androidx/webkit/WebViewFeature. +class WebViewFeature extends PigeonInternalProxyApiBaseClass { + /// Constructs [WebViewFeature] without creating the associated native object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies for an [PigeonInstanceManager]. + @protected + WebViewFeature.pigeon_detached({ + super.pigeon_binaryMessenger, + super.pigeon_instanceManager, + }); + + late final _PigeonInternalProxyApiBaseCodec _pigeonVar_codecWebViewFeature = + _PigeonInternalProxyApiBaseCodec(pigeon_instanceManager); + + static void pigeon_setUpMessageHandlers({ + bool pigeon_clearHandlers = false, + BinaryMessenger? pigeon_binaryMessenger, + PigeonInstanceManager? pigeon_instanceManager, + WebViewFeature Function()? pigeon_newInstance, + }) { + final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = + _PigeonInternalProxyApiBaseCodec( + pigeon_instanceManager ?? PigeonInstanceManager.instance); + final BinaryMessenger? binaryMessenger = pigeon_binaryMessenger; + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.webview_flutter_android.WebViewFeature.pigeon_newInstance', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (pigeon_clearHandlers) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebViewFeature.pigeon_newInstance was null.'); + final List args = (message as List?)!; + final int? arg_pigeon_instanceIdentifier = (args[0] as int?); + assert(arg_pigeon_instanceIdentifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebViewFeature.pigeon_newInstance was null, expected non-null int.'); + try { + (pigeon_instanceManager ?? PigeonInstanceManager.instance) + .addHostCreatedInstance( + pigeon_newInstance?.call() ?? + WebViewFeature.pigeon_detached( + pigeon_binaryMessenger: pigeon_binaryMessenger, + pigeon_instanceManager: pigeon_instanceManager, + ), + arg_pigeon_instanceIdentifier!, + ); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } + + static Future isFeatureSupported( + String feature, { + BinaryMessenger? pigeon_binaryMessenger, + PigeonInstanceManager? pigeon_instanceManager, + }) async { + final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = + _PigeonInternalProxyApiBaseCodec( + pigeon_instanceManager ?? PigeonInstanceManager.instance); + final BinaryMessenger? pigeonVar_binaryMessenger = pigeon_binaryMessenger; + const String pigeonVar_channelName = + 'dev.flutter.pigeon.webview_flutter_android.WebViewFeature.isFeatureSupported'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([feature]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + @override + WebViewFeature pigeon_copy() { + return WebViewFeature.pigeon_detached( + pigeon_binaryMessenger: pigeon_binaryMessenger, + pigeon_instanceManager: pigeon_instanceManager, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webkit_constants.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webkit_constants.dart index 81cd06c7176..2d962ccd2f1 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webkit_constants.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webkit_constants.dart @@ -117,3 +117,14 @@ class WebViewClientConstants { /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_UNSUPPORTED_SCHEME static const int errorUnsupportedScheme = -10; } + +/// Class constants for [WebViewFeature]. +/// +/// Since the Dart [WebViewFeature] is generated, the constants for the class +/// are added here. +class WebViewFeatureConstants { + /// This feature covers [WebSettingsCompat.setPaymentRequestEnabled]. + /// + /// See https://developer.android.com/reference/androidx/webkit/WebViewFeature#PAYMENT_REQUEST. + static const String paymentRequest = 'PAYMENT_REQUEST'; +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart index a9954d76bad..50f233181cc 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart @@ -833,6 +833,38 @@ class AndroidWebViewController extends PlatformWebViewController { }; return _webView.settings.setMixedContentMode(androidMode); } + + /// Checks if a WebView feature is supported on the current device. + /// + /// This method uses [android_webview.WebViewFeature.isFeatureSupported] to check + /// if the specified WebView feature is available on the current device and WebView version. + /// + /// See [WebViewFeatureType] for available feature constants. + Future isWebViewFeatureSupported(WebViewFeatureType featureType) { + final String feature = switch (featureType) { + WebViewFeatureType.paymentRequest => + WebViewFeatureConstants.paymentRequest, + }; + return _androidWebViewParams.androidWebViewProxy + .isWebViewFeatureSupported(feature); + } + + /// Sets whether the WebView should enable the Payment Request API. + /// + /// This method uses [android_webview.WebSettingsCompat.setPaymentRequestEnabled] + /// to enable or disable the Payment Request API for the WebView. + /// + /// Before calling this method, you should check if the feature is supported using + /// [isWebViewFeatureSupported] with [WebViewFeatureType.paymentRequest]. + /// + /// This feature requires adding queries to the AndroidManifest.xml to allow WebView to query the device for the user's payment applications: + /// See https://developer.android.com/reference/androidx/webkit/WebSettingsCompat#setPaymentRequestEnabled(android.webkit.WebSettings,boolean). + Future setPaymentRequestEnabled(bool enabled) { + return _androidWebViewParams.androidWebViewProxy.setPaymentRequestEnabled( + _webView.settings, + enabled, + ); + } } /// Android implementation of [PlatformWebViewPermissionRequest]. @@ -961,6 +993,16 @@ enum MixedContentMode { neverAllow, } +/// WebView support library feature types used to query for support on the device. +/// +/// See https://developer.android.com/reference/androidx/webkit/WebViewFeature#constants_1. +enum WebViewFeatureType { + /// Feature for isFeatureSupported. + /// + /// This feature covers [WebSettingsCompat.setPaymentRequestEnabled]. + paymentRequest, +} + /// Parameters received when the `WebView` should show a file selector. @immutable class FileSelectorParams { diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webkit.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webkit.dart index e8041c59abe..9dadba738d8 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webkit.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webkit.dart @@ -1091,3 +1091,32 @@ abstract class Certificate { /// The encoded form of this certificate. Uint8List getEncoded(); } + +/// Compatibility version of `WebSettings`. +/// +/// See https://developer.android.com/reference/kotlin/androidx/webkit/WebSettingsCompat. +@ProxyApi( + kotlinOptions: KotlinProxyApiOptions( + fullClassName: 'androidx.webkit.WebSettingsCompat', + ), +) +abstract class WebSettingsCompat { + @static + void setPaymentRequestEnabled( + WebSettings webSettings, + bool enabled, + ); +} + +/// Utility class for checking which WebView Support Library features are supported on the device. +/// +/// See https://developer.android.com/reference/kotlin/androidx/webkit/WebViewFeature. +@ProxyApi( + kotlinOptions: KotlinProxyApiOptions( + fullClassName: 'androidx.webkit.WebViewFeature', + ), +) +abstract class WebViewFeature { + @static + bool isFeatureSupported(String feature); +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 3400227bbe4..444894d8100 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 4.9.1 +version: 4.10.0 environment: sdk: ^3.6.0 diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart index d3a4c634930..b7ca4ec92c8 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart @@ -89,6 +89,9 @@ void main() { android_webview.WebViewClient? mockWebViewClient, android_webview.WebStorage? mockWebStorage, android_webview.WebSettings? mockSettings, + Future Function(String)? isWebViewFeatureSupported, + Future Function(android_webview.WebSettings, bool)? + setPaymentRequestEnabled, }) { final android_webview.WebView nonNullMockWebView = mockWebView ?? MockWebView(); @@ -247,6 +250,10 @@ void main() { postMessage, }) => mockJavaScriptChannel ?? MockJavaScriptChannel(), + isWebViewFeatureSupported: + isWebViewFeatureSupported ?? (_) async => false, + setPaymentRequestEnabled: + setPaymentRequestEnabled ?? (_, __) async {}, )); when(nonNullMockWebView.settings) @@ -1863,6 +1870,48 @@ void main() { expect(controller.webViewIdentifier, 0); }); + test('isWebViewFeatureSupported', () async { + String? captured; + const bool expectedIsWebViewFeatureEnabled = true; + + final AndroidWebViewController controller = createControllerWithMocks( + isWebViewFeatureSupported: (String feature) async { + captured = feature; + return expectedIsWebViewFeatureEnabled; + }, + ); + + final bool result = await controller.isWebViewFeatureSupported( + WebViewFeatureType.paymentRequest, + ); + + expect(WebViewFeatureConstants.paymentRequest, captured); + expect(expectedIsWebViewFeatureEnabled, result); + }); + + test('setPaymentRequestEnabled', () async { + android_webview.WebSettings? capturedSettings; + bool? capturedEnabled; + const bool expectedEnabled = true; + + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockSettings, + setPaymentRequestEnabled: + (android_webview.WebSettings settings, bool enabled) async { + capturedSettings = settings; + capturedEnabled = enabled; + }, + ); + + await controller.setPaymentRequestEnabled(expectedEnabled); + + expect(mockSettings, capturedSettings); + expect(expectedEnabled, capturedEnabled); + }); + group('AndroidWebViewWidget', () { testWidgets('Builds Android view using supplied parameters', (WidgetTester tester) async { diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart index a786a9394c9..44b94ecd49f 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart @@ -1018,6 +1018,29 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i8.Future.value(), returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); + + @override + _i8.Future isWebViewFeatureSupported( + _i7.WebViewFeatureType? featureType) => + (super.noSuchMethod( + Invocation.method( + #isWebViewFeatureSupported, + [featureType], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + + @override + _i8.Future setPaymentRequestEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method( + #setPaymentRequestEnabled, + [enabled], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); } /// A class which mocks [AndroidWebViewProxy]. @@ -1829,6 +1852,36 @@ class MockAndroidWebViewProxy extends _i1.Mock Invocation.getter(#instanceWebStorage), ), ) as _i2.WebStorage Function()); + + @override + _i8.Future Function(String) get isWebViewFeatureSupported => + (super.noSuchMethod( + Invocation.getter(#isWebViewFeatureSupported), + returnValue: (String __p0) => _i8.Future.value(false), + returnValueForMissingStub: (String __p0) => + _i8.Future.value(false), + ) as _i8.Future Function(String)); + + @override + _i8.Future Function( + _i2.WebSettings, + bool, + ) get setPaymentRequestEnabled => (super.noSuchMethod( + Invocation.getter(#setPaymentRequestEnabled), + returnValue: ( + _i2.WebSettings __p0, + bool __p1, + ) => + _i8.Future.value(), + returnValueForMissingStub: ( + _i2.WebSettings __p0, + bool __p1, + ) => + _i8.Future.value(), + ) as _i8.Future Function( + _i2.WebSettings, + bool, + )); } /// A class which mocks [AndroidWebViewWidgetCreationParams]. diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart index b9e807b88ad..c305241cbf6 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart @@ -709,4 +709,26 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i5.Future.value(), returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); + + @override + _i5.Future isWebViewFeatureSupported( + _i6.WebViewFeatureType? featureType) => + (super.noSuchMethod( + Invocation.method( + #isWebViewFeatureSupported, + [featureType], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future setPaymentRequestEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method( + #setPaymentRequestEnabled, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); }