Android 付款应用开发者指南

了解如何调整 Android 支付应用以与 Web Payments 搭配使用,并为客户提供更好的用户体验。

发布时间:2020 年 5 月 5 日,上次更新时间:2025 年 5 月 27 日

Payment Request API 为 Web 带来了一个内置的基于浏览器的界面,让用户可以比以往更轻松地输入所需的支付 信息。该 API 还可以调用特定于平台的支付应用。

Browser Support

  • Chrome: 60.
  • Edge: 15.
  • Firefox: behind a flag.
  • Safari: 11.1.

Source

使用 Web Payments 的特定于平台的 Google Pay 应用的结账流程。

与仅使用 Android intent 相比,Web Payments 可以更好地与浏览器、安全性和用户体验集成:

  • 支付应用以模态形式在商家网站的上下文中启动。
  • 实现是对现有支付应用的补充,让您可以充分利用用户群。
  • 系统会检查支付应用的签名,以防止 旁加载
  • 支付应用可以支持多种支付方式。
  • 您可以集成任何支付方式,例如加密货币、银行转账等。Android 设备上的支付应用甚至可以集成需要访问设备上的硬件芯片的方法。

在 Android 支付应用中实现 Web Payments 需要执行四个步骤:

  1. 让商家发现您的支付应用。
  2. 让商家知道客户是否有已注册的支付工具(例如信用卡)可用于支付。
  3. 让客户进行支付。
  4. 验证调用方的签名证书。

如需查看 Web Payments 的实际应用,请查看 android-web-payment 演示。

第 1 步:让商家发现您的支付应用

按照 设置支付方式中的说明,在 Web 应用清单中设置 related_applications 属性。

为了让商家使用您的支付应用,他们需要使用 Payment Request API 并 使用 payment method identifier 指定您支持的支付方式。

如果您有支付应用独有的支付方式标识符,则可以设置自己的支付方式 清单,以便浏览器可以 发现您的应用。

第 2 步:让商家知道客户是否有已注册的支付工具可用于支付

商家可以调用 hasEnrolledInstrument()查询客户 是否能够进行支付。您可以将 IS_READY_TO_PAY 实现为 Android 服务,以回答此查询。

AndroidManifest.xml

使用具有 org.chromium.intent.action.IS_READY_TO_PAY 操作的 intent 过滤器声明您的服务。

<service
  android:name=".SampleIsReadyToPayService"
  android:exported="true">
  <intent-filter>
    <action android:name="org.chromium.intent.action.IS_READY_TO_PAY" />
  </intent-filter>
</service>

IS_READY_TO_PAY 服务是可选的。如果支付应用中没有此类 intent 处理程序,则 Web 浏览器会假定该应用始终可以进行支付。

AIDL

IS_READY_TO_PAY 服务的 API 在 AIDL 中定义。创建两个包含以下内容的 AIDL 文件:

org/chromium/IsReadyToPayServiceCallback.aidl

package org.chromium;

interface IsReadyToPayServiceCallback {
    oneway void handleIsReadyToPay(boolean isReadyToPay);
}

org/chromium/IsReadyToPayService.aidl

package org.chromium;

import org.chromium.IsReadyToPayServiceCallback;

interface IsReadyToPayService {
    oneway void isReadyToPay(IsReadyToPayServiceCallback callback, in Bundle parameters);
}

实现 IsReadyToPayService

以下示例展示了 IsReadyToPayService 的最简单实现:

Kotlin

class SampleIsReadyToPayService : Service() {
    private val binder = object : IsReadyToPayService.Stub() {
        override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
            callback?.handleIsReadyToPay(true)
        }
    }

    override fun onBind(intent: Intent?): IBinder? {
        return binder
    }
}

Java

import org.chromium.IsReadyToPayService;

public class SampleIsReadyToPayService extends Service {
    private final IsReadyToPayService.Stub mBinder =
        new IsReadyToPayService.Stub() {
            @Override
            public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
                if (callback != null) {
                    callback.handleIsReadyToPay(true);
                }
            }
        };

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

响应

