Hướng dẫn dành cho nhà phát triển ứng dụng thanh toán trên Android

Tìm hiểu cách điều chỉnh ứng dụng thanh toán Android để hoạt động với Web Payments và mang lại trải nghiệm người dùng tốt hơn cho khách hàng.

Xuất bản: Ngày 5 tháng 5 năm 2020, Lần cập nhật gần đây nhất: Ngày 27 tháng 5 năm 2025

Payment Request API mang đến cho web một giao diện tích hợp sẵn dựa trên trình duyệt, cho phép người dùng nhập thông tin thanh toán bắt buộc dễ dàng hơn bao giờ hết. API này cũng có thể gọi các ứng dụng thanh toán dành riêng cho nền tảng.

Browser Support

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

Source

Quy trình thanh toán bằng ứng dụng Google Pay dành riêng cho nền tảng sử dụng Web Payments.

So với việc chỉ sử dụng Ý định của Android, Web Payments cho phép tích hợp tốt hơn với trình duyệt, tính bảo mật và trải nghiệm người dùng:

  • Ứng dụng thanh toán được khởi chạy dưới dạng một phương thức, trong bối cảnh trang web của người bán.
  • Việc triển khai này bổ sung cho ứng dụng thanh toán hiện có của bạn, cho phép bạn tận dụng cơ sở người dùng của mình.
  • Chữ ký của ứng dụng thanh toán được kiểm tra để ngăn chặn việc tải ứng dụng bên ngoài.
  • Các ứng dụng thanh toán có thể hỗ trợ nhiều phương thức thanh toán.
  • Bạn có thể tích hợp mọi phương thức thanh toán, chẳng hạn như tiền mã hoá, chuyển khoản ngân hàng và nhiều phương thức khác. Các ứng dụng thanh toán trên thiết bị Android thậm chí có thể tích hợp các phương thức yêu cầu quyền truy cập vào chip phần cứng trên thiết bị.

Bạn cần thực hiện 4 bước để triển khai Web Payments trong một ứng dụng thanh toán Android:

  1. Giúp người bán khám phá ứng dụng thanh toán của bạn.
  2. Cho người bán biết liệu khách hàng có công cụ đã đăng ký (chẳng hạn như thẻ tín dụng) sẵn sàng thanh toán hay không.
  3. Cho phép khách hàng thanh toán.
  4. Xác minh chứng chỉ ký của bên gọi.

Để xem Web Payments hoạt động, hãy xem bản minh hoạ android-web-payment.

Bước 1: Giúp người bán khám phá ứng dụng thanh toán của bạn

Đặt thuộc tính related_applications trong tệp kê khai ứng dụng web theo hướng dẫn trong phần Thiết lập phương thức thanh toán.

Để người bán có thể sử dụng ứng dụng thanh toán của bạn, họ cần sử dụng Payment Request API và chỉ định phương thức thanh toán mà bạn hỗ trợ bằng cách sử dụng mã nhận dạng phương thức thanh toán.

Nếu có một mã nhận dạng phương thức thanh toán riêng cho ứng dụng thanh toán của mình, bạn có thể thiết lập tệp kê khai phương thức thanh toán riêng để các trình duyệt có thể phát hiện ứng dụng của bạn.

Bước 2: Thông báo cho người bán biết nếu khách hàng có một công cụ đã đăng ký và sẵn sàng thanh toán

Người bán có thể gọi hasEnrolledInstrument() để truy vấn xem khách hàng có thể thanh toán hay không. Bạn có thể triển khai IS_READY_TO_PAY dưới dạng một dịch vụ Android để trả lời truy vấn này.

AndroidManifest.xml

Khai báo dịch vụ bằng một bộ lọc ý định có thao tác org.chromium.intent.action.IS_READY_TO_PAY.

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

Dịch vụ IS_READY_TO_PAY là không bắt buộc. Nếu không có trình xử lý ý định như vậy trong ứng dụng thanh toán, thì trình duyệt web sẽ giả định rằng ứng dụng luôn có thể thực hiện thanh toán.

AIDL

API cho dịch vụ IS_READY_TO_PAY được xác định trong AIDL. Tạo 2 tệp AIDL có nội dung sau:

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);
}

Triển khai IsReadyToPayService

Ví dụ sau đây minh hoạ cách triển khai đơn giản nhất của 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;
    }
}

Phản hồi

Dịch vụ có thể gửi phản hồi bằng phương thức handleIsReadyToPay(Boolean).

Kotlin

callback?.handleIsReadyToPay(true)

Java

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

Quyền

Bạn có thể dùng Binder.getCallingUid() để kiểm tra xem người gọi là ai. Xin lưu ý rằng bạn phải thực hiện việc này trong phương thức isReadyToPay, chứ không phải trong phương thức onBind, vì Hệ điều hành Android có thể lưu vào bộ nhớ đệm và sử dụng lại kết nối dịch vụ, điều này không kích hoạt phương thức 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());
        // ...

