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 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
30 changes: 30 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,36 @@ Java:
import io.flutter.plugins.webviewflutter.WebViewFlutterAndroidExternalApi;
```

## Enable Payment Request in WebView

Payment Request feature can be enabled by `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