Android 付款應用程式開發人員指南

瞭解如何調整 Android 付款應用程式,讓其與網路支付服務搭配運作,並為客戶提供更優質的使用者體驗。

發布日期:2020 年 5 月 5 日,上次更新日期:2025 年 5 月 27 日

Payment Request API 為網頁提供內建的瀏覽器介面,讓使用者更輕鬆地輸入必要的付款資訊。此 API 也可以叫用特定平台的付款應用程式。

Browser Support

  • Chrome: 60.
  • Edge: 15.
  • Firefox: behind a flag.
  • Safari: 11.1.

Source

使用採用網路支付的平台專屬 Google Pay 應用程式,進行結帳流程。

與僅使用 Android Intent 相比,Web Payments 可提供更佳的瀏覽器整合、安全性和使用者體驗:

  • 付款應用程式會以模態視窗形式在商家網站的內容中啟動。
  • 導入這項功能可補足現有付款應用程式,讓您充分運用使用者群。
  • 系統會檢查付款應用程式的簽章,以防側載
  • 付款應用程式可支援多種付款方式。
  • 您可以整合任何付款方式,例如加密貨幣、銀行轉帳等。Android 裝置上的付款應用程式甚至可以整合需要存取裝置硬體晶片的方法。

在 Android 付款應用程式中導入網路支付功能,需要四個步驟:

  1. 讓商家發現您的付款應用程式。
  2. 請向商家說明,客戶是否已註冊可用於付款的付款工具 (例如信用卡)。
  3. 讓客戶付款。
  4. 驗證呼叫者的簽署憑證。

如要瞭解 Web Payments 的實際運作情形,請查看 android-web-payment 示範。

步驟 1:讓商家發現您的付款應用程式

按照「設定付款方式」一文的說明,在 Web 應用程式資訊清單中設定 related_applications 屬性。

商家必須使用 Payment Request 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 最簡單的實作方式:

KotlinJava
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) 方法傳送回應。

KotlinJava
callback?.handleIsReadyToPay(true)
if (callback != null) {
    callback.handleIsReadyToPay(true);
}

權限

您可以使用 Binder.getCallingUid() 檢查調用者的身分。請注意,您必須在 isReadyToPay 方法中執行這項操作,而非在 onBind 方法中,因為 Android OS 可以快取及重複使用服務連線,而不會觸發 onBind() 方法。

KotlinJava
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());
        // ...

接收處理序間通訊 (IPC) 呼叫時,請務必檢查 null 的輸入參數。這點尤其重要,因為 Android 作業系統的不同版本或分支版本可能會以非預期的方式運作,如果未妥善處理,可能會導致錯誤。

雖然 packageManager.getPackagesForUid() 通常會傳回單一元素,但您的程式碼必須處理呼叫端使用多個套件名稱的罕見情況。這可確保應用程式保持穩定。

如要瞭解如何驗證呼叫套件是否具有正確的簽章,請參閱「驗證呼叫端的簽署憑證」。

參數

parameters 套件已在 Chrome 139 中新增。應一律與 null 進行檢查。

系統會將下列參數傳遞至 parameters 套件中的服務:

  • packageName
  • methodNames
  • methodData
  • topLevelOrigin
  • paymentRequestOrigin
  • topLevelCertificateChain

packageName 是在 Chrome 138 中新增的。您必須先驗證這個參數是否符合 Binder.getCallingUid(),才能使用其值。這項驗證程序非常重要,因為 parameters 組合由呼叫端完全控制,而 Binder.getCallingUid() 則由 Android OS 控制。

