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