该服务可以使用 handleIsReadyToPay(Boolean) 方法发送其响应。

Kotlin

callback?.handleIsReadyToPay(true)

Java

if (callback != null) {
    callback.handleIsReadyToPay(true);
}

权限

您可以使用 Binder.getCallingUid() 检查调用方是谁。请注意,您 必须在 isReadyToPay 方法中执行此操作,而不是在 onBind 方法中执行此操作, 因为 Android OS 可以缓存并重复使用服务连接,而这不会 触发 onBind() 方法。

Kotlin

override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
    try {
        val untrustedPackageName = parameters?.getString("packageName")
        val actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid())
        // ...

Java

@Override
public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
    try {
        String untrustedPackageName = parameters != null
                ? parameters.getString("packageName")
                : null;
        String[] actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid());
        // ...

在接收进程间通信 (IPC) 调用时,请务必检查输入参数是否为 null。这一点尤为重要,因为不同版本或分支的 Android OS 可能会以意外方式运行,如果不加以处理,可能会导致错误。

虽然 packageManager.getPackagesForUid() 通常会返回单个元素,但您的代码必须处理调用方使用多个软件包名称的罕见情况。这可确保您的应用保持稳健。

如需了解如何验证调用软件包是否具有正确的签名,请参阅验证调用方的签名证书关于如何

参数

parameters Bundle 已在 Chrome 139 中添加。应始终针对 null 进行检查。

以下参数在 parameters Bundle 中传递给服务:

  • packageName
  • methodNames
  • methodData
  • topLevelOrigin
  • paymentRequestOrigin
  • topLevelCertificateChain

packageName 已在 Chrome 138 中添加。您必须先针对 Binder.getCallingUid() 验证此参数,然后才能使用其值。此验证至关重要,因为 parameters Bundle 完全由调用方控制,而 Binder.getCallingUid() 由 Android OS 控制。

topLevelCertificateChain 在 WebView 和通常用于本地测试的非 https 网站(例如 http://localhost)中为 null

第 3 步:让客户进行支付

商家调用 show()启动支付 应用 ,以便客户可以进行支付。支付应用使用 Android intent PAY 调用,intent 参数中包含交易信息。

支付应用会响应 methodNamedetails,这些内容特定于支付应用,并且对浏览器是不透明的。浏览器使用 JSON 字符串反序列化将 details 字符串转换为 JavaScript 字典供商家使用,但除此之外,不会强制执行任何有效性。浏览器不会修改 details;该参数的值会直接传递给商家。

AndroidManifest.xml

具有 PAY intent 过滤器的 activity 应具有一个 <meta-data> 标记 用于 标识应用的默认支付方式标识符

如需支持多种支付方式,请添加一个带有 <string-array>资源的<meta-data>标记。

<activity
  android:name=".PaymentActivity"
  android:theme="@style/Theme.SamplePay.Dialog">
  <intent-filter>
    <action android:name="org.chromium.intent.action.PAY" />
  </intent-filter>

  <meta-data
    android:name="org.chromium.default_payment_method_name"
    android:value="https://bobbucks.dev/pay" />
  <meta-data
    android:name="org.chromium.payment_method_names"
    android:resource="@array/chromium_payment_method_names" />
</activity>

android:resource 必须是字符串列表,每个字符串都必须是有效的绝对网址,并且采用 HTTPS 方案,如下所示。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="chromium_payment_method_names">
        <item>https://alicepay.com/put/optional/path/here</item>
        <item>https://charliepay.com/put/optional/path/here</item>
    </string-array>
</resources>

参数

以下参数作为 Intent extra 传递给 activity:

  • methodNames
  • methodData
  • merchantName
  • topLevelOrigin
  • topLevelCertificateChain
  • paymentRequestOrigin
  • total
  • modifiers
  • paymentRequestId
  • paymentOptions
  • shippingOptions

Kotlin

val extras: Bundle? = getIntent()?.extras

Java

Bundle extras = getIntent() != null ? getIntent().getExtras() : null;

methodNames

正在使用的方法的名称。这些元素是 methodData 字典中的键。这些是支付应用支持的方法。

Kotlin

val methodNames: List<String>? = extras.getStringArrayList("methodNames")

Java

List<String> methodNames = extras.getStringArrayList("methodNames");

methodData

从每个 methodNamesmethodData 的映射。

Kotlin

val methodData: Bundle? = extras.getBundle("methodData")

Java

Bundle methodData = extras.getBundle("methodData");

merchantName

商家结账页面的 <title> HTML 标记的内容( 浏览器的顶级浏览上下文)。

Kotlin

val merchantName: String? = extras.getString("merchantName")

Java

String merchantName = extras.getString("merchantName");

topLevelOrigin

商家的来源,不包含方案(顶级浏览上下文的无方案来源)。例如,https://mystore.com/checkout 会作为 mystore.com 传递。

Kotlin

val topLevelOrigin: String? = extras.getString("topLevelOrigin")

Java

String topLevelOrigin = extras.getString("topLevelOrigin");

topLevelCertificateChain

商家的证书链(顶级浏览上下文的证书链)。对于 WebView、localhost 或磁盘上的文件,该值为 null。 每个 Parcelable 都是一个 Bundle,其中包含 certificate 键和字节数组值。

Kotlin

val topLevelCertificateChain: Array<Parcelable>? =
        extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
    (p as Bundle).getByteArray("certificate")
}

