Panduan developer aplikasi pembayaran Android

Pelajari cara menyesuaikan aplikasi pembayaran Android Anda agar berfungsi dengan Pembayaran Web dan memberikan pengalaman pengguna yang lebih baik bagi pelanggan.

Payment Request API menghadirkan antarmuka berbasis browser bawaan ke web, yang memungkinkan pengguna memasukkan informasi pembayaran yang diperlukan dengan lebih mudah daripada sebelumnya. API juga dapat memanggil aplikasi pembayaran khusus platform.

Dukungan Browser

  • 60
  • 15
  • 11.1

Sumber

Alur checkout dengan aplikasi Google Pay khusus platform yang menggunakan Pembayaran Web.

Dibandingkan dengan Intent Android saja, Pembayaran Web memungkinkan integrasi yang lebih baik dengan browser, keamanan, dan pengalaman pengguna:

  • Aplikasi pembayaran diluncurkan sebagai modal, dalam konteks situs penjual.
  • Penerapannya bersifat tambahan untuk aplikasi pembayaran yang sudah ada sehingga Anda dapat memanfaatkan basis pengguna Anda.
  • Tanda tangan aplikasi pembayaran diperiksa untuk mencegah sideload.
  • Aplikasi pembayaran dapat mendukung beberapa metode pembayaran.
  • Metode pembayaran apa pun, seperti mata uang kripto, transfer bank, dan lainnya, dapat diintegrasikan. Aplikasi pembayaran di perangkat Android bahkan dapat mengintegrasikan metode yang memerlukan akses ke chip hardware di perangkat.

Dibutuhkan empat langkah untuk menerapkan Pembayaran Web di aplikasi pembayaran Android:

  1. Beri kesempatan bagi penjual untuk menemukan aplikasi pembayaran Anda.
  2. Beri tahu penjual jika pelanggan memiliki instrumen terdaftar (seperti kartu kredit) yang siap digunakan untuk melakukan pembayaran.
  3. Izinkan pelanggan melakukan pembayaran.
  4. Verifikasi sertifikat penandatanganan pemanggil.

Untuk melihat cara kerja Pembayaran Web, lihat demo android-web-payment.

Langkah 1: Izinkan penjual menemukan aplikasi pembayaran Anda

Agar penjual dapat menggunakan aplikasi pembayaran Anda, mereka harus menggunakan Payment Request API dan menentukan metode pembayaran yang Anda dukung menggunakan ID metode pembayaran.

Jika memiliki ID metode pembayaran yang unik untuk aplikasi pembayaran, Anda dapat menyiapkan manifes metode pembayaran sendiri agar browser dapat menemukan aplikasi Anda.

Langkah 2: Beri tahu penjual jika pelanggan memiliki instrumen terdaftar yang siap untuk melakukan pembayaran

Penjual dapat memanggil hasEnrolledInstrument() untuk melakukan kueri apakah pelanggan dapat melakukan pembayaran. Anda dapat mengimplementasikan IS_READY_TO_PAY sebagai layanan Android untuk menjawab kueri ini.

AndroidManifest.xml

Deklarasikan layanan Anda dengan filter intent dengan tindakan 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>

Layanan IS_READY_TO_PAY bersifat opsional. Jika tidak ada pengendali intent seperti itu di aplikasi pembayaran, browser web akan mengasumsikan bahwa aplikasi selalu dapat melakukan pembayaran.

AIDL

API untuk layanan IS_READY_TO_PAY ditentukan dalam AIDL. Buat dua file AIDL dengan konten berikut:

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

Menerapkan IsReadyToPayService

Implementasi paling sederhana dari IsReadyToPayService ditampilkan dalam contoh berikut:

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

Respons

Layanan dapat mengirimkan responsnya melalui metode handleIsReadyToPay(Boolean).

callback?.handleIsReadyToPay(true)

Izin

Anda dapat menggunakan Binder.getCallingUid() untuk memeriksa siapa pemanggilnya. Perhatikan bahwa Anda harus melakukannya dalam metode isReadyToPay, bukan dalam metode onBind.

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

Lihat Memverifikasi sertifikat penandatanganan pemanggil tentang cara memverifikasi bahwa paket panggilan memiliki tanda tangan yang benar.

Langkah 3: Izinkan pelanggan melakukan pembayaran

Penjual memanggil show() untuk meluncurkan aplikasi pembayaran agar pelanggan dapat melakukan pembayaran. Aplikasi pembayaran dipanggil melalui PAY intent Android dengan informasi transaksi dalam parameter intent.

Aplikasi pembayaran merespons dengan methodName dan details, yang merupakan aplikasi pembayaran khusus dan tidak transparan terhadap browser. Browser mengonversi string details menjadi objek JavaScript untuk penjual melalui deserialisasi JSON, tetapi tidak menerapkan validitas apa pun selain itu. Browser tidak mengubah details; nilai parameter tersebut diteruskan langsung ke penjual.

