概要
GooglePlay アプリ内課金(以下、IAB)の定期購入の一般的な購入と解約のシーケンスをまとめました。
実現方針について、所感も書いてみました。
購入
IAB の定期購入とサービス提供判断を実現する方法はいくつかあると思います
- アプリで Billing Client の queryPurchases API を利用する
- これは、クライアントAPIを利用してサービス提供中かを確認し分岐する方法です
- Web アプリがないサービスであれば非常にシンプルに実装することが可能です
- ただし、 queryPurchases API は、一部の購読情報を取得することができないため、購入に関する細かな制御は OS レイヤに任せることになります
- 購読情報をもとにバックエンドでサービス提供状態を制御する
- 購入時を含む、購読状態の変更を通知する RTDN(RealTimeDeveloperNotification)を利用して、バックエンドでサービス提供状況を管理する方法です
- 購入に使われた google アカウントと自身のサービスのアカウントをなんらか関連付ける必要があります
- サービス次第ですが、google アカウントと関連付けられたアカウントが 1:1 となるようなチェックや仕組みがあるべきでしょう
購入シーケンス
- バックエンド処理は、購入処理が完了した後に行われます
- バックエンドAPIである purchases.subscriptions.get は、googleの購読情報のみしか取得できないため、RTDN 経由で処理を実装した場合、自身のサービスと関連付ける情報を直接的に取得することができません
- developerPayload フィールドは、サービス側で任意の情報を付与することができ、そのフィールドを利用して情報の受け渡しをする
- ただし、v3 でこのフィールドは削除されたようです https://developer.android.com/google/play/billing/developer-payload
- アカウント情報だけであれば、 obfuscatedExternalAccountId フィールドに詰めることも可能
- どちらのフィールドもアプリから決済要求するときに設定する情報のため、アプリ外(Playストアの定期購入画面)で再購入した場合などは設定されずにバックエンドで処理することになる
- バックエンドで処理できない場合は、revoke API を呼び取り消しを行うか、アプリにリストアAPIを用意するかだと思う
- developerPayload フィールドは、サービス側で任意の情報を付与することができ、そのフィールドを利用して情報の受け渡しをする
- 定期購入は、クライアントかバックエンドのどちらかで acknowledge をする必要があり、3日間行われないと自動で取り消しされます
ポイント
- RTDN は、発火したイベントの種別がペイロードに含まれていますが、メッセージングサービス(google pub/sub)を利用しており、到達の順序が保証されていないため、バックエンドから API を経由し、購読情報を取得し、その状態でサービス提供開始か終了かを判断して実装するのが望ましいと思います
- obfuscatedExternalAccountId に設定する情報は、 PII(Personally Identifiable Information) にあたる情報を設定することを禁止されています
- そのため、メールアドレスなどを直接入れることはできないため、アカウントID がメールアドレスの場合はハッシュ化するなどが必要です
- ただし、64文字が上限
- https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedProfileId(java.lang.String)
- 定期購入の Acknowledge は、ユーザビリティやクレームはともかく、負荷の高い返金運用を軽減してくれるため、サービス提供と近いトランザクションとするのがよいです
- バックエンドでサービス提供処理を行っている場合、 Acknowledge もバックエンドに寄せる方がよいと思います
- 決済システム全般に言えることだと思いますが、トレーサビリティのために以下のログは取っておくといいと思います
- RTDNのリクエスト(障害時の再送でパンクしないようにすること。RTDN の記事で詳しく書きます)
- サービス提供したか、しなかったか
- 取消したか
解約
IAB の定期購入の解約は、ユーザが解約したタイミングではサービス提供状態で、次回の更新日でサービス提供終了とすることが義務付けられています。
また、解約は、クライアント・バックエンド、どちらからもAPIで提供されており実施可能ですが、Play ストアからの解約は必ず対応しなければならず、まずは、Play ストアからの対応を実装したのち、アプリ内で解約できるようにするなどの機能拡張していく流れになると思います。
解約シーケンス
- 解約後、期限切れの前はサービス提供中のため、バックエンド処理は、特に必要な処理はないと思います
- 何らかの状態を保存したい場合、再開されることを考慮してください
- 蛇足ですが、再開時にアプリ内から再購入させた場合、ユーザの請求は購入時には行われず次回更新日に行われるよう google play 内でうまく処理してくれるようです
- 期限切れ通知を受けてサービス提供終了のための処理を行います
ポイント
- 別の定期購入のサービス提供を停止してしまわないように契約のIDで一致を確認するのがよいです
- IAB の場合、purchase token で ID できるため、それを利用するのがよいです
- 期限切れはどうあっても非同期になるため、アプリとして定期でリフレッシュする仕組みは必要だと思います
ユーザの継続失敗時
異常系で必ず対応しなければならない継続時のユーザの失敗に伴うサービス提供の停止。
更新失敗によりサービス停止させられた状態を保留中(アカウント保留中)といいます。
(前提) アカウント保留中とは
- 定期購入の更新日に行われる請求に対して、ユーザの支払いが失敗するとアカウント保留中という状態になります
- アカウント保留中になったらサービス提供を停止しなければならないです
- アカウント保留中は、ユーザが、google play から支払方法を修正することで復帰します
- 公式ドキュメントにはユーザに支払方法を修正させるためになんらか通知したほうがいいとのことですが、メールや通知は google から送信されているようでした
- 30日以内に修正されないと自動更新は停止され期限切れ状態へと移行します
- iOSアプリ内課金にある Billing Retry に相当する機構があるかはわからなかったです
- https://developer.android.com/google/play/billing/subscriptions#account-hold
- 支払いに失敗しても即時にサービス提供を停止せずに注意を促す猶予期間を設定することができます
- 猶予期間は、なし・3,7,14,30日を設定できる
- 猶予期間になると、
expiryTimeMillis
(有効期限)は、内部的に自動的に延長されます - https://developer.android.com/google/play/billing/subscriptions#grace
- https://support.google.com/googleplay/android-developer/answer/140504?hl=ja#zippy=%2C%E7%8C%B6%E4%BA%88%E6%9C%9F%E9%96%93
保留中シーケンス
ポイント
- アカウント保留中かの判断は、
expiryTimeMillis
(有効期限)が、切れていて、かつ、autoRenewing
(自動更新)が、有効かで判断ができます-
paymentState
(支払いステータス)が、0
(Payment pending) かでも判断できます- 蛇足ですが、期限切れ時はこのフィールドは未設定になるのですが、私は Golang を利用しており、公式のライブラリでは、ゼロで初期化されてしまうため、見分けがつかない状態になっていました
-
- 支払方法の変更時のイベントは、それほどラグがなく通知されました
- google pub/sub や RTDN 自体に到達時間保証に関するドキュメントはなかったため、検証してみた結果でしかないです
- アプリ内で支払方法を変更する機能を実装することも可能かとは思いますが、基本的にはアプリ外で行われることだと思います。そのため、RTDN を利用した非同期処理がメイン実装となると思います
- アプリは、サーバサイドで行われた復帰処理で通知を送るか、リフレッシュ機能を用意するかが必要になるかと思います