了解如何调整您的 Android 付款应用以与 Web Payments 配合使用,并为客户提供更好的用户体验。
Payment Request API 为 Web 带来了一个内置的基于浏览器的界面,使用户可以比以往更轻松地输入所需的付款信息。该 API 还可以调用特定于平台的付款应用。
与仅使用 Android Intent 相比,Web Payments 可以更好地与浏览器、安全性和用户体验集成
- 付款应用作为模态在商家网站的上下文中启动。
- 该实现是对您现有付款应用的补充,使您能够利用您的用户群。
- 将检查付款应用的签名以防止侧载。
- 付款应用可以支持多种付款方式。
- 任何付款方式,例如加密货币、银行转账等,都可以集成。Android 设备上的付款应用甚至可以集成需要访问设备硬件芯片的方法。
在 Android 付款应用中实现 Web Payments 需要四个步骤
- 让商家发现您的付款应用。
- 让商家知道客户是否有已注册的工具(例如信用卡)可以付款。
- 让客户付款。
- 验证调用者的签名证书。
要查看 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 参数中的交易信息来调用。
付款应用使用 methodName
和 details
进行响应,这些响应特定于付款应用,并且对浏览器是不透明的。浏览器通过 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
从每个 methodNames
到 methodData
的映射。
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
仅包含 supportedMethods
和 total
。
paymentRequestId
“推送付款”应用应与交易状态关联的 PaymentRequest.id
字段。商家网站将使用此字段查询“推送付款”应用的带外交易状态。
val paymentRequestId: String? = extras.getString("paymentRequestId")
响应
Activity 可以通过 setResult
和 RESULT_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 中是否包含非空的 methodName
和 details
。如果验证失败,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) } }