Android 決済アプリ デベロッパー ガイド

Android 決済アプリを Web Payments と連携させ、お客様のユーザー エクスペリエンスを向上させる方法について説明します。

Payment Request API を使用すると、ブラウザベースの組み込みインターフェースをウェブで利用できるようになります。これにより、ユーザーは必要な支払い情報をこれまで以上に簡単に入力できます。API は、プラットフォーム固有の決済アプリを呼び出すこともできます。

対応ブラウザ

  • 60
  • 15
  • 11.1

ソース

ウェブ決済を使用するプラットフォーム固有の Google Pay アプリを使用した購入手続きフロー。

Android のインテントのみを使用する場合と比較して、ウェブ決済を使用すると、ブラウザ、セキュリティ、ユーザー エクスペリエンスとの統合が向上します。

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

Android 決済アプリにウェブ決済を実装するには、次の 4 つのステップを行います。

  1. 販売者が決済アプリを検出できるようにします。
  2. お客様が支払い可能な登録済みのお支払い方法(クレジット カードなど)を持っているかどうかを販売者に知らせます。
  3. お客様に支払いを行ってもらいます。
  4. 呼び出し元の署名証明書を検証します。

ウェブ決済の実際の動作については、android-web-payment デモをご覧ください。

ステップ 1: 販売者が決済アプリを検出できるようにする

販売者が支払いアプリを使用するには、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 ファイルを作成します。

app/src/main/aidl/org/chromium/IsReadyToPayServiceCallback.aidl

package org.chromium;
interface IsReadyToPayServiceCallback {
    oneway void handleIsReadyToPay(boolean isReadyToPay);
}

app/src/main/aidl/org/chromium/IsReadyToPayService.aidl

package org.chromium;
import org.chromium.IsReadyToPayServiceCallback;

interface IsReadyToPayService {
    oneway void isReadyToPay(IsReadyToPayServiceCallback callback);
}

IsReadyToPayService の実装

IsReadyToPayService の最も単純な実装の例を次に示します。

class SampleIsReadyToPayService : Service() {
  private val binder = object : IsReadyToPayService.Stub() {
    override fun isReadyToPay(callback: IsReadyToPayServiceCallback?) {
      callback?.handleIsReadyToPay(true)
    }
  }

  override fun onBind(intent: Intent?): IBinder? {
    return binder
  }
}

レスポンス

サービスは、handleIsReadyToPay(Boolean) メソッドを使用してレスポンスを送信できます。

callback?.handleIsReadyToPay(true)

権限

Binder.getCallingUid() を使用すると、呼び出し元を確認できます。これは、onBind メソッドではなく isReadyToPay メソッドで行う必要があります。

