ดูวิธีปรับแอปการชำระเงิน Android ให้ทำงานร่วมกับการชำระเงินผ่านเว็บและมอบประสบการณ์การใช้งานที่ดีขึ้นให้แก่ลูกค้า
เผยแพร่: 5 พฤษภาคม 2020 อัปเดตล่าสุด: 27 พฤษภาคม 2025
Payment Request API จะนำอินเทอร์เฟซแบบเบราว์เซอร์ในตัวมาสู่เว็บ ซึ่งช่วยให้ผู้ใช้ป้อนข้อมูลการชำระเงินที่จำเป็นได้ง่ายกว่าที่เคย นอกจากนี้ API ยังเรียกใช้แอปการชำระเงินเฉพาะแพลตฟอร์มได้ด้วย
การชำระเงินผ่านเว็บช่วยให้ผสานรวมกับเบราว์เซอร์ ความปลอดภัย และประสบการณ์ของผู้ใช้ได้ดียิ่งขึ้นเมื่อเทียบกับการใช้ Intent ของ Android เพียงอย่างเดียว
- แอปการชำระเงินจะเปิดขึ้นเป็นโมดัลในบริบทของเว็บไซต์ผู้ขาย
- การติดตั้งใช้งานเป็นส่วนเสริมของแอปการชำระเงินที่มีอยู่ ซึ่งจะช่วยให้คุณใช้ประโยชน์จากฐานผู้ใช้ได้
- ระบบจะตรวจสอบลายเซ็นของแอปการชำระเงินเพื่อป้องกันการโหลดจากแหล่งที่ไม่รู้จัก
- แอปการชำระเงินรองรับวิธีการชำระเงินได้หลายวิธี
- คุณสามารถผสานรวมวิธีการชำระเงินใดก็ได้ เช่น คริปโตเคอเรนซี การโอนเงินผ่านธนาคาร และอื่นๆ แอปการชำระเงินในอุปกรณ์ Android ยังผสานรวมวิธีการที่จำเป็นต้องเข้าถึงชิปฮาร์ดแวร์ในอุปกรณ์ได้ด้วย
การติดตั้งใช้งานการชำระเงินผ่านเว็บในแอปการชำระเงิน Android ประกอบด้วย 4 ขั้นตอนดังนี้
- ให้ผู้ขายค้นพบแอปการชำระเงินของคุณ
- แจ้งให้ผู้ขายทราบว่าลูกค้ามีเครื่องมือที่ลงทะเบียน (เช่น บัตรเครดิต) ซึ่งพร้อมชำระเงินหรือไม่
- ให้ลูกค้าชำระเงิน
- ยืนยันใบรับรองการรับรองของผู้โทร
หากต้องการดูการทํางานของการชำระเงินผ่านเว็บ ให้ดูการสาธิต android-web-payment
ขั้นตอนที่ 1: ให้ผู้ขายค้นพบแอปการชำระเงินของคุณ
ตั้งค่าพร็อพเพอร์ตี้ related_applications
ในไฟล์ Manifest ของเว็บแอปตามวิธีการในการตั้งค่าวิธีการชำระเงิน
ผู้ขายต้องใช้ 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 ไฟล์ที่มีเนื้อหาต่อไปนี้
org/chromium/IsReadyToPayServiceCallback.aidl
package org.chromium;
interface IsReadyToPayServiceCallback {
oneway void handleIsReadyToPay(boolean isReadyToPay);
}
org/chromium/IsReadyToPayService.aidl
package org.chromium;
import org.chromium.IsReadyToPayServiceCallback;
interface IsReadyToPayService {
oneway void isReadyToPay(IsReadyToPayServiceCallback callback, in Bundle parameters);
}
การใช้ IsReadyToPayService
การใช้งาน IsReadyToPayService
ที่ง่ายที่สุดแสดงอยู่ในตัวอย่างต่อไปนี้
class SampleIsReadyToPayService : Service() {
private val binder = object : IsReadyToPayService.Stub() {
override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
callback?.handleIsReadyToPay(true)
}
}
override fun onBind(intent: Intent?): IBinder? {
return binder
}
}
import org.chromium.IsReadyToPayService;
public class SampleIsReadyToPayService extends Service {
private final IsReadyToPayService.Stub mBinder =
new IsReadyToPayService.Stub() {
@Override
public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
if (callback != null) {
callback.handleIsReadyToPay(true);
}
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
การตอบกลับ
บริการสามารถส่งการตอบกลับได้โดยใช้เมธอด handleIsReadyToPay(Boolean)
callback?.handleIsReadyToPay(true)
if (callback != null) {
callback.handleIsReadyToPay(true);
}
สิทธิ์
คุณสามารถใช้ Binder.getCallingUid()
เพื่อตรวจสอบว่าใครเป็นผู้โทร โปรดทราบว่าคุณต้องดำเนินการนี้ในเมธอด isReadyToPay
ไม่ใช่เมธอด onBind
เนื่องจากระบบปฏิบัติการ Android สามารถแคชและนำการเชื่อมต่อบริการกลับมาใช้ใหม่ได้ ซึ่งจะไม่ทริกเกอร์เมธอด onBind()
override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
try {
val untrustedPackageName = parameters?.getString("packageName")
val actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid())
// ...
@Override
public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
try {
String untrustedPackageName = parameters != null
? parameters.getString("packageName")
: null;
String[] actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid());
// ...
ตรวจสอบพารามิเตอร์อินพุตสำหรับ null
เสมอเมื่อได้รับการเรียกใช้การสื่อสารระหว่างกระบวนการ (IPC) ซึ่งสำคัญอย่างยิ่งเนื่องจากระบบปฏิบัติการ Android เวอร์ชันต่างๆ หรือเวอร์ชันที่แยกออกมาอาจทำงานในลักษณะที่ไม่คาดคิดและทำให้เกิดข้อผิดพลาดได้หากไม่จัดการ
แม้ว่าโดยทั่วไปแล้ว packageManager.getPackagesForUid()
จะแสดงผลองค์ประกอบเดียว แต่โค้ดของคุณต้องจัดการกับสถานการณ์ที่พบไม่บ่อยนักซึ่งผู้เรียกใช้ใช้ชื่อแพ็กเกจหลายรายการ ซึ่งจะช่วยให้แอปพลิเคชันของคุณยังคงมีประสิทธิภาพ
ดูยืนยันใบรับรองการรับรองของผู้โทรเกี่ยวกับวิธีตรวจสอบว่าแพ็กเกจการเรียกใช้มีลายเซ็นที่ถูกต้อง
พารามิเตอร์
เพิ่ม parameters
Bundle ใน Chrome 139 แล้ว ควรตรวจสอบกับ null
เสมอ
ระบบจะส่งพารามิเตอร์ต่อไปนี้ไปยังบริการใน parameters
Bundle
packageName
methodNames
methodData
topLevelOrigin
paymentRequestOrigin
topLevelCertificateChain
packageName
เพิ่มเข้ามาใน Chrome 138 คุณต้องยืนยันพารามิเตอร์นี้กับ Binder.getCallingUid()
ก่อนใช้ค่า การยืนยันนี้มีความจําเป็นเนื่องจากผู้ใช้จะควบคุมแพ็กเกจ parameters
ได้อย่างเต็มที่ ในขณะที่ระบบปฏิบัติการ Android จะควบคุม Binder.getCallingUid()
topLevelCertificateChain
คือ null
ใน WebView และในเว็บไซต์ที่ไม่ใช่ https ซึ่งมักใช้สำหรับการทดสอบในเครื่อง เช่น http://localhost
ขั้นตอนที่ 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/chromium_payment_method_names" />
</activity>
android:resource
ต้องเป็นรายการสตริง โดยแต่ละรายการต้องเป็น URL แบบสัมบูรณ์ที่ถูกต้องซึ่งมีรูปแบบ HTTPS ดังที่แสดงที่นี่
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="chromium_payment_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
merchantName
topLevelOrigin
topLevelCertificateChain
paymentRequestOrigin
total
modifiers
paymentRequestId
paymentOptions
shippingOptions
val extras: Bundle? = getIntent()?.extras
Bundle extras = getIntent() != null ? getIntent().getExtras() : null;
methodNames
ชื่อของเมธอดที่ใช้ องค์ประกอบคือคีย์ในพจนานุกรม methodData
ซึ่งแอปการชำระเงินรองรับวิธีเหล่านี้
val methodNames: List<String>? = extras.getStringArrayList("methodNames")
List<String> methodNames = extras.getStringArrayList("methodNames");
methodData
การแมปจาก methodNames
แต่ละรายการไปยัง methodData
val methodData: Bundle? = extras.getBundle("methodData")
Bundle methodData = extras.getBundle("methodData");
merchantName
เนื้อหาของแท็ก <title>
HTML ของหน้าชำระเงินของผู้ขาย (บริบทการท่องเว็บระดับบนสุดของเบราว์เซอร์)
val merchantName: String? = extras.getString("merchantName")
String merchantName = extras.getString("merchantName");
topLevelOrigin
ต้นทางของผู้ขายที่ไม่มีรูปแบบ (ต้นทางที่ไม่มีรูปแบบของบริบทการท่องเว็บระดับบนสุด) เช่น https://mystore.com/checkout
จะส่งเป็น mystore.com
val topLevelOrigin: String? = extras.getString("topLevelOrigin")
String topLevelOrigin = extras.getString("topLevelOrigin");
topLevelCertificateChain
เชนใบรับรองของผู้ขาย (เชนใบรับรองของบริบทการท่องเว็บระดับบนสุด) ค่าคือ null
สำหรับ WebView, localhost หรือไฟล์บนดิสก์
Parcelable
แต่ละรายการคือ Bundle ที่มีคีย์ certificate
และค่าอาร์เรย์ไบต์
val topLevelCertificateChain: Array<Parcelable>? =
extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
(p as Bundle).getByteArray("certificate")
}
Parcelable[] topLevelCertificateChain =
extras.getParcelableArray("topLevelCertificateChain");
if (topLevelCertificateChain != null) {
for (Parcelable p : topLevelCertificateChain) {
if (p != null && p instanceof Bundle) {
((Bundle) p).getByteArray("certificate");
}
}
}
paymentRequestOrigin
ต้นทางที่ไม่มีรูปแบบของบริบทการท่องเว็บ iframe ที่เรียกใช้คอนสตรัคเตอร์ new
PaymentRequest(methodData, details, options)
ใน JavaScript หากเรียกใช้คอนสตรคเตอร์จากบริบทระดับบนสุด ค่าของพารามิเตอร์นี้จะเท่ากับค่าของพารามิเตอร์ topLevelOrigin
val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")
String paymentRequestOrigin = extras.getString("paymentRequestOrigin");
total
สตริง JSON ที่แสดงยอดเงินรวมของธุรกรรม
val total: String? = extras.getString("total")
String total = extras.getString("total");
ตัวอย่างเนื้อหาของสตริงมีดังนี้
{"currency":"USD","value":"25.00"}
modifiers
เอาต์พุตของ JSON.stringify(details.modifiers)
โดยที่ details.modifiers
มีเฉพาะ supportedMethods
, data
และ total
paymentRequestId
ช่อง PaymentRequest.id
ที่แอป "การชำระเงินแบบ Push" ควรเชื่อมโยงกับสถานะธุรกรรม เว็บไซต์ของผู้ขายจะใช้ช่องนี้เพื่อค้นหาสถานะธุรกรรมนอกแบนด์ในแอป "การชำระเงินแบบ Push"
val paymentRequestId: String? = extras.getString("paymentRequestId")
String paymentRequestId = 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()
Intent result = new Intent();
Bundle extras = new Bundle();
extras.putString("methodName", "https://bobbucks.dev/pay");
extras.putString("instrumentDetails", "{\"token\": \"put-some-data-here\"}");
result.putExtras(extras);
setResult(Activity.RESULT_OK, result);
finish();
คุณต้องระบุพารามิเตอร์ 2 รายการเป็นข้อมูลเพิ่มเติมของ Intent ดังนี้
methodName
: ชื่อเมธอดที่ใช้details
: สตริง JSON ที่มีข้อมูลที่จําเป็นสําหรับผู้ขายในการทําธุรกรรมให้เสร็จสมบูรณ์ หากความสําเร็จคือtrue
details
ต้องสร้างขึ้นในลักษณะที่JSON.parse(details)
จะประสบความสําเร็จ หากไม่มีข้อมูลที่ต้องแสดงผล ให้ใช้สตริงเป็น"{}"
ซึ่งเว็บไซต์ของผู้ขายจะได้รับเป็นพจนานุกรม JavaScript ว่าง
คุณสามารถส่ง RESULT_CANCELED
ได้หากธุรกรรมยังไม่เสร็จสมบูรณ์ในแอปการชำระเงิน เช่น หากผู้ใช้พิมพ์รหัส PIN ที่ถูกต้องสำหรับบัญชีในแอปการชำระเงินไม่ได้ เบราว์เซอร์อาจอนุญาตให้ผู้ใช้เลือกแอปการชำระเงินอื่น
setResult(Activity.RESULT_CANCELED)
finish()
setResult(Activity.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
String caller = getCallingPackage();
ขั้นตอนสุดท้ายคือการยืนยันใบรับรองการรับรองของผู้โทรเพื่อยืนยันว่าแพ็กเกจการโทรมีลายเซ็นที่ถูกต้อง
ขั้นตอนที่ 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
)
String packageName = … // The caller's package name
byte[] certificate = … // The correct signing certificate
boolean 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 expected: Set<String> = … // The correct set of signing certificates
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val sha256 = MessageDigest.getInstance("SHA-256")
val actual = packageInfo.signatures.map {
SerializeByteArrayToString(sha256.digest(it.toByteArray()))
}
val verified = actual.equals(expected)
String packageName = … // The caller's package name
Set<String> expected = … // The correct set of signing certificates
PackageInfo packageInfo =
packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
Set<String> actual = new HashSet<>();
for (Signature it : packageInfo.signatures) {
actual.add(SerializeByteArrayToString(sha256.digest(it.toByteArray())));
}
boolean verified = actual.equals(expected);
แก้ไขข้อบกพร่อง
ใช้คําสั่งต่อไปนี้เพื่อดูข้อผิดพลาดหรือข้อความที่ให้ข้อมูล
adb logcat | grep -i pay