คู่มือนักพัฒนาแอปสำหรับการชำระเงินใน Android

ดูวิธีปรับแอปการชำระเงิน Android ให้ทำงานร่วมกับการชำระเงินผ่านเว็บและมอบประสบการณ์การใช้งานที่ดีขึ้นให้แก่ลูกค้า

Payment Request API นำมาสู่อินเทอร์เฟซแบบเบราว์เซอร์ภายในเว็บที่ช่วยให้ผู้ใช้ป้อนข้อมูลการชำระเงินที่จำเป็นได้ง่ายกว่าที่เคย นอกจากนี้ API ยังเรียกใช้แอปการชำระเงินเฉพาะแพลตฟอร์มได้ด้วย

การรองรับเบราว์เซอร์

  • Chrome: 60
  • Edge: 15.
  • Firefox: อยู่หลังธง
  • Safari: 11.1

แหล่งที่มา

ขั้นตอนการชำระเงินด้วยแอป Google Pay เฉพาะแพลตฟอร์มที่ใช้การชำระเงินบนเว็บ

การชำระเงินผ่านเว็บช่วยให้ผสานรวมกับเบราว์เซอร์ ความปลอดภัย และประสบการณ์ของผู้ใช้ได้ดียิ่งขึ้นเมื่อเทียบกับการใช้ Intent ของ Android เพียงอย่างเดียว

  • แอปการชำระเงินจะเปิดขึ้นเป็นโมดัลในบริบทของเว็บไซต์ผู้ขาย
  • การติดตั้งใช้งานจะช่วยเสริมแอปการชำระเงินที่มีอยู่ ซึ่งจะช่วยให้คุณใช้ประโยชน์จากฐานผู้ใช้ได้
  • ระบบจะตรวจสอบลายเซ็นของแอปการชำระเงินเพื่อป้องกันการโหลดจากแหล่งที่ไม่รู้จัก
  • แอปการชำระเงินรองรับวิธีการชำระเงินได้หลายวิธี
  • คุณผสานรวมวิธีการชำระเงินใดก็ได้ เช่น คริปโตเคอเรนซี การโอนเงินผ่านธนาคาร และอื่นๆ แอปการชำระเงินในอุปกรณ์ Android ยังผสานรวมวิธีการที่จำเป็นต้องเข้าถึงชิปฮาร์ดแวร์ในอุปกรณ์ได้ด้วย

การติดตั้งใช้งานการชำระเงินผ่านเว็บในแอปการชำระเงิน Android ประกอบด้วย 4 ขั้นตอนดังนี้

  1. ช่วยให้ผู้ขายค้นพบแอปการชำระเงินของคุณ
  2. แจ้งให้ผู้ขายทราบว่าลูกค้ามีเครื่องมือที่ลงทะเบียน (เช่น บัตรเครดิต) ซึ่งพร้อมชำระเงินหรือไม่
  3. ให้ลูกค้าชำระเงิน
  4. ยืนยันใบรับรองการรับรองของผู้โทร

หากต้องการดูการทํางานของการชำระเงินผ่านเว็บ ให้ดูการสาธิต android-web-payment

ขั้นตอนที่ 1: ให้ผู้ขายค้นพบแอปการชำระเงินของคุณ

ผู้ขายต้องใช้ Payment Request API และระบุวิธีการชำระเงินที่คุณรองรับโดยใช้ตัวระบุวิธีการชำระเงินจึงจะใช้แอปการชำระเงินของคุณได้

หากมีตัวระบุวิธีการชำระเงินที่ไม่ซ้ำกันสำหรับแอปการชำระเงินของคุณ คุณจะตั้งค่าไฟล์ Manifest ของวิธีการชำระเงินของคุณเองเพื่อให้เบราว์เซอร์ค้นพบแอปได้

ขั้นตอนที่ 2: แจ้งให้ผู้ขายทราบหากลูกค้ามีเครื่องมือที่ลงทะเบียนไว้พร้อมชำระเงิน

ผู้ขายสามารถโทรไปที่ hasEnrolledInstrument() เพื่อสอบถามว่าลูกค้าสามารถชำระเงินได้หรือไม่ คุณสามารถใช้ IS_READY_TO_PAY เป็นบริการ Android เพื่อตอบการค้นหานี้ได้

AndroidManifest.xml

ประกาศบริการด้วยตัวกรอง Intent ที่มีการดำเนินการ 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>