Luôn kiểm tra các tham số đầu vào cho null khi nhận được các lệnh gọi Giao tiếp giữa các quy trình (IPC). Điều này đặc biệt quan trọng vì các phiên bản hoặc nhánh khác nhau của hệ điều hành Android có thể hoạt động theo những cách không mong muốn và dẫn đến lỗi nếu không được xử lý.

Mặc dù packageManager.getPackagesForUid() thường trả về một phần tử duy nhất, nhưng mã của bạn phải xử lý trường hợp không phổ biến khi một phương thức gọi sử dụng nhiều tên gói. Điều này giúp đảm bảo ứng dụng của bạn vẫn hoạt động ổn định.

Hãy xem phần Xác minh chứng chỉ ký của người gọi để biết cách xác minh rằng gói gọi có chữ ký phù hợp.

Thông số

Gói parameters đã được thêm vào Chrome 139. Bạn phải luôn kiểm tra với null.

Các tham số sau được truyền đến dịch vụ trong Gói parameters:

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

packageName được thêm vào Chrome 138. Bạn phải xác minh tham số này dựa trên Binder.getCallingUid() trước khi sử dụng giá trị của tham số. Quy trình xác minh này là cần thiết vì gói parameters hoàn toàn do phương thức gọi kiểm soát, trong khi Binder.getCallingUid() do hệ điều hành Android kiểm soát.

topLevelCertificateChainnull trong WebView và trên các trang web không phải https thường được dùng để kiểm thử cục bộ, chẳng hạn như http://localhost.

Bước 3: Cho phép khách hàng thanh toán

Người bán gọi show() để chạy ứng dụng thanh toán để khách hàng có thể thanh toán. Ứng dụng thanh toán được gọi bằng cách sử dụng ý định Android PAY có thông tin giao dịch trong các tham số ý định.

Ứng dụng thanh toán phản hồi bằng methodNamedetails. Đây là các ứng dụng thanh toán cụ thể và không hiển thị cho trình duyệt. Trình duyệt chuyển đổi chuỗi details thành từ điển JavaScript cho người bán bằng cách sử dụng quy trình chuyển đổi chuỗi JSON, nhưng không thực thi bất kỳ tính hợp lệ nào ngoài quy trình đó. Trình duyệt không sửa đổi details; giá trị của tham số đó được truyền trực tiếp đến người bán.

AndroidManifest.xml

Hoạt động có bộ lọc ý định PAY phải có thẻ <meta-data> xác định giá trị nhận dạng phương thức thanh toán mặc định cho ứng dụng.

Để hỗ trợ nhiều phương thức thanh toán, hãy thêm thẻ <meta-data> bằng tài nguyên <string-array>.

<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 phải là một danh sách các chuỗi, mỗi chuỗi phải là một URL tuyệt đối hợp lệ có giao thức HTTPS như minh hoạ ở đây.

<?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>

Thông số

Các tham số sau được truyền đến hoạt động dưới dạng các mã bổ sung Intent:

  • 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

Tên của các phương thức đang được sử dụng. Các phần tử là khoá trong từ điển methodData. Đây là những phương thức mà ứng dụng thanh toán hỗ trợ.

Kotlin

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

Java

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

methodData

Một mối liên kết từ mỗi methodNames đến methodData.

Kotlin

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

Java

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

merchantName

Nội dung của thẻ HTML <title> trên trang thanh toán của người bán (bối cảnh duyệt web cấp cao nhất của trình duyệt).

Kotlin

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

Java

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

topLevelOrigin

Nguồn gốc của người bán mà không có lược đồ (Nguồn gốc không có lược đồ của bối cảnh duyệt web cấp cao nhất). Ví dụ: https://mystore.com/checkout được truyền dưới dạng mystore.com.

Kotlin

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

Java

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

topLevelCertificateChain

Chuỗi chứng chỉ của người bán (chuỗi chứng chỉ của ngữ cảnh duyệt web cấp cao nhất). Giá trị là null cho WebView, localhost hoặc một tệp trên ổ đĩa. Mỗi Parcelable là một Bundle có khoá certificate và giá trị mảng byte.

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

Nguồn gốc không có lược đồ của ngữ cảnh duyệt web iframe đã gọi hàm khởi tạo new PaymentRequest(methodData, details, options) trong JavaScript. Nếu hàm khởi tạo được gọi từ ngữ cảnh cấp cao nhất, thì giá trị của tham số này sẽ bằng giá trị của tham số topLevelOrigin.

Kotlin

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

Java

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

total

Chuỗi JSON biểu thị tổng số tiền của giao dịch.

Kotlin

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

