ดูวิธีปรับแอปการชำระเงิน 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 ที่มี Action 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 ที่ง่ายที่สุดแสดงไว้ในตัวอย่างต่อไปนี้
Kotlin
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
}
}
Java
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)
Kotlin
callback?.handleIsReadyToPay(true)
Java
if (callback != null) {
callback.handleIsReadyToPay(true);
}
สิทธิ์
คุณสามารถใช้ Binder.getCallingUid() เพื่อตรวจสอบผู้เรียก โปรดทราบว่าคุณ
ต้องดำเนินการนี้ในเมธอด isReadyToPay ไม่ใช่ในเมธอด onBind,
เนื่องจากระบบปฏิบัติการ Android สามารถแคชและใช้การเชื่อมต่อบริการซ้ำได้ ซึ่งจะไม่
ทริกเกอร์เมธอด onBind()
Kotlin
override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
try {
val untrustedPackageName = parameters?.getString("packageName")
val actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid())
// ...
Java
@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
packageNamemethodNamesmethodDatatopLevelOriginpaymentRequestOrigintopLevelCertificateChain
ระบบได้เพิ่ม packageName ใน Chrome 138 คุณต้องยืนยันพารามิเตอร์นี้กับ Binder.getCallingUid() ก่อนที่จะใช้ค่าของพารามิเตอร์ การยืนยันนี้มีความสำคัญเนื่องจากผู้เรียกควบคุม Bundle parameters ได้อย่างเต็มที่ ขณะที่ระบบปฏิบัติการ Android ควบคุม Binder.getCallingUid()
topLevelCertificateChain จะเป็น null ใน WebView และในเว็บไซต์ที่ไม่ใช่ HTTPS ซึ่งโดยทั่วไปจะใช้สำหรับการทดสอบในเครื่อง เช่น http://localhost
ขั้นตอนที่ 3: ช่วยให้ลูกค้าชำระเงินได้
ผู้ขายเรียกใช้ show() เพื่อ เปิดแอปการชำระเงิน
เพื่อให้ลูกค้าชำระเงินได้ ระบบจะเรียกใช้แอปการชำระเงินโดยใช้ Intent ของ Android PAY พร้อมข้อมูลธุรกรรมในพารามิเตอร์ Intent
แอปการชำระเงินจะตอบกลับด้วย methodName และ details ซึ่งเป็นข้อมูลเฉพาะของแอปการชำระเงินและเบราว์เซอร์ไม่สามารถเข้าถึงได้ เบราว์เซอร์จะแปลงสตริง details เป็น Dictionary 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 ต้องเป็นรายการสตริง ซึ่งแต่ละสตริงต้องเป็น Absolute 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
methodNamesmethodDatamerchantNametopLevelOrigintopLevelCertificateChainpaymentRequestOrigintotalmodifierspaymentRequestIdpaymentOptionsshippingOptions
Kotlin
val extras: Bundle? = getIntent()?.extras
Java
Bundle extras = getIntent() != null ? getIntent().getExtras() : null;
methodNames
ชื่อของวิธีการที่ใช้ องค์ประกอบต่างๆ คือคีย์ใน Dictionary methodData ซึ่งเป็นวิธีการที่แอปการชำระเงินรองรับ
Kotlin
val methodNames: List<String>? = extras.getStringArrayList("methodNames")
Java
List<String> methodNames = extras.getStringArrayList("methodNames");
methodData
การแมปจาก methodNames แต่ละรายการไปยัง
methodData
Kotlin
val methodData: Bundle? = extras.getBundle("methodData")
Java
Bundle methodData = extras.getBundle("methodData");
merchantName
เนื้อหาของแท็ก <title> HTML ของหน้าชำระเงินของผู้ขาย (บริบทการท่องเว็บระดับบนสุดของเบราว์เซอร์)
Kotlin
val merchantName: String? = extras.getString("merchantName")
Java
String merchantName = extras.getString("merchantName");
topLevelOrigin
ต้นทางของผู้ขายที่ไม่มีแผนการตรวจสอบสิทธิ์ (ต้นทางที่ไม่มีแผนการตรวจสอบสิทธิ์ของบริบทการท่องเว็บระดับบนสุด) เช่น ระบบจะส่ง https://mystore.com/checkout เป็น
mystore.com
Kotlin
val topLevelOrigin: String? = extras.getString("topLevelOrigin")
Java
String topLevelOrigin = extras.getString("topLevelOrigin");
topLevelCertificateChain
ชุดใบรับรองของผู้ขาย (ชุดใบรับรองของบริบทการท่องเว็บระดับบนสุด) ค่าจะเป็น null สำหรับ WebView, localhost หรือไฟล์ในดิสก์
Parcelable แต่ละรายการคือ Bundle ที่มีคีย์ certificate และค่าอาร์เรย์ไบต์
Kotlin
val topLevelCertificateChain: Array<Parcelable>? =
extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
(p as Bundle).getByteArray("certificate")
}
Java
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
Kotlin
val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")
Java
String paymentRequestOrigin = extras.getString("paymentRequestOrigin");
total
สตริง JSON ที่แสดงจำนวนเงินรวมของธุรกรรม
Kotlin
val total: String? = extras.getString("total")
Java
String total = extras.getString("total");
ตัวอย่างเนื้อหาของสตริง
{"currency":"USD","value":"25.00"}
modifiers
เอาต์พุตของ JSON.stringify(details.modifiers) โดยที่ details.modifiers
มีเพียง supportedMethods, data และ total
paymentRequestId
ฟิลด์ PaymentRequest.id ที่แอป "การชำระเงินแบบพุช" ควรเชื่อมโยงกับสถานะธุรกรรม เว็บไซต์ผู้ขายจะใช้ฟิลด์นี้เพื่อสอบถามแอป "การชำระเงินแบบพุช" เกี่ยวกับสถานะธุรกรรมนอกแบนด์
Kotlin
val paymentRequestId: String? = extras.getString("paymentRequestId")
Java
String paymentRequestId = extras.getString("paymentRequestId");
การตอบกลับ
กิจกรรมสามารถส่งการตอบกลับกลับมาผ่าน setResult ด้วย RESULT_OK
Kotlin
setResult(Activity.RESULT_OK, Intent().apply {
putExtra("methodName", "https://bobbucks.dev/pay")
putExtra("details", "{\"token\": \"put-some-data-here\"}")
})
finish()
Java
Intent result = new Intent();
Bundle extras = new Bundle();
extras.putString("methodName", "https://bobbucks.dev/pay");
extras.putString("details", "{\"token\": \"put-some-data-here\"}");
result.putExtras(extras);
setResult(Activity.RESULT_OK, result);
finish();
คุณต้องระบุพารามิเตอร์ 2 รายการเป็นส่วนเพิ่มเติมของ Intent ดังนี้
methodName: ชื่อของวิธีการที่ใช้details: สตริง JSON ที่มีข้อมูลที่จำเป็นเพื่อให้ผู้ขายทำธุรกรรมให้เสร็จสมบูรณ์ หาก success เป็นtrueคุณต้องสร้างdetailsในลักษณะที่JSON.parse(details)จะสำเร็จ หากไม่มีข้อมูลที่ต้องส่งคืน สตริงนี้จะเป็น"{}"ซึ่งเว็บไซต์ผู้ขายจะได้รับเป็น Dictionary JavaScript ว่างเปล่า
คุณสามารถส่ง RESULT_CANCELED หากผู้ใช้ยกเลิกธุรกรรมในแอปการชำระเงิน การดำเนินการดังกล่าวจะทำให้ request.show() ปฏิเสธด้วย AbortError ในเว็บไซต์ผู้ขาย ซึ่งบ่งบอกว่าผู้ใช้ยกเลิก
Kotlin
setResult(Activity.RESULT_CANCELED)
finish()
Java
setResult(Activity.RESULT_CANCELED);
finish();
Chrome 149 เป็นต้นไปรองรับค่าผลลัพธ์ต่อไปนี้
Kotlin
Activity.RESULT_CANCELED // 0 (0x00000000)
Activity.RESULT_OK // -1 (0xffffffff)
const val INTERNAL_PAYMENT_APP_ERROR = Activity.RESULT_FIRST_USER // 1 (0x00000001)
Java
Activity.RESULT_CANCELED // 0 (0x00000000)
Activity.RESULT_OK // -1 (0xffffffff)
static final int INTERNAL_PAYMENT_APP_ERROR = Activity.RESULT_FIRST_USER; // 1 (0x00000001)
หากแอปการชำระเงินล้มเหลวเนื่องจากข้อผิดพลาดภายใน คุณสามารถระบุได้โดยส่ง Activity.RESULT_FIRST_USER เป็นรหัสผลลัพธ์
หากระบบส่งคืน INTERNAL_PAYMENT_APP_ERROR request.show() จะปฏิเสธด้วย OperationError ในเว็บไซต์ผู้ขาย ซึ่งบ่งบอกว่ามีข้อผิดพลาดในแอปการชำระเงิน
ความแตกต่างระหว่าง RESULT_CANCELED (0) สำหรับการยกเลิกของผู้ใช้ ซึ่งทำให้เกิด AbortError และ INTERNAL_PAYMENT_APP_ERROR (1) สำหรับข้อผิดพลาดภายในแอป ซึ่งทำให้เกิด OperationError ช่วยให้ผู้ขายสร้างโฟลว์ผู้ใช้ได้ดียิ่งขึ้น
Kotlin
setResult(Activity.RESULT_FIRST_USER)
finish()
Java
setResult(Activity.RESULT_FIRST_USER);
finish();
หากตั้งค่าผลลัพธ์ของกิจกรรมของการตอบกลับการชำระเงินที่ได้รับจากแอปการชำระเงินที่เรียกใช้เป็น RESULT_OK Chrome จะตรวจสอบ methodName และ details ที่ไม่ว่างเปล่าในส่วนเพิ่มเติม หากการตรวจสอบไม่สำเร็จ Chrome จะส่งคืน Promise ที่ถูกปฏิเสธจาก request.show() พร้อมข้อความแสดงข้อผิดพลาดต่อไปนี้สำหรับนักพัฒนาแอป
'Payment app returned invalid response. Missing field "details".'
'Payment app returned invalid response. Missing field "methodName".'
สิทธิ์
กิจกรรมสามารถตรวจสอบผู้เรียกด้วยเมธอด getCallingPackage()
Kotlin
val caller: String? = callingPackage
Java
String caller = getCallingPackage();
ขั้นตอนสุดท้ายคือการยืนยันใบรับรองการลงนามของผู้เรียกเพื่อยืนยันว่าแพ็กเกจที่เรียกมีลายเซ็นที่ถูกต้อง
ขั้นตอนที่ 4: ยืนยันใบรับรองการลงนามของผู้เรียก
คุณสามารถตรวจสอบชื่อแพ็กเกจของผู้เรียกด้วย Binder.getCallingUid() ใน
IS_READY_TO_PAY และด้วย Activity.getCallingPackage() ใน PAY หากต้องการยืนยันว่าผู้เรียกเป็นเบราว์เซอร์ที่คุณต้องการจริงๆ คุณควรตรวจสอบใบรับรองการลงนามและตรวจสอบว่าตรงกับค่าที่ถูกต้อง
หากกำหนดเป้าหมายเป็น API ระดับ 28 ขึ้นไปและผสานรวมกับเบราว์เซอร์ที่มีใบรับรองการลงนามเดียว คุณสามารถใช้ PackageManager.hasSigningCertificate() ได้
Kotlin
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
)
Java
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 ได้
Kotlin
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)
Java
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