瞭解如何調整 Android 付款應用程式,使其支援網路付款功能,為顧客提供更優質的使用者體驗。
發布日期:2020 年 5 月 5 日,上次更新日期:2025 年 5 月 27 日
付款要求 API 為網頁帶來內建的瀏覽器介面,讓使用者能以前所未有的輕鬆方式輸入必要付款資訊。這個 API 也可叫用平台專屬的付款應用程式。
相較於僅使用 Android Intent,Web Payments 可與瀏覽器、安全性和使用者體驗更完美整合:
- 系統會以強制回應的形式啟動付款應用程式,並顯示商家網站的相關資訊。
- 這項功能是現有付款應用程式的補充功能,可協助您充分運用使用者群。
- 系統會檢查付款應用程式的簽章,以防範側載。
- 付款應用程式可以支援多種付款方式。
- 任何付款方式 (例如加密貨幣、銀行轉帳等) 都可以整合。Android 裝置上的付款應用程式甚至可以整合需要存取裝置硬體晶片的方法。
在 Android 付款應用程式中導入 Web Payments 需完成四個步驟:
- 讓商家發掘您的付款應用程式。
- 讓商家知道顧客是否已註冊付款方式 (例如信用卡),可隨時付款。
- 讓顧客付款。
- 驗證呼叫者的簽署憑證。
如要查看 Web Payments 的實際運作情形,請參閱 android-web-payment 示範。
步驟 1:讓商家發現你的付款應用程式
根據「設定付款方式」中的說明,在網頁應用程式資訊清單中設定 related_applications 屬性。
商家必須使用 PaymentRequest API,並透過付款方式 ID 指定您支援的付款方式,才能使用您的付款應用程式。
如果您有專屬的付款應用程式付款方式 ID,可以設定自己的付款方式資訊清單,讓瀏覽器探索您的應用程式。
步驟 2:告知商家顧客是否已註冊付款方式,並準備付款
商家可以呼叫 hasEnrolledInstrument(),查詢顧客是否能夠付款。您可以將 IS_READY_TO_PAY 實作為 Android 服務,以回答這項查詢。
AndroidManifest.xml
使用意圖篩選器宣告服務,並將動作設為 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 服務為選用項目。如果付款應用程式中沒有這類意圖處理常式,網頁瀏覽器會假設應用程式一律可以付款。
AIDL
IS_READY_TO_PAY 服務的 API 是在 AIDL 中定義。建立兩個 AIDL 檔案,並加入以下內容:
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());
// ...
接收處理序間通訊 (IPC) 呼叫時,請務必檢查 null 的輸入參數。這點尤其重要,因為不同版本或分支版本的 Android 作業系統可能會以非預期的方式運作,如果未妥善處理,可能會導致錯誤。
雖然 packageManager.getPackagesForUid() 通常會傳回單一元素,但您的程式碼必須處理呼叫端使用多個套件名稱的罕見情況。確保應用程式維持穩定。
如要瞭解如何驗證呼叫套件是否具有正確簽章,請參閱「驗證呼叫端的簽章憑證」。
參數
parameters Bundle 已在 Chrome 139 中新增。這個值一律應根據 null 檢查。
下列參數會傳遞至 parameters 組合中的服務:
packageNamemethodNamesmethodDatatopLevelOriginpaymentRequestOrigintopLevelCertificateChain
packageName 是 Chrome 138 版新增的功能。您必須先根據 Binder.getCallingUid() 驗證這個參數,才能使用其值。這項驗證至關重要,因為 parameters 套件完全由呼叫端控制,而 Binder.getCallingUid() 則由 Android 作業系統控制。
在 WebView 和非 HTTPS 網站上,topLevelCertificateChain 為 null,通常用於本機測試,例如 http://localhost。
步驟 3:讓顧客付款
商家會呼叫 show() 啟動付款應用程式,讓消費者付款。系統會使用 Android 意圖 PAY 叫用付款應用程式,意圖參數中包含交易資訊。
付款應用程式會傳回 methodName 和 details,這些是付款應用程式專屬的項目,瀏覽器無法辨識。瀏覽器會使用 JSON 字串還原序列化,將字串轉換為商家適用的 JavaScript 字典,但不會強制執行任何有效性。details瀏覽器不會修改 details,該參數的值會直接傳遞給商家。
AndroidManifest.xml
具有 PAY 意圖篩選器的活動應包含 <meta-data> 標記,用來識別應用程式的預設付款方式 ID。
如要支援多種付款方式,請新增含有 <string-array> 資源的 <meta-data> 標記。
<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 必須是字串清單,每個字串都必須是有效的絕對網址,且採用 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
所用方法的名稱。這些元素是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
商家的憑證鏈結 (頂層瀏覽環境的憑證鏈結)。如果是 WebView、localhost 或磁碟上的檔案,值為 null。
每個 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
在 JavaScript 中叫用 new
PaymentRequest(methodData, details, options) 建構函式的 iframe 瀏覽環境,其不含結構定義的來源。如果建構函式是從頂層環境叫用,則這個參數的值會等於 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();
您必須指定兩個參數做為 Intent 額外資訊:
methodName:所用方法的名稱。details:包含商家完成交易所需資訊的 JSON 字串。如果成功為true,則details的建構方式必須能確保JSON.parse(details)成功。如果沒有需要傳回的資料,這個字串可以是"{}",商家網站會收到空白的 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,導致 AbortError) 和內部應用程式錯誤 (INTERNAL_PAYMENT_APP_ERROR,導致 OperationError),有助於商家建構更完善的使用者流程。
Kotlin
setResult(Activity.RESULT_FIRST_USER)
finish()
Java
setResult(Activity.RESULT_FIRST_USER);
finish();
如果從叫用的付款應用程式收到的付款回應活動結果設為 RESULT_OK,Chrome 會檢查其 extras 中是否有非空白的 methodName 和 details。如果驗證失敗,Chrome 會從 request.show() 傳回遭拒的 Promise,並顯示下列其中一個開發人員錯誤訊息:
'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:驗證呼叫端的簽署憑證
您可以在 IS_READY_TO_PAY 中使用 Binder.getCallingUid(),以及在 PAY 中使用 Activity.getCallingPackage(),檢查呼叫端的套件名稱。如要實際驗證呼叫端是否為您預期的瀏覽器,請檢查其簽署憑證,並確認憑證與正確值相符。
如果您指定 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