คุณจะใช้บริการ IS_READY_TO_PAY หรือไม่ก็ได้ หากไม่มีตัวแฮนเดิล Intent ดังกล่าวในแอปการชำระเงิน เว็บเบราว์เซอร์จะถือว่าแอปชำระเงินได้เสมอ

AIDL

API สำหรับบริการ IS_READY_TO_PAY กำหนดไว้ใน AIDL สร้างไฟล์ AIDL 2 ไฟล์ที่มีเนื้อหาต่อไปนี้

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

การใช้ IsReadyToPayService

การใช้งาน IsReadyToPayService ที่ง่ายที่สุดแสดงอยู่ในตัวอย่างต่อไปนี้

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

คำตอบ

บริการสามารถส่งการตอบกลับผ่านเมธอด handleIsReadyToPay(Boolean)

callback?.handleIsReadyToPay(true)

สิทธิ์

คุณสามารถใช้ Binder.getCallingUid() เพื่อตรวจสอบว่าใครเป็นผู้โทร โปรดทราบว่าคุณต้องทำในเมธอด isReadyToPay ไม่ใช่เมธอด onBind

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

ดูยืนยันใบรับรองการรับรองของผู้โทรเกี่ยวกับวิธีตรวจสอบว่าแพ็กเกจการเรียกใช้มีลายเซ็นที่ถูกต้อง

ขั้นตอนที่ 3: ให้ลูกค้าชำระเงิน

ผู้ขายโทรหา show() เพื่อเปิดแอปการชำระเงินเพื่อให้ลูกค้าชำระเงินได้ ระบบจะเรียกใช้แอปการชำระเงินผ่าน Android Intent PAY ที่มีข้อมูลธุรกรรมในพารามิเตอร์ Intent

แอปการชำระเงินตอบกลับด้วย methodName และ details ซึ่งเป็นแอปการชำระเงินเฉพาะและไม่ชัดเจนสำหรับเบราว์เซอร์ เบราว์เซอร์จะแปลงสตริง details เป็นออบเจ็กต์ JavaScript สำหรับผู้ขายผ่านการดีซีเรียลไลซ์ JSON แต่ไม่บังคับใช้ความถูกต้องใดๆ นอกเหนือจากนั้น เบราว์เซอร์จะไม่แก้ไข details ระบบจะส่งค่าพารามิเตอร์ดังกล่าวไปยังผู้ขายโดยตรง

AndroidManifest.xml

กิจกรรมที่มีตัวกรอง Intent PAY ควรมีแท็ก <meta-data> ที่ระบุตัวระบุวิธีการชำระเงินเริ่มต้นสําหรับแอป

หากต้องการรองรับวิธีการชำระเงินหลายวิธี ให้เพิ่มแท็ก <meta-data> ที่มีทรัพยากร <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 ต้องเป็นรายการสตริง โดยแต่ละรายการต้องเป็น URL แบบสัมบูรณ์ที่ถูกต้องซึ่งมีรูปแบบ HTTPS ดังที่แสดงที่นี่

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

พารามิเตอร์

ระบบจะส่งพารามิเตอร์ต่อไปนี้ไปยังกิจกรรมเป็นข้อมูลเพิ่มเติมของ Intent

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

methodNames

ชื่อของวิธีที่ใช้ องค์ประกอบคือคีย์ในพจนานุกรม methodData ซึ่งแอปการชำระเงินรองรับวิธีเหล่านี้

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

methodData

การแมปจาก methodNames แต่ละรายการไปยัง methodData

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

merchantName

เนื้อหาของแท็ก <title> HTML ของหน้าชำระเงินของผู้ขาย (บริบทการท่องเว็บระดับบนสุดของเบราว์เซอร์)

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

topLevelOrigin

ต้นทางของผู้ขายที่ไม่มีรูปแบบ (ต้นทางที่ไม่มีรูปแบบของบริบทการท่องเว็บระดับบนสุด) ตัวอย่างเช่น https://mystore.com/checkout ถูกส่งผ่าน เป็น mystore.com

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

topLevelCertificateChain

กลุ่มใบรับรองของผู้ขาย (กลุ่มใบรับรองของบริบทการเรียกดูระดับบนสุด) Null สำหรับ localhost และไฟล์ในดิสก์ ซึ่งเป็นบริบทที่ปลอดภัยทั้งคู่โดยไม่มีใบรับรอง SSL Parcelable แต่ละรายการคือแพ็กเกจที่มีคีย์ certificate และค่าอาร์เรย์ไบต์

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

paymentRequestOrigin

