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