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 trên Android sao cho phù hợp với dịch vụ Thanh toán trên web và mang đến trải nghiệm tốt hơn cho khách hàng.

API Yêu cầu thanh toán mang đến giao diện tích hợp sẵn dựa trên trình duyệt cho web, cho phép người dùng nhập thông tin thanh toán cần thiết 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.

Hỗ trợ trình duyệt

  • 60
  • 15
  • 11,1

Nguồn

Quy trình thanh toán bằng ứng dụng Google Pay dành riêng cho nền tảng, có sử dụng Thanh toán trên web.

So với việc chỉ sử dụng Android Intent, phương thức Thanh toán trên web 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, theo bối cảnh của trang web của người bán.
  • Hoạt động triển khai 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 để tránh cài đặt không qua cửa hàng ứng dụng.
  • Ứ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à các 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ị.

Cần thực hiện bốn bước để triển khai Thanh toán trên web trong ứng dụng thanh toán Android:

  1. Cho phép người bán khám phá ứng dụng thanh toán của bạn.
  2. Hãy cho người bán biết nếu khách hàng đã đăng ký phương thức thanh toán (chẳng hạn như thẻ tín dụng) và có thể thanh toán.
  3. Cho phép khách hàng thanh toán.
  4. Xác minh chứng chỉ ký của phương thức gọi.

Để xem cách hoạt động của tính năng Thanh toán trên web, hãy xem bản minh hoạ android-web-payment.

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

Để người bán sử dụng ứng dụng thanh toán của bạn, họ cần phải sử dụng API yêu cầu thanh toán 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ó giá trị nhận dạng phương thức thanh toán dành riêng cho ứng dụng thanh toán của mình, thì bạn có thể thiết lập tệp kê khai phương thức thanh toán của riêng mình để các trình duyệt có thể khám phá ứng dụng của bạn.

Bước 2: Thông báo cho người bán nếu khách hàng đã đăng ký phương thức thanh toán và có thể dù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 bộ lọc ý định với 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ể 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:

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

Triển khai IsReadyToPayService

Cách triển khai đơn giản nhất của IsReadyToPayService được thể hiện trong ví dụ sau:

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

Phản hồi

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

callback?.handleIsReadyToPay(true)

Quyền

Bạn có thể sử dụng Binder.getCallingUid() để kiểm tra xem người gọi là ai. 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.

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

Hãy xem bài viết Xác minh chứng chỉ ký của phương thức gọi về cách xác minh rằng gói gọi có chữ ký chính xác.

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 nhằm giúp khách hàng có thể thanh toán. Ứng dụng thanh toán được gọi qua một ý định PAY trên Android với 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 dành riêng và được làm mờ đối với trình duyệt. Trình duyệt chuyển đổi chuỗi details thành đối tượng JavaScript cho người bán thông qua quá trình huỷ chuyển đổi tuần tự JSON, nhưng không thực thi bất kỳ tính hợp lệ nào ngoài đối tượng đó. Trình duyệt không sửa đổi details; giá trị của thông 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> giúp 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> có 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/method_names" />
</activity>

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ệ với lược đồ HTTPS như minh hoạ ở đây.

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

Tham số

Các tham số sau đây được truyền đến hoạt động dưới dạng thành phần bổ sung Ý định:

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

methodNames

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

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

methodData

Ánh xạ từ mỗi methodNames đến methodData.

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

merchantName

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

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

topLevelOrigin

Nguồn gốc của người bán mà không có giao thức (Nguồn gốc không có lược đồ của ngữ 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.

val topLevelOrigin: String? = 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ị rỗng đối với máy chủ cục bộ và tệp trên ổ đĩa. Cả hai đều là ngữ cảnh bảo mật không có chứng chỉ SSL. Mỗi Parcelable là một Gói có một khoá certificate và một giá trị mảng byte.

val topLevelCertificateChain: Array<Parcelable>? =
    extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
  (p as Bundle).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.

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

total

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

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

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

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

modifiers

Kết quả của JSON.stringify(details.modifiers), trong đó details.modifiers chỉ chứa supportedMethodstotal.

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" cho trạng thái giao dịch nằm ngoài phạm vi.

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

Phản hồi

Hoạt động này có thể gửi lại phản hồi thông qua setResult bằng RESULT_OK.

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

Bạn phải chỉ định hai tham số làm tham số Ý định bổ sung:

  • 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.

Bạn có thể chuyển RESULT_CANCELED nếu giao dịch không được hoàn tất trong ứng dụng thanh toán, chẳng hạn như nếu người dùng không nhập đúng mã PIN cho tài khoản của họ trong ứng dụng thanh toán. Trình duyệt có thể cho phép người dùng chọn một ứng dụng thanh toán khác.

setResult(RESULT_CANCELED)
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 đã gọi được đặt thành RESULT_OK, thì Chrome sẽ kiểm tra để tìm methodNamedetails không trống trong các phần bổ sung. Nếu 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 mà nhà phát triển gặp phải sau:

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

Quyền

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

val caller: String? = callingPackage

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ó đúng chữ ký.

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 được phương thức gọi là trình duyệt mà bạn nghĩ đến, bạn nên kiểm tra chứng chỉ ký của trình duyệt đó và đảm bảo phương thức gọi khớp với đúng giá trị.

Nếu đang nhắm mục tiêu 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ý, thì bạn có thể sử dụng 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() được ưu tiên cho các trình duyệt chứng chỉ đơn 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ý). Các ứng dụng có nhiều chứng chỉ ký sẽ không thể xoay vòng các chứng chỉ đó.

Nếu cần hỗ trợ API cấp 27 cũ 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ể sử dụng 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) } }