ต้นทางที่ไม่มีรูปแบบของบริบทการท่องเว็บ iframe ที่เรียกใช้คอนสตรัคเตอร์ new PaymentRequest(methodData, details, options) ใน JavaScript หากมีการเรียกตัวสร้างจากบริบทระดับบนสุด ค่าของพารามิเตอร์นี้จะเท่ากับค่าของพารามิเตอร์ topLevelOrigin

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

total

สตริง JSON ที่แสดงจำนวนเงินรวมของธุรกรรม

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

ตัวอย่างเนื้อหาของสตริงมีดังนี้

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

modifiers

เอาต์พุตของ JSON.stringify(details.modifiers) โดยที่ details.modifiers มีเพียง supportedMethods และ total

paymentRequestId

ช่อง PaymentRequest.id ที่แอป "การชำระเงินแบบ Push" ควรเชื่อมโยงกับสถานะธุรกรรม เว็บไซต์ผู้ขายจะใช้ช่องนี้ในการค้นหาแอป "การชำระเงินแบบพุช" เพื่อดูสถานะของธุรกรรมนอกช่วง

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

คำตอบ

กิจกรรมสามารถส่งการตอบกลับกลับผ่าน setResult ด้วย RESULT_OK

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

คุณต้องระบุพารามิเตอร์ 2 รายการเป็นข้อมูลเพิ่มเติมของ Intent ดังนี้

  • methodName: ชื่อเมธอดที่ใช้
  • details: สตริง JSON ที่มีข้อมูลที่จําเป็นสําหรับผู้ขายในการทําธุรกรรมให้เสร็จสมบูรณ์ หากความสำเร็จคือ true ก็จะต้องสร้าง details ในลักษณะที่ JSON.parse(details) จะประสบความสำเร็จ

คุณสามารถส่ง RESULT_CANCELED ได้หากธุรกรรมยังไม่เสร็จสมบูรณ์ในแอปการชำระเงิน เช่น หากผู้ใช้พิมพ์รหัส PIN ที่ถูกต้องสำหรับบัญชีในแอปการชำระเงินไม่ได้ เบราว์เซอร์อาจอนุญาตให้ผู้ใช้เลือกแอปการชำระเงินอื่น

setResult(RESULT_CANCELED)
finish()

หากตั้งค่าผลลัพธ์กิจกรรมของการตอบกลับการชำระเงินที่ได้รับจากแอปการชำระเงินที่เรียกให้แสดงเป็น RESULT_OK แล้ว Chrome จะตรวจสอบว่า methodName และ details ในข้อมูลเพิ่มเติมไม่ใช่ค่าว่าง หากการตรวจสอบไม่สำเร็จ Chrome จะแสดงการปฏิเสธจาก request.show() พร้อมข้อความแสดงข้อผิดพลาดต่อไปนี้แก่นักพัฒนาแอป

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

สิทธิ์

กิจกรรมจะตรวจสอบผู้เรียกได้ด้วยเมธอด getCallingPackage()

val caller: String? = callingPackage

ขั้นตอนสุดท้ายคือการยืนยันใบรับรองการรับรองของผู้โทรเพื่อยืนยันว่าแพ็กเกจการโทรมีลายเซ็นที่ถูกต้อง

ขั้นตอนที่ 4: ยืนยันใบรับรองการรับรองของผู้โทร

คุณสามารถตรวจสอบชื่อแพ็กเกจของผู้โทรด้วย Binder.getCallingUid() ใน IS_READY_TO_PAY และ Activity.getCallingPackage() ใน PAY หากต้องการยืนยันว่าผู้เรียกใช้คือเบราว์เซอร์ที่คุณคิดไว้จริงๆ คุณควรตรวจสอบใบรับรองการรับรองและตรวจสอบว่าตรงกับค่าที่ถูกต้อง

หากคุณกําหนดเป้าหมาย API ระดับ 28 ขึ้นไปและผสานรวมกับเบราว์เซอร์ที่มีใบรับรองการรับรองใบเดียว คุณจะใช้ 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() กับเบราว์เซอร์ที่มีใบรับรองรายการเดียว เนื่องจากจัดการการหมุนเวียนใบรับรองได้อย่างถูกต้อง (Chrome มี ใบรับรองการลงชื่อเพียงครั้งเดียว) แอปที่มีใบรับรองการรับรองหลายรายการจะหมุนเวียนใบรับรองไม่ได้

หากต้องการรองรับ API ระดับเก่ากว่า 27 ขึ้นไป หรือหากต้องจัดการเบราว์เซอร์ที่มีใบรับรองการรับรองหลายรายการ คุณสามารถใช้ 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) } }