Android 決済アプリをウェブ決済に対応させて、ユーザー エクスペリエンスを向上させる方法を学びます。
Payment Request API を使用すると、ブラウザベースの組み込みインターフェースをウェブで利用できるようになります。これにより、ユーザーは必要なお支払い情報をこれまで以上に簡単に入力できます。この API は、プラットフォーム固有の支払いアプリを呼び出すこともできます。
Android インテントを単独で使用する方法と比較して、ウェブ決済ではブラウザとの統合、セキュリティ、ユーザー エクスペリエンスが向上します。
- 支払いアプリは、販売者のウェブサイトのコンテキストでモーダルとして起動されます。
- 実装は既存の支払いアプリを補完するもので、ユーザーベースを活用できます。
- 支払いアプリの署名がチェックされ、サイドローディングが防止されます。
- 決済アプリは複数のお支払い方法をサポートできます。
- 暗号通貨、銀行振込など、任意のお支払い方法と統合できます。Android デバイスの支払いアプリでは、デバイス上のハードウェア チップへのアクセスが必要な方法を統合することもできます。
Android の支払いアプリにウェブ決済を実装するには、次の 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
を介して呼び出されます。
決済アプリは 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/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/checkout
は mystore.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
には supportedMethods
と total
のみが含まれます。
paymentRequestId
「プッシュ決済」アプリが取引状態に関連付ける 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 つのパラメータを Intent の追加情報として指定する必要があります。
methodName
: 使用されているメソッドの名前。details
: 販売者が取引を完了するために必要な情報が含まれる JSON 文字列。成功がtrue
の場合、JSON.parse(details)
が成功するようにdetails
を構築する必要があります。
支払いアプリで取引が完了しなかった場合は、RESULT_CANCELED
を渡すことができます(たとえば、ユーザーが支払いアプリでアカウントの正しい PIN コードを入力しなかった場合など)。ブラウザで、ユーザーが別の支払いアプリを選択できる場合があります。
setResult(RESULT_CANCELED)
finish()
呼び出された支払いアプリから受信した支払いレスポンスのアクティビティ結果が RESULT_OK
に設定されている場合、Chrome は追加情報で空でない methodName
と details
をチェックします。検証が失敗すると、Chrome は 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_PAY
の Binder.getCallingUid()
と PAY
の Activity.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) } }