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