Learn how to adapt your Android payment application to work with Web Payments and provide a better user experience for customers.
the Payment request API brings to the web an integrated browser-based interface that allows users to enter required payment information easier than ever. The API can also invoke platform-specific payment applications.
Compared to using just Android Intents, web payments allow for better browser integration, security, and user experience:
- The payment application is launched as modal, in the context of the merchant's website.
- The implementation is complementary to your existing payment application, allowing you to take advantage of your user base.
- Payment app signature is checked to avoid
lateral load. - Payment apps can support multiple payment methods.
- Any payment method can be integrated, such as cryptocurrencies, bank transfers and more. Payment applications on Android devices can even integrate methods that require access to the hardware chip in the device.
There are four steps required to implement web payments in an Android payment application:
- Let merchants discover your payment application.
- Inform a merchant if a customer has a registered instrument (such as a credit card) that they are ready to pay.
- Let a customer make the payment.
- Check the signing certificate of the caller.
To see web payments in action, see the
payment-web-android
manifestation.
Step 1: let merchants discover your payment app
For a merchant to use your payment application, they must use the Payment request API and specify the payment method you support using the payment method identifier.
If you have a unique payment method identifier for your payment app, you can set yours payment method manifest so that browsers can discover your application.
Step 2: Inform a merchant if a client has a registered instrument that they are ready to pay
The merchant can call hasEnrolledInstrument ()
to see if the customer can make a payment. You can implement IS_READY_TO_PAY
as an Android service to answer this query.
AndroidManifest.xml
Declare your service with an intent filter with action
org.chromium.intent.action.IS_READY_TO_PAY
.
<service
android:yam=".SampleIsReadyToPayService"
android:exported="true">
<intent-filter>
<action android:yam="org.chromium.intent.action.IS_READY_TO_PAY" />
</intent-filter>
</service>
the IS_READY_TO_PAY
the service is optional. If there is no such intent handler in the payment application, then the web browser assumes that the application can always make payments.
AIDL
The API for IS_READY_TO_PAY
the service is defined in AIDL. Create two AIDL files with the following content:
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);
}
Implement IsReadyToPayService
The simplest implementation of IsReadyToPayService
shown in the following example:
class SampleIsReadyToPayService : Service() {
private val binder = object : IsReadyToPayService.Stub() {
override fun isReadyToPay(callback: IsReadyToPayServiceCallback?) {
callback?.handleIsReadyToPay(true)
}
}override fun onBind(I tried: Intent?): IBinder? {
return binder
}
}
Parameters
Pass the following parameters to onBind
as Intent extras:
methodNames
methodData
topLevelOrigin
topLevelCertificateChain
topLevelCertificateChain
paymentRequestOrigin
override fun onBind(I tried: Intent?): IBinder? {
val extras: Bundle? = I tried?.extras
}
methodNames
The names of the methods that are queried. The elements are the keys in the
methodData
dictionary and indicate the methods that the payment application supports.
val methodNames: List<String>? = extras.getStringArrayList("methodNames")
methodData
A mapping of each input of methodNames
to the
methodData
.
val methodData: Bundle? = extras.getBundle("methodData")
topLevelOrigin
The origin of the merchant without the schema (the origin without the schema of the top-level navigation context). For example, https://mystore.com/checkout
it will pass like mystore.com
.
val topLevelOrigin: String? = extras.getString("topLevelOrigin")
topLevelCertificateChain
The merchant's certificate chain (the certificate chain from the top-level navigation context). Null for localhost and file on disk, which are secure contexts without SSL certificates. The certificate chain is necessary because a payment application may have different trust requirements for websites.
val topLevelCertificateChain: Array<Parcelable>? =
extras.getParcelableArray("topLevelCertificateChain")
Every Parcelable
is a Bundle
with a "certificate"
key and a byte array value.
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
(p ace Bundle).getByteArray("certificate")
}
paymentRequestOrigin
The non-schema source of the iframe navigation context that was invoking new PaymentRequest (methodData, details, options)
constructor in JavaScript. If the constructor was invoked from the top-level context, then the value of this parameter is equal to the value of topLevelOrigin
parameter.
val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")
Answer
The service can send its response through handleIsReadyToPay (Boolean)
method.
callback?.handleIsReadyToPay(true)
Excuse me
You can use Binder.getCallingUid ()
to check who the caller is. Note that you have to do this in the isReadyToPay
method, not the onBind
method.
override fun isReadyToPay(callback: IsReadyToPayServiceCallback?) {
try {
val callingPackage = packageManager.getNameForUid(Binder.getCallingUid())
See Verify the caller's signing certificate for how to verify that the calling packet has the correct signature.
Step 3: let a customer check out
The merchant calls Show()
to start the payment application so that the customer can make a payment. The paid app is invoked through an Android intent PAY
with transaction information in the intent parameters.
The payment application responds with methodName
and details
, which are specific to the payment application and are opaque to the browser. The browser converts the details
String in a JavaScript object for the merchant via JSON deserialization, but it doesn't apply any validity beyond that. The browser does not modify the
details
; the value of that parameter is passed directly to the merchant.
AndroidManifest.xml
The activity with the PAY
The intent filter must have a A tag that identifies the default payment method identifier for the application.
To support multiple payment methods, add a label with a
resource.
<activity
android:yam=".PaymentActivity"
android:theme="@style/Theme.SamplePay.Dialog">
<intent-filter>
<action android:yam="org.chromium.intent.action.PAY" />
</intent-filter><meta-data
android:yam="org.chromium.default_payment_method_name"
android:value="https://bobpay.xyz/pay" />
<meta-data
android:yam="org.chromium.payment_method_names"
android:resource="@array/method_names" />
</activity>
the resource
must be a list of strings, each of which must be a valid absolute URL with an HTTPS scheme as shown here.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array yam="method_names">
<item>https://alicepay.com/put/optional/path/here</item>
<item>https://charliepay.com/put/optional/path/here</item>
</string-array>
</resources>
Parameters
The following parameters are passed to the activity as intent extras:
methodNames
methodData
topLevelOrigin
topLevelCertificateChain
paymentRequestOrigin
total
modifiers
paymentRequestId
val extras: Bundle? = I tried?.extras
methodNames
The names of the methods that are used. The elements are the keys in the
methodData
dictionary. These are the methods that the payment application supports.
val methodNames: List<String>? = extras.getStringArrayList("methodNames")
methodData
A mapping of each of the methodNames
to the
methodData
.
val methodData: Bundle? = extras.getBundle("methodData")
Merchant name
The content of the
HTML tag of the merchant checkout page (the top-level navigation context of the browser).
val merchantName: String? = extras.getString("merchantName")
topLevelOrigin
The origin of the merchant without the schema (The origin without the schema of the top-level navigation context). For example, https://mystore.com/checkout
it happens like mystore.com
.
val topLevelOrigin: String? = extras.getString("topLevelOrigin")
topLevelCertificateChain
The merchant's certificate chain (the certificate chain from the top-level navigation context). Null for localhost and file on disk, which are secure contexts without SSL certificates. Every Parcelable
it's a package with a
certificate
key and a byte array value.
val topLevelCertificateChain: Array<Parcelable>? =
extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
(p ace Bundle).getByteArray("certificate")
}
paymentRequestOrigin
The non-schema source of the iframe navigation context that was invoking new PaymentRequest (methodData, details, options)
constructor in JavaScript. If the constructor was invoked from the top-level context, then the value of this parameter is equal to the value of topLevelOrigin
parameter.
val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")
total
The JSON string that represents the total amount of the transaction.
val total: String? = extras.getString("total")
Here is an example of string content:
{"currency":"USD","value":"25.00"}
modifiers
The output of JSON.stringify (details.modifiers)
, where details.modifiers
contain only supportedMethods
and total
.
paymentRequestId
the PaymentRequest.id
field that “automatic payment” applications should associate with the status of the transaction. Merchant websites will use this field to query "auto pay" applications for out-of-band transaction status.
val paymentRequestId: String? = extras.getString("paymentRequestId")
Answer
The activity can send its response through setResult
with RESULT_OK
.
setResult(Activity.RESULT_OK, Intent().apply {
putExtra("methodName", "https://bobpay.xyz/pay")
putExtra("details", "{"token": "put-some-data-here"}")
})
finish()
You must specify two parameters as intent extras:
methodName
: The name of the method that is being used.details
: JSON string containing the information necessary for the merchant to complete the transaction. If success istrue
, thendetails
must be constructed in such a way thatJSON.parse (details)
it could happen.
You can pass RESULT_CANCELED
if the transaction was not completed in the payment application, for example if the user did not enter the correct PIN code for their account in the payment application. The browser may allow the user to choose a different payment application.
setResult(RESULT_CANCELED)
finish()
If the activity result of a payment response received from the invoked payment application is set to RESULT_OK
, then Chrome will check if it is not empty methodName
and
details
in your extras. If validation fails, Chrome will return a rejected promise of request.show ()
with one of the following error messages faced by developers:
'Payment app returned invalid response. Missing field "details".'
'Payment app returned invalid response. Missing field "methodName".'
Excuse me
The activity can check the caller with their getCallingPackage ()
method.
val caller: String? = callingPackage
The last step is to verify the caller's signing certificate to confirm that the calling packet has the correct signature.
Step 4: Verify the caller's signing certificate
You can check the caller's package name with Binder.getCallingUid ()
in
IS_READY_TO_PAY
, and with Activity.getCallingPackage ()
in PAY
. To really verify that the caller is the browser you have in mind, you need to check their signing certificate and make sure it matches the correct value.
If you are targeting API level 28 and above and you are integrating with a browser that has a single signing certificate, you can use
PackageManager.hasSigningCertificate ()
.
val packageName: String = …
val certificate: ByteArray = …
val verified = packageManager.hasSigningCertificate(
callingPackage,
certificate,
PackageManager.CERT_INPUT_SHA256
)
PackageManager.hasSigningCertificate ()
It is preferred for single certificate browsers as it handles certificate rotation correctly. (Chrome has only one signing certificate.) Applications that have multiple signing certificates cannot rotate them.
If you need to support API levels older than 27 and lower, or if you need to handle browsers with multiple signing certificates, you can use
PackageManager.GET_SIGNATURES
.
val packageName: String = …
val certificates: Set<ByteArray> = … val packageInfo = getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val sha256 = MessageDigest.getInstance("SHA-256")
val signatures = packageInfo.signatures.map { sha256.digest(Item.toByteArray()) }
val verified = signatures.size == certificates.size &&
signatures.there { s -> certificates.any { Item.contentEquals(s) } }