Java

Parcelable[] topLevelCertificateChain =
        extras.getParcelableArray("topLevelCertificateChain");
if (topLevelCertificateChain != null) {
    for (Parcelable p : topLevelCertificateChain) {
        if (p != null && p instanceof Bundle) {
            ((Bundle) p).getByteArray("certificate");
        }
    }
}

paymentRequestOrigin

调用 JavaScript 中的 new PaymentRequest(methodData, details, options) 构造函数的 iframe 浏览上下文的无方案来源。如果构造函数是从顶级上下文调用的,则此参数的值等于 topLevelOrigin 参数的值。

Kotlin

val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")

Java

String paymentRequestOrigin = extras.getString("paymentRequestOrigin");

total

表示交易总金额的 JSON 字符串。

Kotlin

val total: String? = extras.getString("total")

Java

String total = extras.getString("total");

以下是字符串内容的示例:

{"currency":"USD","value":"25.00"}

modifiers

JSON.stringify(details.modifiers) 的输出,其中 details.modifiers 仅包含 supportedMethodsdatatotal

paymentRequestId

“推送支付”应用应与交易状态关联的 PaymentRequest.id 字段。商家网站将使用此字段来查询“推送支付”应用的带外交易状态。

Kotlin

val paymentRequestId: String? = extras.getString("paymentRequestId")

Java

String paymentRequestId = extras.getString("paymentRequestId");

响应

activity 可以通过 setResultRESULT_OK 将响应发回。

Kotlin

setResult(Activity.RESULT_OK, Intent().apply {
    putExtra("methodName", "https://bobbucks.dev/pay")
    putExtra("details", "{\"token\": \"put-some-data-here\"}")
})
finish()

Java

Intent result = new Intent();
Bundle extras = new Bundle();
extras.putString("methodName", "https://bobbucks.dev/pay");
extras.putString("details", "{\"token\": \"put-some-data-here\"}");
result.putExtras(extras);
setResult(Activity.RESULT_OK, result);
finish();

您必须将两个参数指定为 intent extra:

  • methodName:正在使用的方法的名称。
  • details:包含商家完成交易所需信息的 JSON 字符串。如果成功为 true,则 details 必须以 JSON.parse(details) 将成功的方式 构建。如果没有 需要返回的数据,则此字符串可以是 "{}",商家网站将收到一个空的 JavaScript 字典。

如果用户在支付应用中取消交易,您可以传递 RESULT_CANCELED。这样做会导致 request.show() 在商家网站上拒绝并显示 AbortError,表明用户已取消。

Kotlin

setResult(Activity.RESULT_CANCELED)
finish()

Java

setResult(Activity.RESULT_CANCELED);
finish();

