Android 付款应用开发者指南

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

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

浏览器支持

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

来源

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

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

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

在 Android 付款应用中实现 Web Payments 需要四个步骤

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

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

步骤 1:让商家发现您的付款应用

为了让商家使用您的付款应用,他们需要使用 Payment Request API 并使用付款方式标识符指定您支持的付款方式。

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

步骤 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 文件

app/src/main/aidl/org/chromium/IsReadyToPayServiceCallback.aidl

package org.chromium;
interface IsReadyToPayServiceCallback {
    oneway void handleIsReadyToPay(boolean isReadyToPay);
}

app/src/main/aidl/org/chromium/IsReadyToPayService.aidl

package org.chromium;
import org.chromium.IsReadyToPayServiceCallback;

interface IsReadyToPayService {
    oneway void isReadyToPay(IsReadyToPayServiceCallback callback);
}

实现 IsReadyToPayService

IsReadyToPayService 的最简单实现如下例所示

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

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

响应

服务可以通过 handleIsReadyToPay(Boolean) 方法发送其响应。

callback?.handleIsReadyToPay(true)

权限

您可以使用 Binder.getCallingUid() 来检查调用者是谁。请注意,您必须在 isReadyToPay 方法中执行此操作,而不是在 onBind 方法中。

override fun isReadyToPay(callback: IsReadyToPayServiceCallback?) {
  try {
    val callingPackage = packageManager.getNameForUid(Binder.getCallingUid())
    // …

有关如何验证调用软件包是否具有正确签名的信息,请参阅验证调用者的签名证书

步骤 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/method_names" />
</activity>

resource 必须是字符串列表,每个字符串都必须是有效的绝对 URL,并带有 HTTPS 方案,如此处所示。

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

参数

以下参数作为 Intent extras 传递给 Activity

  • methodNames
  • methodData
  • topLevelOrigin
  • topLevelCertificateChain
  • paymentRequestOrigin
  • total
  • modifiers
  • paymentRequestId
val extras: Bundle? = intent?.extras

methodNames

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

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

methodData

从每个 methodNamesmethodData 的映射。

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

merchantName

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

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

topLevelOrigin

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

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

topLevelCertificateChain

商家的证书链(顶级浏览上下文的证书链)。对于 localhost 和磁盘上的文件,两者都是没有 SSL 证书的安全上下文,则为 Null。每个 Parcelable 都是一个 Bundle,其中包含一个 certificate 键和一个字节数组值。

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

paymentRequestOrigin

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

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

total

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

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

以下是字符串的示例内容

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

modifiers

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

paymentRequestId

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

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

响应

Activity 可以通过 setResultRESULT_OK 发送回响应。

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

您必须指定两个参数作为 Intent extras

  • methodName:正在使用的方法的名称。
  • details:JSON 字符串,包含商家完成交易所需的信息。如果 success 为 true,则必须以 JSON.parse(details) 将成功的方式构造 details

如果交易未在付款应用中完成,例如,如果用户未能在付款应用中键入其帐户的正确 PIN 码,则可以传递 RESULT_CANCELED。浏览器可能会让用户选择其他付款应用。

setResult(RESULT_CANCELED)
finish()

如果从调用的付款应用收到的付款响应的 Activity 结果设置为 RESULT_OK,则 Chrome 将检查其 extras 中是否包含非空的 methodNamedetails。如果验证失败,Chrome 将从 request.show() 返回一个被拒绝的 Promise,并显示以下面向开发者的错误消息之一

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

权限

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

val caller: String? = callingPackage

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

步骤 4:验证调用者的签名证书

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

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

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
)

PackageManager.hasSigningCertificate() 是单证书浏览器的首选,因为它正确处理证书轮换。(Chrome 具有单个签名证书。)具有多个签名证书的应用无法轮换它们。

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

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

val packageInfo = getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val sha256 = MessageDigest.getInstance("SHA-256")
val signatures = packageInfo.signatures.map { sha256.digest(it.toByteArray()) }
val verified = signatures.size == certificates.size &&
    signatures.all { s -> certificates.any { it.contentEquals(s) } }