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 mengarahkan ke web antarmuka bawaan berbasis browser yang memungkinkan pengguna memasukkan pembayaran yang diperlukan informasi menjadi lebih mudah. API juga dapat memanggil pembayaran khusus platform aplikasi.

Dukungan Browser

  • Chrome: 60.
  • Edge: 15.
  • Firefox: di balik bendera.
  • Safari: 11.1.

Sumber

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

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

  • Aplikasi pembayaran diluncurkan sebagai modal, dalam konteks situs penjual.
  • Penerapan ini hanya tambahan untuk aplikasi pembayaran yang sudah ada, sehingga Anda dapat memanfaatkan basis pengguna Anda.
  • Tanda tangan aplikasi pembayaran diperiksa untuk mencegah melakukan sideload.
  • Aplikasi pembayaran dapat mendukung beberapa metode pembayaran.
  • Metode pembayaran apa pun, seperti mata uang kripto, transfer bank, dan lainnya, dapat terintegrasi. Aplikasi pembayaran di perangkat Android bahkan dapat mengintegrasikan metode yang memerlukan akses ke {i>chip<i} perangkat keras pada perangkat.

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

  1. Permudah penjual menemukan aplikasi pembayaran Anda.
  2. Beri tahu penjual jika pelanggan memiliki instrumen yang terdaftar (seperti saldo ) yang siap digunakan untuk membayar.
  3. Mengizinkan pelanggan melakukan pembayaran.
  4. Verifikasi sertifikat penandatanganan penelepon.

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

Langkah 1: Biarkan penjual menemukan aplikasi pembayaran Anda

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

Jika Anda memiliki ID metode pembayaran yang unik untuk aplikasi pembayaran Anda, dapat menyiapkan metode pembayaran Anda sendiri manifes sehingga 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 meminta informasi apakah pelanggan bisa melakukan pembayaran. Anda dapat menerapkan IS_READY_TO_PAY sebagai layanan Android untuk menjawab kueri ini.

AndroidManifest.xml

Mendeklarasikan 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, maka browser web berasumsi bahwa aplikasi selalu bisa melakukan pembayaran.

AIDL

API untuk layanan IS_READY_TO_PAY ditentukan dalam AIDL. Membuat dua AIDL file 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 ditunjukkan dalam contoh berikut contoh:

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 pemanggil. Perlu diketahui bahwa Anda harus melakukannya dalam metode isReadyToPay, bukan di metode onBind.

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

Lihat Memverifikasi sertifikat penandatanganan penelepon untuk mengetahui caranya untuk memverifikasi bahwa paket panggilan memiliki tanda tangan yang benar.

Langkah 3: Izinkan pelanggan melakukan pembayaran

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

Aplikasi pembayaran merespons dengan methodName dan details, yang merupakan aplikasi pembayaran spesifik dan tidak transparan terhadap browser. Browser mengonversi details ke dalam objek JavaScript untuk penjual melalui deserialisasi JSON, tetapi tidak memberlakukan validitas apa pun di luar 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 akan mengidentifikasi ID metode pembayaran default untuk aplikasi.

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 valid, URL mutlak dengan skema HTTPS seperti yang ditunjukkan 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 tambahan Intent:

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

methodNames

Nama metode yang digunakan. Elemen-elemen tersebut adalah kunci dalam Kamus methodData. Ini 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 tingkat atas browser).

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

topLevelOrigin

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

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

topLevelCertificateChain

Rantai sertifikat penjual (Rantai sertifikat tingkat teratas konteks penjelajahan). Null untuk localhost dan file pada disk, yang keduanya aman tanpa adanya 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

Origin tanpa skema dari konteks penjelajahan iframe yang memanggil konstruktor new PaymentRequest(methodData, details, options) di JavaScript. Jika fungsi konstruktor dipanggil dari konteks tingkat atas, lalu nilai dari 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 adalah 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 "push-payment" aplikasi harus terkait dengan status transaksi. Situs penjual akan menggunakan kolom ini untuk mengkueri &quot;push-payment&quot; aplikasi untuk status transaksi out of 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 penjual untuk menyelesaikan transaksi. Jika berhasil true, maka details harus dibangun sedemikian rupa sehingga JSON.parse(details) akan berhasil.

Anda dapat meneruskan RESULT_CANCELED jika transaksi belum diselesaikan di aplikasi pembayaran, misalnya, jika pengguna gagal mengetikkan kode PIN yang benar untuk akun mereka di aplikasi pembayaran. Browser dapat membiarkan pengguna memilih aplikasi pembayaran lain.

setResult(RESULT_CANCELED)
finish()

Jika hasil aktivitas dari respons pembayaran diterima dari pembayaran yang dipanggil aplikasi disetel ke RESULT_OK, maka Chrome akan memeriksa methodName yang tidak kosong dan details dalam tambahannya. Jika validasi gagal, Chrome akan menampilkan penolakan promise dari request.show() dengan salah satu developer berikut mengalami error pesan:

'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 memiliki tanda tangan yang benar.

Langkah 4: Verifikasi sertifikat penandatanganan penelepon

Anda dapat memeriksa nama paket pemanggil dengan Binder.getCallingUid() di IS_READY_TO_PAY, dan dengan Activity.getCallingPackage() dalam PAY. Untuk benar-benar memverifikasi bahwa pemanggil adalah {i>browser<i} yang Anda inginkan, Anda harus periksa sertifikat penandatanganannya dan pastikan sertifikatnya cocok dengan dengan sejumlah nilai.

Jika Anda menargetkan API level 28 dan yang lebih tinggi, 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 satu sertifikat browser Anda, karena metode ini menangani rotasi sertifikat dengan benar. (Chrome memiliki sertifikat penandatanganan tunggal.) Aplikasi yang memiliki beberapa sertifikat penandatanganan tidak dapat memutarnya.

Jika Anda perlu mendukung level API 27 dan yang lebih lama, atau jika Anda perlu menangani {i>browser<i} yang memiliki 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) } }