Java

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

Sau đây là nội dung mẫu của chuỗi:

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

modifiers

Đầu ra của JSON.stringify(details.modifiers), trong đó details.modifiers chỉ chứa supportedMethods, datatotal.

paymentRequestId

Trường PaymentRequest.id mà các ứng dụng "thanh toán đẩy" phải liên kết với trạng thái giao dịch. Trang web của người bán sẽ sử dụng trường này để truy vấn các ứng dụng "thanh toán đẩy" về trạng thái của giao dịch ngoài băng tần.

Kotlin

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

Java

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

Phản hồi

Hoạt động có thể gửi phản hồi trở lại thông qua setResult bằng RESULT_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();

Bạn phải chỉ định 2 tham số dưới dạng các phần bổ sung của Ý định:

  • methodName: Tên của phương thức đang được sử dụng.
  • details: Chuỗi JSON chứa thông tin cần thiết để người bán hoàn tất giao dịch. Nếu thành công là true, thì details phải được tạo theo cách mà JSON.parse(details) sẽ thành công. Nếu không có dữ liệu cần được trả về, thì chuỗi này có thể là "{}". Trang web của người bán sẽ nhận được chuỗi này dưới dạng một từ điển JavaScript trống.

Bạn có thể truyền RESULT_CANCELED nếu người dùng huỷ giao dịch trong ứng dụng thanh toán. Thao tác này sẽ khiến request.show() từ chối bằng một AbortError trên trang web của người bán, cho biết người dùng đã huỷ.

Kotlin

setResult(Activity.RESULT_CANCELED)
finish()

Java

setResult(Activity.RESULT_CANCELED);
finish();

Kể từ Chrome 149, các giá trị kết quả sau đây được hỗ trợ:

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)

Nếu ứng dụng thanh toán không hoạt động do lỗi nội bộ, bạn có thể cho biết điều này bằng cách truyền Activity.RESULT_FIRST_USER làm mã kết quả.

Nếu INTERNAL_PAYMENT_APP_ERROR được trả về, request.show() sẽ từ chối bằng một OperationError trên trang web của người bán, cho biết có lỗi trong ứng dụng thanh toán.

Sự khác biệt này giữa RESULT_CANCELED (0) cho trường hợp người dùng huỷ (gây ra AbortError) và INTERNAL_PAYMENT_APP_ERROR (1) cho trường hợp lỗi nội bộ của ứng dụng (gây ra OperationError) cho phép người bán xây dựng luồng người dùng tốt hơn.

Kotlin

setResult(Activity.RESULT_FIRST_USER)
finish()

Java

setResult(Activity.RESULT_FIRST_USER);
finish();

Nếu kết quả hoạt động của một phản hồi thanh toán nhận được từ ứng dụng thanh toán được gọi được đặt thành RESULT_OK, thì Chrome sẽ kiểm tra methodNamedetails không trống trong các phần bổ sung của ứng dụng. Nếu quá trình xác thực không thành công, Chrome sẽ trả về một lời hứa bị từ chối từ request.show() kèm theo một trong các thông báo lỗi sau đây dành cho nhà phát triển:

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

Quyền

Hoạt động có thể kiểm tra phương thức gọi bằng phương thức getCallingPackage().

Kotlin

val caller: String? = callingPackage

Java

String caller = getCallingPackage();

Bước cuối cùng là xác minh chứng chỉ ký của phương thức gọi để xác nhận rằng gói gọi có chữ ký phù hợp.

Bước 4: Xác minh chứng chỉ ký của người gọi

Bạn có thể kiểm tra tên gói của phương thức gọi bằng Binder.getCallingUid() trong IS_READY_TO_PAY và bằng Activity.getCallingPackage() trong PAY. Để thực sự xác minh rằng phương thức gọi là trình duyệt mà bạn muốn, bạn nên kiểm tra chứng chỉ ký của trình duyệt đó và đảm bảo rằng chứng chỉ này khớp với giá trị chính xác.

Nếu đang nhắm đến API cấp 28 trở lên và đang tích hợp với một trình duyệt có một chứng chỉ ký duy nhất, bạn có thể sử dụng 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() được ưu tiên cho các trình duyệt có một chứng chỉ, vì trình duyệt này xử lý chính xác việc xoay vòng chứng chỉ. (Chrome có một chứng chỉ ký duy nhất.) Những ứng dụng có nhiều chứng chỉ ký không thể xoay vòng các chứng chỉ đó.

Nếu cần hỗ trợ API cấp 27 trở xuống hoặc nếu cần xử lý các trình duyệt có nhiều chứng chỉ ký, bạn có thể dùng 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);

Gỡ lỗi

Sử dụng lệnh sau để quan sát các lỗi hoặc thông báo thông tin:

adb logcat | grep -i pay