Skip to content

[webview_flutter] Add support for payment requests on Android #9679

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2d07746
Add wrapper for androidx.webkit.WebSettingsCompat
mataku Jul 26, 2025
1a01f13
Add wrapper for androidx.webkit.WebViewFeature
mataku Jul 26, 2025
d098b47
Expose wrappers
mataku Jul 26, 2025
8fa5fe1
Add native unit tests for payment request feature
mataku Jul 26, 2025
381ae6f
Add sample menu for payment request
mataku Jul 26, 2025
5eee7b3
Prepare CHANGELOG
mataku Jul 26, 2025
d43bde1
Run auto-formatter
mataku Jul 26, 2025
a20f0fb
Simplify method description
mataku Jul 26, 2025
103e29f
Specify details with added methods
mataku Jul 26, 2025
8c73517
Fix RequiresFeature lint: setPaymentRequestEnabled should only be cal…
mataku Jul 26, 2025
5ed43ab
Fix format
mataku Jul 26, 2025
28f8889
Fix doc comments to correspond to the method
mataku Aug 1, 2025
3f68c0e
Remove sample for WebSettingsCompat and WebViewFeature.
mataku Aug 1, 2025
878fd38
Update generated files according to comment update
mataku Aug 1, 2025
b0388bc
Specify collect type
mataku Aug 2, 2025
5f7f689
Merge main into feature/expose-payment-request-enabled
mataku Aug 2, 2025
ddea6d4
Client should use setPaymentRequestEnabled if only WebViewFeatureProx…
mataku Aug 8, 2025
2b06e3d
Add Payment Request section for webview_flutter_android
mataku Aug 8, 2025
69c8a3e
Merge branch upstream/main into feature/expose-payment-request-enabled
mataku Aug 8, 2025
7337199
Fix tests according to SuppressLint
mataku Aug 8, 2025
47b6b6f
Address code-excerpt
mataku Aug 8, 2025
1cb2a20
Run update-excerpts
mataku Aug 8, 2025
5575248
Minor README wording changes
stuartmorgan-g Aug 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
31 changes: 31 additions & 0 deletions packages/webview_flutter/webview_flutter_android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

<?code-excerpt "example/lib/main.dart (payment_request_example)"?>
```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
<queries>
<intent>
<action android:name="org.chromium.intent.action.PAY"/>
</intent>
<intent>
<action android:name="org.chromium.intent.action.IS_READY_TO_PAY"/>
</intent>
<intent>
<action android:name="org.chromium.intent.action.UPDATE_PAYMENT_DETAILS"/>
</intent>
</queries>
```

## Fullscreen Video

To display a video as fullscreen, an app must manually handle the notification that the current page
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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() {
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Any?>(
binaryMessenger,
"dev.flutter.pigeon.webview_flutter_android.WebSettingsCompat.setPaymentRequestEnabled",
codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val webSettingsArg = args[0] as android.webkit.WebSettings
val enabledArg = args[1] as Boolean
val wrapped: List<Any?> =
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>) -> 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<Any?>(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<Any?>(
binaryMessenger,
"dev.flutter.pigeon.webview_flutter_android.WebViewFeature.isFeatureSupported",
codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val featureArg = args[0] as String
val wrapped: List<Any?> =
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>) -> 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<Any?>(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)))
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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);
}
}
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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);
}
}
Original file line number Diff line number Diff line change
@@ -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<WebSettingsCompat> mockedStatic = mockStatic(WebSettingsCompat.class)) {
try (MockedStatic<WebViewFeature> 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());
}
}
}
Loading