topLevelCertificateChain 在 WebView 和通常用於本機測試的非 https 網站 (例如 http://localhost) 中為 null

步驟 3:讓客戶付款

商家會呼叫 show()啟動付款應用程式,讓消費者付款。系統會使用 Android 意圖 PAY 叫用付款應用程式,並在意圖參數中提供交易資訊。

付款應用程式會回傳 methodNamedetails,這兩者是付款應用程式專屬,對瀏覽器而言是不可見的。瀏覽器會使用 JSON 字串反序列化功能,將 details 字串轉換為商家的 JavaScript 字典,但不會強制執行任何有效性。瀏覽器不會修改 details;該參數的值會直接傳遞給商家。

AndroidManifest.xml

使用 PAY 意圖篩選器的活動應包含 <meta-data> 標記,用於識別應用程式的預設付款方式識別碼

如要支援多種付款方式,請新增含有 <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 額外項目的形式傳遞至活動:

  • methodNames
  • methodData
  • merchantName
  • topLevelOrigin
  • topLevelCertificateChain
  • paymentRequestOrigin
  • total
  • modifiers
  • paymentRequestId
  • paymentOptions
  • shippingOptions

KotlinJava
val extras: Bundle? = getIntent()?.extras
Bundle extras = getIntent() != null ? getIntent().getExtras() : null;

methodNames

所用方法的名稱。元素是 methodData 字典中的鍵。這些是付款應用程式支援的方法。

KotlinJava
val methodNames: List<String>? = extras.getStringArrayList("methodNames")
List<String> methodNames = extras.getStringArrayList("methodNames");

methodData

從每個 methodNames 對應至 methodData

KotlinJava
val methodData: Bundle? = extras.getBundle("methodData")
Bundle methodData = extras.getBundle("methodData");

merchantName

商家結帳頁面 (瀏覽器的頂層瀏覽情境) 的 <title> HTML 標記內容。

KotlinJava
val merchantName: String? = extras.getString("merchantName")
String merchantName = extras.getString("merchantName");

topLevelOrigin

商家的來源,不含架構 (頂層瀏覽內容的無架構來源)。例如,https://mystore.com/checkout 會以 mystore.com 的形式傳遞。

KotlinJava
val topLevelOrigin: String? = extras.getString("topLevelOrigin")
String topLevelOrigin = extras.getString("topLevelOrigin");

topLevelCertificateChain

商家的憑證鏈結 (頂層瀏覽內容的憑證鏈結)。如果是 WebView、本機主機或磁碟上的檔案,值為 null。每個 Parcelable 都是含有 certificate 鍵和位元組陣列值的 Bundle。

KotlinJava
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

在 JavaScript 中叫用 new PaymentRequest(methodData, details, options) 建構函式的 iframe 瀏覽內容的無配置文件來源。如果建構函式是從頂層內容叫用,則此參數的值會等於 topLevelOrigin 參數的值。

KotlinJava
val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")
String paymentRequestOrigin = extras.getString("paymentRequestOrigin");

total

代表交易總金額的 JSON 字串。

KotlinJava
val total: String? = extras.getString("total")
String total = extras.getString("total");

以下是字串的內容範例:

{"currency":"USD","value":"25.00"}

modifiers

JSON.stringify(details.modifiers) 的輸出內容,其中 details.modifiers 只包含 supportedMethodsdatatotal

paymentRequestId

「推送付款」應用程式應與交易狀態建立關聯的 PaymentRequest.id 欄位。商家網站會使用這個欄位,針對交易狀態在外部管道查詢「推送付款」應用程式。

KotlinJava
val paymentRequestId: String? = extras.getString("paymentRequestId")
String paymentRequestId = extras.getString("paymentRequestId");

回應

活動可以透過 setResultRESULT_OK 傳回回應。

KotlinJava
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("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 字典。

如果付款應用程式未完成交易 (例如使用者無法在付款應用程式中輸入正確的帳戶 PIN 碼),您可以傳遞 RESULT_CANCELED。瀏覽器可能會讓使用者選擇其他付款應用程式。

KotlinJava
setResult(Activity.RESULT_CANCELED)
finish()
setResult(Activity.RESULT_CANCELED);
finish();

如果從叫用的付款應用程式收到的付款回應活動結果設為 RESULT_OK,Chrome 會檢查其額外資料中是否有非空的 methodNamedetails。如果驗證失敗,Chrome 會從 request.show() 傳回已遭拒絕的承諾,並顯示下列其中一個開發人員錯誤訊息:

'Payment app returned invalid response. Missing field "details".'
'Payment app returned invalid response. Missing field "methodName".'

權限

活動可以使用其 getCallingPackage() 方法檢查呼叫端。

KotlinJava
val caller: String? = callingPackage
String caller = getCallingPackage();

最後一個步驟是驗證呼叫端的簽署憑證,確認呼叫套件具有正確的簽名。

步驟 4:驗證呼叫端的簽署憑證

您可以使用 IS_READY_TO_PAY 中的 Binder.getCallingUid(),以及 PAY 中的 Activity.getCallingPackage() 檢查呼叫端的套件名稱。如要實際驗證呼叫端是否為您要使用的瀏覽器,請檢查其簽署憑證,並確認該憑證與正確值相符。

如果您指定的 API 級別為 28 以上,且與具有單一簽署憑證的瀏覽器整合,則可以使用 PackageManager.hasSigningCertificate()

KotlinJava
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

KotlinJava
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