Android 決済アプリを Web Payments に対応させ、ユーザー エクスペリエンスを向上させる方法について説明します。
公開日: 2020 年 5 月 5 日、最終更新日: 2025 年 5 月 27 日
Payment Request API は、ウェブに組み込みのブラウザベースのインターフェースを提供します。これにより、ユーザーはこれまで以上に簡単に必要な支払い情報を入力できます。この API は、プラットフォーム固有の支払いアプリを呼び出すこともできます。
Android インテントのみを使用する場合と比較して、ウェブ決済ではブラウザとの統合、セキュリティ、ユーザー エクスペリエンスが向上します。
- 支払いアプリは、販売者のウェブサイトのコンテキストでモーダルとして起動されます。
- 実装は既存の決済アプリを補完するもので、ユーザーベースを活用できます。
- 支払いアプリの署名がチェックされ、サイドローディングが防止されます。
- 決済アプリは複数のお支払い方法をサポートできます。
- 暗号通貨や銀行振込など、あらゆるお支払い方法を統合できます。Android デバイスの決済アプリは、デバイスのハードウェア チップへのアクセスを必要とする方法を統合することもできます。
Android の支払いアプリに Web Payments を実装するには、次の 4 つのステップが必要です。
- 販売者がお支払いアプリを見つけられるようにします。
- お客様が登録済みのお支払い方法(クレジット カードなど)で支払う準備ができているかどうかを販売者に知らせます。
- お客様にお支払いいただく。
- 発信者の署名証明書を確認します。
ウェブ決済の動作を確認するには、android-web-payment デモをご覧ください。
ステップ 1: 加盟店が支払いアプリを見つけられるようにする
お支払い方法の設定の手順に沿って、ウェブアプリ マニフェストで related_applications プロパティを設定します。
販売者がお支払いアプリを使用するには、Payment Request API を使用し、お支払い方法識別子を使用してサポートするお支払い方法を指定する必要があります。
お支払いアプリに固有のお支払い方法識別子がある場合は、ブラウザがアプリを検出できるように、独自のお支払い方法マニフェストを設定できます。
ステップ 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 ファイルを 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() を使用して、呼び出し元を確認できます。Android OS はサービス接続をキャッシュに保存して再利用できるため、onBind() メソッドはトリガーされません。そのため、この処理は onBind メソッドではなく isReadyToPay メソッドで行う必要があります。
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 OS のさまざまなバージョンやフォークが予期しない動作をし、処理されないとエラーにつながる可能性があるため、特に重要です。
通常、packageManager.getPackagesForUid() は単一の要素を返しますが、呼び出し元が複数のパッケージ名を使用するまれなシナリオを処理する必要があります。これにより、アプリケーションの堅牢性が維持されます。
呼び出し元のパッケージに正しい署名があることを確認する方法については、呼び出し元の署名証明書を検証するをご覧ください。
パラメータ
parameters バンドルは Chrome 139 で追加されました。常に null と比較する必要があります。
次のパラメータが parameters Bundle でサービスに渡されます。
packageNamemethodNamesmethodDatatopLevelOriginpaymentRequestOrigintopLevelCertificateChain
packageName は Chrome 138 で追加されました。このパラメータの値を使用する前に、Binder.getCallingUid() と照合する必要があります。parameters バンドルは呼び出し元が完全に制御する一方、Binder.getCallingUid() は Android OS が制御するため、この検証は不可欠です。
WebView と、通常ローカルテストに使用される http://localhost などの非 https ウェブサイトでは、topLevelCertificateChain は null です。
ステップ 3: お客様に支払いを行っていただく
販売者は show() を呼び出して支払いアプリを起動し、お客様が支払えるようにします。支払いアプリは、インテント パラメータに取引情報を含む Android インテント PAY を使用して呼び出されます。
支払いアプリは methodName と details で応答します。これらは支払いアプリ固有であり、ブラウザには透過的です。ブラウザは、JSON 文字列の逆シリアル化を使用して、販売者向けの details 文字列を JavaScript 辞書に変換しますが、それ以上の有効性は強制しません。ブラウザは details を変更しません。このパラメータの値は販売者に直接渡されます。
AndroidManifest.xml
PAY インテント フィルタを持つアクティビティには、アプリのデフォルトの支払い方法の ID を特定する <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 は文字列のリストである必要があります。各文字列は、有効な絶対 URL で、HTTPS スキームを使用している必要があります(例: https://example.com)。
<?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 は、certificate キーとバイト配列値を含む Bundle です。
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");
レスポンス
アクティビティは、RESULT_OK を使用して setResult を介してレスポンスを返送できます。
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 エクストラとして 2 つのパラメータを指定する必要があります。
methodName: 使用されているメソッドの名前。details: 販売者が取引を完了するために必要な情報を含む JSON 文字列。成功がtrueの場合、JSON.parse(details)が成功するように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 を使用して拒否し、支払いアプリのエラーを示します。
ユーザーによるキャンセル(AbortError を引き起こす RESULT_CANCELED(0))とアプリの内部エラー(OperationError を引き起こす INTERNAL_PAYMENT_APP_ERROR(1))を区別することで、販売者はより優れたユーザーフローを構築できます。
Kotlin
setResult(Activity.RESULT_FIRST_USER)
finish()
Java
setResult(Activity.RESULT_FIRST_USER);
finish();
呼び出された支払いアプリから受け取った支払いレスポンスのアクティビティ結果が RESULT_OK に設定されている場合、Chrome はそのエキストラで空でない 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