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

Android 決済アプリをウェブ決済と連携させ、優れたユーザー エクスペリエンスを提供する方法について説明します。

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() を呼び出して、顧客が支払いが可能かどうかを照会できます。このクエリに応答する Android サービスとして IS_READY_TO_PAY を実装できます。

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> タグが必要です。

複数のお支払い方法をサポートするには、<meta-data> タグと <string-array> リソースを追加します。

<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

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

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

push-payment アプリが取引の状態に関連付ける PaymentRequest.id フィールド。販売者のウェブサイトはこのフィールドを使用して、プッシュ型決済アプリに帯域外の取引状況をクエリします。

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 は request.show() から拒否された Promise を返します。その際、デベロッパー向けに次のいずれかのエラー メッセージが表示されます。

'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 以降をターゲットとしており、単一の署名証明書を持つブラウザと統合する場合は、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 には単一の署名証明書があります)。複数の署名証明書があるアプリは、それらの証明書をローテーションできません。

古い 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) } }