AndroidManifest.xml

Aktivitas dengan filter intent PAY harus memiliki tag <meta-data> yang mengidentifikasi ID metode pembayaran default untuk aplikasi tersebut.

Untuk mendukung beberapa metode pembayaran, tambahkan tag <meta-data> dengan resource <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 harus berupa daftar string, yang masing-masing harus berupa URL absolut yang valid dengan skema HTTPS seperti yang ditampilkan di sini.

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

Parameter

Parameter berikut diteruskan ke aktivitas sebagai ekstra Intent:

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

methodNames

Nama-nama metode yang digunakan. Elemen tersebut adalah kunci dalam kamus methodData. Berikut adalah metode yang didukung aplikasi pembayaran.

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

methodData

Pemetaan dari setiap methodNames ke methodData.

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

merchantName

Konten tag HTML <title> di halaman checkout penjual (konteks penjelajahan level atas browser).

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

topLevelOrigin

Origin penjual tanpa skema (Origin tanpa skema dari konteks penjelajahan tingkat atas). Misalnya, https://mystore.com/checkout diteruskan sebagai mystore.com.

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

topLevelCertificateChain

Rantai sertifikat penjual (Rantai sertifikat dari konteks penjelajahan level atas). Null untuk localhost dan file pada disk, yang merupakan konteks aman tanpa sertifikat SSL. Setiap Parcelable adalah Paket dengan kunci certificate dan nilai array byte.

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

paymentRequestOrigin

Asal tanpa skema dari konteks penjelajahan iframe yang memanggil konstruktor new PaymentRequest(methodData, details, options) di JavaScript. Jika konstruktor dipanggil dari konteks tingkat atas, nilai parameter ini sama dengan nilai parameter topLevelOrigin.

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

total

String JSON yang mewakili jumlah total transaksi.

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

Berikut ini contoh konten string:

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

modifiers

Output JSON.stringify(details.modifiers), dengan details.modifiers hanya berisi supportedMethods dan total.

paymentRequestId

Kolom PaymentRequest.id yang harus dikaitkan oleh aplikasi "pembayaran push" dengan status transaksi. Situs penjual akan menggunakan kolom ini untuk mengkueri aplikasi "pembayaran push" untuk status transaksi di luar band.

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

Respons

Aktivitas dapat mengirimkan responsnya kembali melalui setResult dengan RESULT_OK.

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

Anda harus menentukan dua parameter sebagai tambahan Intent:

  • methodName: Nama metode yang digunakan.
  • details: String JSON yang berisi informasi yang diperlukan bagi penjual untuk menyelesaikan transaksi. Jika kesuksesannya adalah true, details harus dibuat sedemikian rupa sehingga JSON.parse(details) akan berhasil.

Anda dapat meneruskan RESULT_CANCELED jika transaksi tidak diselesaikan di aplikasi pembayaran, misalnya, jika pengguna gagal mengetikkan kode PIN yang benar untuk akunnya di aplikasi pembayaran. Browser dapat mengizinkan pengguna memilih aplikasi pembayaran lain.

setResult(RESULT_CANCELED)
finish()

Jika hasil aktivitas respons pembayaran yang diterima dari aplikasi pembayaran yang dipanggil ditetapkan ke RESULT_OK, Chrome akan memeriksa methodName dan details yang tidak kosong di bagian tambahannya. Jika validasi gagal, Chrome akan menampilkan promise yang ditolak dari request.show() dengan salah satu pesan error yang ditampilkan kepada developer berikut:

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

Izin

Aktivitas ini dapat memeriksa pemanggil dengan metode getCallingPackage().

val caller: String? = callingPackage

Langkah terakhir adalah memverifikasi sertifikat penandatanganan pemanggil untuk mengonfirmasi bahwa paket panggilan memiliki tanda tangan yang tepat.

Langkah 4: Verifikasi sertifikat penandatanganan pemanggil

Anda dapat memeriksa nama paket pemanggil dengan Binder.getCallingUid() di IS_READY_TO_PAY, dan dengan Activity.getCallingPackage() di PAY. Untuk benar-benar memverifikasi bahwa pemanggil adalah browser yang Anda inginkan, Anda harus memeriksa sertifikat penandatanganannya dan memastikannya cocok dengan nilai yang benar.

Jika menargetkan API level 28 dan yang lebih baru, serta berintegrasi dengan browser yang memiliki satu sertifikat penandatanganan, Anda dapat menggunakan 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() lebih disarankan untuk browser sertifikat tunggal, karena browser ini menangani rotasi sertifikat dengan benar. (Chrome memiliki sertifikat penandatanganan tunggal.) Aplikasi yang memiliki beberapa sertifikat penandatanganan tidak dapat merotasinya.

Jika Anda perlu mendukung API level 27 dan yang lebih lama, atau jika Anda perlu menangani browser dengan beberapa sertifikat penandatanganan, Anda dapat menggunakan 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) } }