override fun isReadyToPay(callback: IsReadyToPayServiceCallback?) {
  try {
    val callingPackage = packageManager.getNameForUid(Binder.getCallingUid())
    // …

呼び出し元パッケージの署名が正しいかどうかを確認する方法については、呼び出し元の署名証明書を確認するをご覧ください。

ステップ 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/method_names" />
</activity>

resource には文字列のリストを指定する必要があります。各文字列は HTTPS スキームの有効な絶対 URL にする必要があります。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="method_names">
        <item>https://alicepay.com/put/optional/path/here</item>
        <item>https://charliepay.com/put/optional/path/here</item>
    </string-array>
</resources>

パラメータ

以下のパラメータは、インテント エクストラとしてアクティビティに渡されます。

  • methodNames
  • methodData
  • topLevelOrigin
  • topLevelCertificateChain
  • paymentRequestOrigin
  • total
  • modifiers
  • paymentRequestId
val extras: Bundle? = intent?.extras

methodNames

使用されているメソッドの名前。要素は methodData 辞書のキーです。決済アプリでサポートされている方法です。

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

methodData

methodNames から methodData へのマッピング。

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

merchantName

販売者の購入手続きページ(ブラウザの最上位のブラウジング コンテキスト)の <title> HTML タグの内容。

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

topLevelOrigin

スキームのない販売者のオリジン(トップレベル ブラウジング コンテキストのスキームのないオリジン)。たとえば、https://mystore.com/checkoutmystore.com として渡されます。

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

topLevelCertificateChain

販売者の証明書チェーン(最上位のブラウジング コンテキストの証明書チェーン)。ローカルホストとディスク上のファイル(どちらも SSL 証明書のない安全なコンテキスト)の場合は null。各 Parcelable は、certificate キーとバイト配列値を持つバンドルです。

val topLevelCertificateChain: Array<Parcelable>? =
    extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
  (p as Bundle).getByteArray("certificate")
}

paymentRequestOrigin

JavaScript で new PaymentRequest(methodData, details, options) コンストラクタを呼び出した iframe ブラウジング コンテキストの、スキームのないオリジン。コンストラクタがトップレベル コンテキストから呼び出された場合、このパラメータの値は topLevelOrigin パラメータの値と等しくなります。

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

total

トランザクションの合計金額を表す JSON 文字列。

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

文字列の内容の例を次に示します。

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

modifiers

JSON.stringify(details.modifiers) の出力。ここで、details.modifiers には supportedMethodstotal のみが含まれます。

paymentRequestId

PaymentRequest.id フィールド: 「push-payment」アプリがトランザクション ステータスに関連付ける必要があります。販売者のウェブサイトは、このフィールドを使用して、帯域外のトランザクションの状態を「push-payment」アプリにクエリします。

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

レスポンス

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

setResult(Activity.RESULT_OK, Intent().apply {
  putExtra("methodName", "https://bobbucks.dev/pay")
  putExtra("details", "{\"token\": \"put-some-data-here\"}")
})
finish()

インテント エクストラとして次の 2 つのパラメータを指定する必要があります。

  • methodName: 使用されているメソッドの名前。
  • details: 販売者がトランザクションを完了するために必要な情報を含む JSON 文字列。成功が true の場合、JSON.parse(details) が成功するように details を構築する必要があります。

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

setResult(RESULT_CANCELED)
finish()

呼び出された支払いアプリから受信した支払いレスポンスのアクティビティ結果が RESULT_OK に設定されている場合、Chrome はエクストラに空でない methodNamedetails があるかどうかをチェックします。検証で不合格になると、Chrome は拒否された Promise を request.show() から返し、次のいずれかのデベロッパー メッセージが返されます。

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

権限

アクティビティは、getCallingPackage() メソッドを使用して呼び出し元を確認できます。

val caller: String? = callingPackage

最後に、呼び出し元の署名証明書を検証し、呼び出し元パッケージの署名が正しいことを確認します。

ステップ 4: 呼び出し元の署名証明書を確認する

呼び出し元のパッケージ名は、IS_READY_TO_PAYBinder.getCallingUid()PAYActivity.getCallingPackage() で確認できます。想定したブラウザが呼び出し元であることを検証するには、その署名証明書をチェックして、正しい値と一致することを確認する必要があります。

API レベル 28 以降をターゲットとしていて、署名証明書が 1 つしかないブラウザと統合する場合は、PackageManager.hasSigningCertificate() を使用できます。

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
)

PackageManager.hasSigningCertificate() は証明書のローテーションを正しく処理するため、単一の証明書ブラウザに適しています。(Chrome の署名証明書は 1 つだけです)。複数の署名証明書があるアプリはローテーションできません。

古い API レベル 27 以前をサポートする必要がある場合や、複数の署名証明書を持つブラウザを処理する必要がある場合は、PackageManager.GET_SIGNATURES を使用できます。

val packageName: String = … // The caller's package name
val certificates: Set<ByteArray> = … // The correct set of signing certificates

val packageInfo = getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val sha256 = MessageDigest.getInstance("SHA-256")
val signatures = packageInfo.signatures.map { sha256.digest(it.toByteArray()) }
val verified = signatures.size == certificates.size &&
    signatures.all { s -> certificates.any { it.contentEquals(s) } }