从 Chrome 149 开始,支持以下结果值:

Kotlin

Activity.RESULT_CANCELED // 0 (0x00000000)
Activity.RESULT_OK // -1 (0xffffffff)
const val INTERNAL_PAYMENT_APP_ERROR = Activity.RESULT_FIRST_USER // 1 (0x00000001)

Java

Activity.RESULT_CANCELED // 0 (0x00000000)
Activity.RESULT_OK // -1 (0xffffffff)
static final int INTERNAL_PAYMENT_APP_ERROR = Activity.RESULT_FIRST_USER; // 1 (0x00000001)

如果支付应用因内部错误而失败,您可以通过传递 Activity.RESULT_FIRST_USER 作为结果代码来指明。

如果返回 INTERNAL_PAYMENT_APP_ERRORrequest.show() 将在商家网站上拒绝并显示 OperationError,表明支付应用中存在错误。

用户取消时使用 RESULT_CANCELED (0)(导致 AbortError)与应用内部错误时使用 INTERNAL_PAYMENT_APP_ERROR (1)(导致 OperationError)之间的区别,让商家可以构建更好的用户流程。

Kotlin

setResult(Activity.RESULT_FIRST_USER)
finish()

Java

setResult(Activity.RESULT_FIRST_USER);
finish();

如果从调用的支付应用收到的支付响应的 activity 结果设置为 RESULT_OK,则 Chrome 会在其 extra 中检查非空 methodNamedetails。如果验证失败,Chrome 将从 request.show() 返回拒绝的 Promise,并显示以下面向开发者的错误消息之一:

'Payment app returned invalid response. Missing field "details".'
'Payment app returned invalid response. Missing field "methodName".'

权限

activity 可以使用其 getCallingPackage() 方法检查调用方。

Kotlin

val caller: String? = callingPackage

Java

String caller = getCallingPackage();

最后一步是验证调用方的签名证书,以确认调用软件包具有正确的签名。

第 4 步:验证调用方的签名证书

您可以使用 Binder.getCallingUid()IS_READY_TO_PAY 中检查调用方的软件包名称,并使用 Activity.getCallingPackage()PAY 中检查。为了实际验证调用方是您预期的浏览器,您应检查其签名证书并确保其与正确的值匹配。

如果您以 API 级别 28 及更高级别为目标平台,并且与具有单个签名证书的浏览器集成,则可以使用 PackageManager.hasSigningCertificate()

Kotlin

val packageName: String =  // The caller's package name
val certificate: ByteArray =  // The correct signing certificate
val verified = packageManager.hasSigningCertificate(
        callingPackage,
        certificate,
        PackageManager.CERT_INPUT_SHA256
)

Java

String packageName =  // The caller's package name
byte[] certificate =  // The correct signing certificate
boolean verified = packageManager.hasSigningCertificate(
        callingPackage,
        certificate,
        PackageManager.CERT_INPUT_SHA256);

对于单个证书浏览器,最好使用 PackageManager.hasSigningCertificate(),因为它能正确处理证书轮替。(Chrome 具有单个签名证书。) 具有多个签名证书的应用无法轮替这些证书。

如果您需要支持 API 级别 27 及更低级别,或者需要处理具有多个签名证书的浏览器,则可以使用 PackageManager.GET_SIGNATURES

Kotlin

val packageName: String =  // The caller's package name
val expected: Set<String> =  // The correct set of signing certificates

val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val sha256 = MessageDigest.getInstance("SHA-256")
val actual = packageInfo.signatures.map {
    SerializeByteArrayToString(sha256.digest(it.toByteArray()))
}
val verified = actual.equals(expected)

Java

String packageName  =  // The caller's package name
Set<String> expected =  // The correct set of signing certificates

PackageInfo packageInfo =
        packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
Set<String> actual = new HashSet<>();
for (Signature it : packageInfo.signatures) {
    actual.add(SerializeByteArrayToString(sha256.digest(it.toByteArray())));
}
boolean verified = actual.equals(expected);

调试

使用以下命令观察错误或信息性消息:

adb logcat | grep -i pay