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 インテントを単独で使用する方法と比較して、ウェブ決済ではブラウザとの統合、セキュリティ、ユーザー エクスペリエンスが向上します。

  • 支払いアプリは、販売者のウェブサイトのコンテキストでモーダルとして起動されます。
  • 既存の支払いアプリを補完する形で実装できるため、ユーザーベースを活用できます。
  • 支払いアプリの署名がチェックされ、サイドローディングが防止されます。
  • 支払いアプリは複数のお支払い方法に対応できます。
  • 暗号通貨や銀行振込など、任意のお支払い方法と統合できます。Android デバイスの支払いアプリでは、デバイス上のハードウェア チップへのアクセスが必要な方法を統合することもできます。

Android の支払いアプリにウェブ決済を実装するには、次の 4 つの手順が必要です。

  1. 販売者がお支払いアプリを見つけられるようにします。
  2. お客様が支払い可能な登録済みのお支払い方法(クレジット カードなど)を持っているかどうかを販売者に伝えます。
  3. お客様に支払いを許可する。
  4. 呼び出し元の署名証明書を確認します。

ウェブ決済の仕組みを実際に確認するには、android-web-payment デモをご覧ください。

ステップ 1: 販売者がお支払いアプリを見つけられるようにする

お支払い方法を設定するの手順に沿って、ウェブアプリ マニフェストに 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 で定義されています。次の内容の 2 つの 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() を使用して、呼び出し元を確認できます。Android OS はサービス接続をキャッシュに保存して再利用できるため、onBind() メソッドがトリガーされません。そのため、この操作は onBind メソッドではなく isReadyToPay メソッドで行う必要があります。

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 OS のバージョンやフォークが異なると、予期しない動作が発生し、処理されない場合エラーにつながる可能性があるため、特に重要です。

通常、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 インテント フィルタを持つアクティビティには、アプリのデフォルトの支払い方法 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 は文字列のリストで、それぞれが HTTPS スキームを含む有効な絶対 URL である必要があります。

<?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/checkoutmystore.com として渡されます。

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

topLevelCertificateChain

販売者の証明書チェーン(最上位のブラウジング コンテキストの証明書チェーン)。値は、WebView、localhost、ディスク上のファイルの場合は 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");

レスポンス

アクティビティは、RESULT_OK を使用して setResult 経由でレスポンスを返すことができます。

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();

2 つのパラメータを Intent の追加情報として指定する必要があります。

  • methodName: 使用されているメソッドの名前。
  • details: 販売者が取引を完了するために必要な情報が含まれる JSON 文字列。成功が true の場合、JSON.parse(details) が成功するように details を構築する必要があります。返すデータがない場合、この文字列は "{}" にすることができます。この文字列は、販売者のウェブサイトに空の JavaScript ディクショナリとして受信されます。

支払いアプリで取引が完了しなかった場合は、RESULT_CANCELED を渡すことができます(たとえば、ユーザーが支払いアプリでアカウントの正しい PIN コードを入力しなかった場合など)。ブラウザで、ユーザーが別の支払いアプリを選択できる場合があります。

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_PAYBinder.getCallingUid()PAYActivity.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