Skip to content

Commit f53df16

Browse files
authored
[webview_flutter] Add support for payment requests on Android (#9679)
Expose android native [WebViewFeature#isFeatureSupported](https://developer.android.com/reference/androidx/webkit/WebViewFeature#isFeatureSupported(java.lang.String)), [WebSettingsCompat#setPaymentRequestEnabled](https://developer.android.com/reference/kotlin/androidx/webkit/WebSettingsCompat#setPaymentRequestEnabled(android.webkit.WebSettings,boolean)) and [WebViewFeature#PAYMENT_REQUEST](https://developer.android.com/reference/androidx/webkit/WebViewFeature#PAYMENT_REQUEST()) to allow developers to check if payment request feature is supported in WebView. This change is for Google Pay integration in Android WebView. Fixes flutter/flutter#172150 ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 5b08378 commit f53df16

18 files changed

+840
-1
lines changed

packages/webview_flutter/webview_flutter_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 4.10.0
2+
3+
* Adds support for the Payment Request API with `AndroidWebViewController.isWebViewFeatureSupported` and `AndroidWebViewController.setPaymentRequestEnabled`.
4+
15
## 4.9.1
26

37
* Updates kotlin version to 2.2.0 to enable gradle 8.11 support.

packages/webview_flutter/webview_flutter_android/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,37 @@ Java:
5252
import io.flutter.plugins.webviewflutter.WebViewFlutterAndroidExternalApi;
5353
```
5454

55+
## Enable Payment Request in WebView
56+
57+
The Payment Request API can be enabled by calling `AndroidWebViewController.setPaymentRequestEnabled` after
58+
checking `AndroidWebViewController.isWebViewFeatureSupported`.
59+
60+
<?code-excerpt "example/lib/readme_excerpts.dart (payment_request_example)"?>
61+
```dart
62+
final bool paymentRequestEnabled = await androidController
63+
.isWebViewFeatureSupported(WebViewFeatureType.paymentRequest);
64+
65+
if (paymentRequestEnabled) {
66+
await androidController.setPaymentRequestEnabled(true);
67+
}
68+
```
69+
70+
Add intent filters to your AndroidManifest.xml to discover and invoke Android payment apps using system intents:
71+
72+
```xml
73+
<queries>
74+
<intent>
75+
<action android:name="org.chromium.intent.action.PAY"/>
76+
</intent>
77+
<intent>
78+
<action android:name="org.chromium.intent.action.IS_READY_TO_PAY"/>
79+
</intent>
80+
<intent>
81+
<action android:name="org.chromium.intent.action.UPDATE_PAYMENT_DETAILS"/>
82+
</intent>
83+
</queries>
84+
```
85+
5586
## Fullscreen Video
5687

5788
To display a video as fullscreen, an app must manually handle the notification that the current page

packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/AndroidWebkitLibrary.g.kt

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,18 @@ abstract class AndroidWebkitLibraryPigeonProxyApiRegistrar(val binaryMessenger:
567567
*/
568568
abstract fun getPigeonApiCertificate(): PigeonApiCertificate
569569

570+
/**
571+
* An implementation of [PigeonApiWebSettingsCompat] used to add a new Dart instance of
572+
* `WebSettingsCompat` to the Dart `InstanceManager`.
573+
*/
574+
abstract fun getPigeonApiWebSettingsCompat(): PigeonApiWebSettingsCompat
575+
576+
/**
577+
* An implementation of [PigeonApiWebViewFeature] used to add a new Dart instance of
578+
* `WebViewFeature` to the Dart `InstanceManager`.
579+
*/
580+
abstract fun getPigeonApiWebViewFeature(): PigeonApiWebViewFeature
581+
570582
fun setUp() {
571583
AndroidWebkitLibraryPigeonInstanceManagerApi.setUpMessageHandlers(
572584
binaryMessenger, instanceManager)
@@ -598,6 +610,9 @@ abstract class AndroidWebkitLibraryPigeonProxyApiRegistrar(val binaryMessenger:
598610
binaryMessenger, getPigeonApiSslCertificateDName())
599611
PigeonApiSslCertificate.setUpMessageHandlers(binaryMessenger, getPigeonApiSslCertificate())
600612
PigeonApiCertificate.setUpMessageHandlers(binaryMessenger, getPigeonApiCertificate())
613+
PigeonApiWebSettingsCompat.setUpMessageHandlers(
614+
binaryMessenger, getPigeonApiWebSettingsCompat())
615+
PigeonApiWebViewFeature.setUpMessageHandlers(binaryMessenger, getPigeonApiWebViewFeature())
601616
}
602617

603618
fun tearDown() {
@@ -623,6 +638,8 @@ abstract class AndroidWebkitLibraryPigeonProxyApiRegistrar(val binaryMessenger:
623638
PigeonApiSslCertificateDName.setUpMessageHandlers(binaryMessenger, null)
624639
PigeonApiSslCertificate.setUpMessageHandlers(binaryMessenger, null)
625640
PigeonApiCertificate.setUpMessageHandlers(binaryMessenger, null)
641+
PigeonApiWebSettingsCompat.setUpMessageHandlers(binaryMessenger, null)
642+
PigeonApiWebViewFeature.setUpMessageHandlers(binaryMessenger, null)
626643
}
627644
}
628645

@@ -727,6 +744,10 @@ private class AndroidWebkitLibraryPigeonProxyApiBaseCodec(
727744
registrar.getPigeonApiSslCertificate().pigeon_newInstance(value) {}
728745
} else if (value is java.security.cert.Certificate) {
729746
registrar.getPigeonApiCertificate().pigeon_newInstance(value) {}
747+
} else if (value is androidx.webkit.WebSettingsCompat) {
748+
registrar.getPigeonApiWebSettingsCompat().pigeon_newInstance(value) {}
749+
} else if (value is androidx.webkit.WebViewFeature) {
750+
registrar.getPigeonApiWebViewFeature().pigeon_newInstance(value) {}
730751
}
731752

732753
when {
@@ -6310,3 +6331,159 @@ abstract class PigeonApiCertificate(
63106331
}
63116332
}
63126333
}
6334+
/**
6335+
* Compatibility version of `WebSettings`.
6336+
*
6337+
* See https://developer.android.com/reference/kotlin/androidx/webkit/WebSettingsCompat.
6338+
*/
6339+
@Suppress("UNCHECKED_CAST")
6340+
abstract class PigeonApiWebSettingsCompat(
6341+
open val pigeonRegistrar: AndroidWebkitLibraryPigeonProxyApiRegistrar
6342+
) {
6343+
abstract fun setPaymentRequestEnabled(webSettings: android.webkit.WebSettings, enabled: Boolean)
6344+
6345+
companion object {
6346+
@Suppress("LocalVariableName")
6347+
fun setUpMessageHandlers(binaryMessenger: BinaryMessenger, api: PigeonApiWebSettingsCompat?) {
6348+
val codec = api?.pigeonRegistrar?.codec ?: AndroidWebkitLibraryPigeonCodec()
6349+
run {
6350+
val channel =
6351+
BasicMessageChannel<Any?>(
6352+
binaryMessenger,
6353+
"dev.flutter.pigeon.webview_flutter_android.WebSettingsCompat.setPaymentRequestEnabled",
6354+
codec)
6355+
if (api != null) {
6356+
channel.setMessageHandler { message, reply ->
6357+
val args = message as List<Any?>
6358+
val webSettingsArg = args[0] as android.webkit.WebSettings
6359+
val enabledArg = args[1] as Boolean
6360+
val wrapped: List<Any?> =
6361+
try {
6362+
api.setPaymentRequestEnabled(webSettingsArg, enabledArg)
6363+
listOf(null)
6364+
} catch (exception: Throwable) {
6365+
AndroidWebkitLibraryPigeonUtils.wrapError(exception)
6366+
}
6367+
reply.reply(wrapped)
6368+
}
6369+
} else {
6370+
channel.setMessageHandler(null)
6371+
}
6372+
}
6373+
}
6374+
}
6375+
6376+
@Suppress("LocalVariableName", "FunctionName")
6377+
/** Creates a Dart instance of WebSettingsCompat and attaches it to [pigeon_instanceArg]. */
6378+
fun pigeon_newInstance(
6379+
pigeon_instanceArg: androidx.webkit.WebSettingsCompat,
6380+
callback: (Result<Unit>) -> Unit
6381+
) {
6382+
if (pigeonRegistrar.ignoreCallsToDart) {
6383+
callback(
6384+
Result.failure(
6385+
AndroidWebKitError("ignore-calls-error", "Calls to Dart are being ignored.", "")))
6386+
} else if (pigeonRegistrar.instanceManager.containsInstance(pigeon_instanceArg)) {
6387+
callback(Result.success(Unit))
6388+
} else {
6389+
val pigeon_identifierArg =
6390+
pigeonRegistrar.instanceManager.addHostCreatedInstance(pigeon_instanceArg)
6391+
val binaryMessenger = pigeonRegistrar.binaryMessenger
6392+
val codec = pigeonRegistrar.codec
6393+
val channelName =
6394+
"dev.flutter.pigeon.webview_flutter_android.WebSettingsCompat.pigeon_newInstance"
6395+
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
6396+
channel.send(listOf(pigeon_identifierArg)) {
6397+
if (it is List<*>) {
6398+
if (it.size > 1) {
6399+
callback(
6400+
Result.failure(
6401+
AndroidWebKitError(it[0] as String, it[1] as String, it[2] as String?)))
6402+
} else {
6403+
callback(Result.success(Unit))
6404+
}
6405+
} else {
6406+
callback(
6407+
Result.failure(AndroidWebkitLibraryPigeonUtils.createConnectionError(channelName)))
6408+
}
6409+
}
6410+
}
6411+
}
6412+
}
6413+
/**
6414+
* Utility class for checking which WebView Support Library features are supported on the device.
6415+
*
6416+
* See https://developer.android.com/reference/kotlin/androidx/webkit/WebViewFeature.
6417+
*/
6418+
@Suppress("UNCHECKED_CAST")
6419+
abstract class PigeonApiWebViewFeature(
6420+
open val pigeonRegistrar: AndroidWebkitLibraryPigeonProxyApiRegistrar
6421+
) {
6422+
abstract fun isFeatureSupported(feature: String): Boolean
6423+
6424+
companion object {
6425+
@Suppress("LocalVariableName")
6426+
fun setUpMessageHandlers(binaryMessenger: BinaryMessenger, api: PigeonApiWebViewFeature?) {
6427+
val codec = api?.pigeonRegistrar?.codec ?: AndroidWebkitLibraryPigeonCodec()
6428+
run {
6429+
val channel =
6430+
BasicMessageChannel<Any?>(
6431+
binaryMessenger,
6432+
"dev.flutter.pigeon.webview_flutter_android.WebViewFeature.isFeatureSupported",
6433+
codec)
6434+
if (api != null) {
6435+
channel.setMessageHandler { message, reply ->
6436+
val args = message as List<Any?>
6437+
val featureArg = args[0] as String
6438+
val wrapped: List<Any?> =
6439+
try {
6440+
listOf(api.isFeatureSupported(featureArg))
6441+
} catch (exception: Throwable) {
6442+
AndroidWebkitLibraryPigeonUtils.wrapError(exception)
6443+
}
6444+
reply.reply(wrapped)
6445+
}
6446+
} else {
6447+
channel.setMessageHandler(null)
6448+
}
6449+
}
6450+
}
6451+
}
6452+
6453+
@Suppress("LocalVariableName", "FunctionName")
6454+
/** Creates a Dart instance of WebViewFeature and attaches it to [pigeon_instanceArg]. */
6455+
fun pigeon_newInstance(
6456+
pigeon_instanceArg: androidx.webkit.WebViewFeature,
6457+
callback: (Result<Unit>) -> Unit
6458+
) {
6459+
if (pigeonRegistrar.ignoreCallsToDart) {
6460+
callback(
6461+
Result.failure(
6462+
AndroidWebKitError("ignore-calls-error", "Calls to Dart are being ignored.", "")))
6463+
} else if (pigeonRegistrar.instanceManager.containsInstance(pigeon_instanceArg)) {
6464+
callback(Result.success(Unit))
6465+
} else {
6466+
val pigeon_identifierArg =
6467+
pigeonRegistrar.instanceManager.addHostCreatedInstance(pigeon_instanceArg)
6468+
val binaryMessenger = pigeonRegistrar.binaryMessenger
6469+
val codec = pigeonRegistrar.codec
6470+
val channelName =
6471+
"dev.flutter.pigeon.webview_flutter_android.WebViewFeature.pigeon_newInstance"
6472+
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
6473+
channel.send(listOf(pigeon_identifierArg)) {
6474+
if (it is List<*>) {
6475+
if (it.size > 1) {
6476+
callback(
6477+
Result.failure(
6478+
AndroidWebKitError(it[0] as String, it[1] as String, it[2] as String?)))
6479+
} else {
6480+
callback(Result.success(Unit))
6481+
}
6482+
} else {
6483+
callback(
6484+
Result.failure(AndroidWebkitLibraryPigeonUtils.createConnectionError(channelName)))
6485+
}
6486+
}
6487+
}
6488+
}
6489+
}

packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ProxyApiRegistrar.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,16 @@ public void setContext(@NonNull Context context) {
251251
public FlutterAssetManager getFlutterAssetManager() {
252252
return flutterAssetManager;
253253
}
254+
255+
@NonNull
256+
@Override
257+
public PigeonApiWebViewFeature getPigeonApiWebViewFeature() {
258+
return new WebViewFeatureProxyApi(this);
259+
}
260+
261+
@NonNull
262+
@Override
263+
public PigeonApiWebSettingsCompat getPigeonApiWebSettingsCompat() {
264+
return new WebSettingsCompatProxyApi(this);
265+
}
254266
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.webviewflutter;
6+
7+
import android.annotation.SuppressLint;
8+
import android.webkit.WebSettings;
9+
import androidx.annotation.NonNull;
10+
import androidx.webkit.WebSettingsCompat;
11+
12+
/**
13+
* Proxy API implementation for {@link WebSettingsCompat}.
14+
*
15+
* <p>This class may handle instantiating and adding native object instances that are attached to a
16+
* Dart instance or handle method calls on the associated native class or an instance of the class.
17+
*/
18+
public class WebSettingsCompatProxyApi extends PigeonApiWebSettingsCompat {
19+
public WebSettingsCompatProxyApi(@NonNull ProxyApiRegistrar pigeonRegistrar) {
20+
super(pigeonRegistrar);
21+
}
22+
23+
/**
24+
* This method should only be called if {@link WebViewFeatureProxyApi#isFeatureSupported(String)}
25+
* with PAYMENT_REQUEST returns true.
26+
*/
27+
@SuppressLint("RequiresFeature")
28+
@Override
29+
public void setPaymentRequestEnabled(@NonNull WebSettings webSettings, boolean enabled) {
30+
WebSettingsCompat.setPaymentRequestEnabled(webSettings, enabled);
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.webviewflutter;
6+
7+
import androidx.annotation.NonNull;
8+
import androidx.webkit.WebViewFeature;
9+
10+
/**
11+
* Proxy API implementation for {@link WebViewFeature}.
12+
*
13+
* <p>This class may handle instantiating and adding native object instances that are attached to a
14+
* Dart instance or handle method calls on the associated native class or an instance of the class.
15+
*/
16+
public class WebViewFeatureProxyApi extends PigeonApiWebViewFeature {
17+
public WebViewFeatureProxyApi(@NonNull ProxyApiRegistrar pigeonRegistrar) {
18+
super(pigeonRegistrar);
19+
}
20+
21+
@Override
22+
public boolean isFeatureSupported(@NonNull String feature) {
23+
return WebViewFeature.isFeatureSupported(feature);
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.webviewflutter;
6+
7+
import static org.junit.Assert.fail;
8+
import static org.mockito.Mockito.mock;
9+
import static org.mockito.Mockito.mockStatic;
10+
11+
import android.webkit.WebSettings;
12+
import androidx.webkit.WebSettingsCompat;
13+
import androidx.webkit.WebViewFeature;
14+
import org.junit.Test;
15+
import org.mockito.MockedStatic;
16+
17+
public class WebSettingsCompatTest {
18+
@Test
19+
public void setPaymentRequestEnabled() {
20+
final PigeonApiWebSettingsCompat api =
21+
new TestProxyApiRegistrar().getPigeonApiWebSettingsCompat();
22+
23+
final WebSettings webSettings = mock(WebSettings.class);
24+
25+
try (MockedStatic<WebSettingsCompat> mockedStatic = mockStatic(WebSettingsCompat.class)) {
26+
try (MockedStatic<WebViewFeature> mockedWebViewFeature = mockStatic(WebViewFeature.class)) {
27+
mockedWebViewFeature
28+
.when(() -> WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST))
29+
.thenReturn(true);
30+
api.setPaymentRequestEnabled(webSettings, true);
31+
mockedStatic.verify(() -> WebSettingsCompat.setPaymentRequestEnabled(webSettings, true));
32+
} catch (Exception e) {
33+
fail(e.toString());
34+
}
35+
} catch (Exception e) {
36+
fail(e.toString());
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)