ดูวิธีปรับแอปการชำระเงิน Android ให้ทำงานร่วมกับการชำระเงินผ่านเว็บและมอบประสบการณ์การใช้งานที่ดีขึ้นให้แก่ลูกค้า
Payment Request API นำมาสู่อินเทอร์เฟซแบบเบราว์เซอร์ภายในเว็บที่ช่วยให้ผู้ใช้ป้อนข้อมูลการชำระเงินที่จำเป็นได้ง่ายกว่าที่เคย นอกจากนี้ API ยังเรียกใช้แอปการชำระเงินเฉพาะแพลตฟอร์มได้ด้วย
การชำระเงินผ่านเว็บช่วยให้ผสานรวมกับเบราว์เซอร์ ความปลอดภัย และประสบการณ์ของผู้ใช้ได้ดียิ่งขึ้นเมื่อเทียบกับการใช้ Intent ของ Android เพียงอย่างเดียว
- แอปการชำระเงินจะเปิดขึ้นเป็นโมดัลในบริบทของเว็บไซต์ผู้ขาย
- การติดตั้งใช้งานจะช่วยเสริมแอปการชำระเงินที่มีอยู่ ซึ่งจะช่วยให้คุณใช้ประโยชน์จากฐานผู้ใช้ได้
- ระบบจะตรวจสอบลายเซ็นของแอปการชำระเงินเพื่อป้องกันการโหลดจากแหล่งที่ไม่รู้จัก
- แอปการชำระเงินรองรับวิธีการชำระเงินได้หลายวิธี
- คุณผสานรวมวิธีการชำระเงินใดก็ได้ เช่น คริปโตเคอเรนซี การโอนเงินผ่านธนาคาร และอื่นๆ แอปการชำระเงินในอุปกรณ์ Android ยังผสานรวมวิธีการที่จำเป็นต้องเข้าถึงชิปฮาร์ดแวร์ในอุปกรณ์ได้ด้วย
การติดตั้งใช้งานการชำระเงินผ่านเว็บในแอปการชำระเงิน Android ประกอบด้วย 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) } }