Open13

【Android】サブスク実装調査メモ(Billing Library / RTDN)

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

概要

  • Androidモバイルクライアントアプリと、サーバーのあるアプリで、サブスクリプションを実装したい

参考

  • play-billingのサンプルコード

https://github.com/android/play-billing-samples

  • リアルタイムディベロッパー通知なども含めて実装している記事

https://qiita.com/koichi-ozaki/items/7ac8c8135705867e8b14

  • Billing Library 5だが、チュートリアルがあった

https://codelabs.developers.google.com/play-billing-codelab?hl=ja#0

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

全体像

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

リアルタイムデベロッパー通知

https://developer.android.com/google/play/billing/getting-ready?hl=ja

リアルタイム デベロッパー通知(RTDN)は、アプリ内でユーザーの利用権に変更が生じたときに Google から通知を受け取るメカニズムです。RTDN では Google Cloud Pub/Sub が使用されるため、デベロッパーが設定した URL に push されたデータまたはクライアント ライブラリによってポーリングされたデータを受け取ることが可能です。
これらの通知を使用すると、Google Play Developer API をポーリングする必要がなくなり、定期購入の状態の変化にすぐに対応できます。

リアルタイム デベロッパー通知を受信したら、Google Play Developer API を呼び出し、完全なステータスを取得してバックエンドのステータスを更新する必要があります。リアルタイム デベロッパー通知は、購入のステータス変更のみを通知します。購入に関する詳細情報は通知しません。

一般的に、pull サブスクリプションは、通常 RTDN には適用されない大量のメッセージが処理されるときに、リソースの使用率を最適化するために使用されます。push と pull のどちらを開始すべきかわからない場合は、一般的に実装が容易な push を使用することをおすすめします。

RTDNの勧め

https://developer.android.com/google/play/billing/lifecycle?hl=ja

Google Play Developer API を使用して手動で購入ステータスを確認することは可能ですが、定期的なチェックに依存することは、変更のトラッキング方法としては非常に非効率であり、エラーと遅延が発生しやすくなります。RTDN を使用すると、Google Play での購入のライフサイクル トラッキング ロジックを構築することなく、すぐに変更に対応できます。

通知の具体的な形式

https://developer.android.com/google/play/billing/rtdn-reference?hl=ja#sub

  • base64でエンコードされたdataフィールドがある。
  • dataフィールドをデコードすると、その中にsubscriptionNotificationがある(場合がある)。

通知のサンプル

{
  "message": {
    "attributes": {
      "key": "value"
    },
    "data": "eyAidmVyc2lvbiI6IHN0cmluZywgInBhY2thZ2VOYW1lIjogc3RyaW5nLCAiZXZlbnRUaW1lTWlsbGlzIjogbG9uZywgIm9uZVRpbWVQcm9kdWN0Tm90aWZpY2F0aW9uIjogT25lVGltZVByb2R1Y3ROb3RpZmljYXRpb24sICJzdWJzY3JpcHRpb25Ob3RpZmljYXRpb24iOiBTdWJzY3JpcHRpb25Ob3RpZmljYXRpb24sICJ0ZXN0Tm90aWZpY2F0aW9uIjogVGVzdE5vdGlmaWNhdGlvbiB9",
    "messageId": "136969346945"
  },
  "subscription": "projects/myproject/subscriptions/mysubscription"
}
  • messageIdがユニークなキーとして利用できる。リトライされた場合も同じ。ゆえに、このキーを利用して自社DBにリトライの通知が重複保存されないようにする。

SubscriptionNotification

{
  "version": string,
  "notificationType": int,
  "purchaseToken": string,
  "subscriptionId": string
}
  • purchaseToken: 定期購入の購入時にユーザーのデバイスに提供されたトークン。
  • subscriptionId: 購入された定期購入のアイテム ID(例: monthly001)。

RTDNを受診したあと

通知を受信するには、トピックに送信されたメッセージを消費するバックエンド サーバーを作成する必要があります。サーバーは、登録されたエンドポイントへの HTTPS リクエストに応答するか、または Cloud Pub/Sub クライアント ライブラリを使用することにより、それらのメッセージを消費できます。

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

購入ライフサイクル

https://developer.android.com/google/play/billing/integrate?hl=ja

バックエンドのステータスを更新するには、通知に含まれている購入トークンを使用して purchases.subscriptionsv2.get API を呼び出します。このエンドポイントによって、購入トークンに基づく最新の定期購入のステータスを取得できます。これは定期購入の管理における信頼できる情報源と見なされます。

  • 購入トークンには60日の有効期限がある

購入トークンは定期購入の登録後から有効期限の 60 日後まで有効です。この期間を経過すると、購入トークンは無効となり、それを使用して Google Play Developer API を呼び出すことはできなくなります。

  1. 購入できるアイテムをユーザーに表示します。
  2. ユーザーが実際に購入を選択するまでの購入フローを起動します。
  3. サーバーで購入を確認します。
  4. ユーザーにコンテンツを提供します。
  5. コンテンツの配信を承認します。消費可能アイテムの場合は、その購入アイテムを消費し、ユーザーがアイテムを再購入できるようにします。

購入が成功したら

購入が成功すると、購入トークンも生成されます。これは、ユーザーとユーザーが購入したアプリ内アイテムの商品 ID を表す一意の識別子です。アプリでは購入トークンをローカルに保存することもできますが、安全なバックエンド サーバーに渡すことをおすすめします。

クライアントが購入を処理しても、サーバーが処理してもいい感じに読める

ユーザーが購入を完了したら、アプリはその購入を処理する必要があります。ほとんどの場合、アプリは PurchasesUpdatedListener を通じて購入の通知を受け取ります。ただし、購入をフェッチするで説明しているように、アプリが BillingClient.queryPurchasesAsync() を呼び出して購入を認識することがあります。
また、安全なバックエンドにリアルタイム デベロッパー通知クライアントが含まれる場合は、新規購入を知らせる subscriptionNotification または oneTimeProductNotification(保留中の購入に関してのみ)を受信することで、新規購入を登録できます。これらの通知を受け取ったら、Google Play Developer API を呼び出して完全なステータスを取得し、バックエンド ステータスを更新します。

承認が必要

利用権を付与した後、アプリは購入を承認する必要があります。
3 日以内に購入を承認しない場合、ユーザーは自動的に払い戻しを受け、Google Play は購入を取り消します。

可能であれば、購入を安全なバックエンドから確実に承認できるように、Google Play Developer API の Purchases.subscriptions.acknowledge を使用してください。

定期購入の更新は、承認の必要はありません。

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

バックエンド処理(新規購入時)

バックエンドのステータスを更新するには、通知に含まれている購入トークンを使用して purchases.subscriptionsv2.get API を呼び出します。このエンドポイントによって、購入トークンに基づく最新の定期購入のステータスを取得できます。

新規購入

ユーザーが定期購入に登録すると、タイプ SUBSCRIPTION_PURCHASED の SubscriptionNotification メッセージが RTDN クライアントに送信されます。

↓この通知を受け取ったら

  1. purchases.subscriptionsv2.get エンドポイントをクエリして、最新の定期購入のステータスを含む定期購入リソースを取得します。
  2. subscriptionState フィールドの値が SUBSCRIPTION_STATE_ACTIVE であることを確認します。
  3. 購入を確認します。
  4. ユーザーにコンテンツへのアクセスを許可します。
    購入時に setObfuscatedAccountId と setObfuscatedProfileId を使用して識別子が設定されている場合、購入に関連付けられたユーザー アカウントは定期購入リソースの ExternalAccountIdentifiers オブジェクトで識別できます。

↑この4の情報でアプリのユーザーと定期購入を結びつける

3の確認する、とは、承認することでありこちらのドキュメントにその詳細が書かれている

https://developer.android.com/google/play/billing/security?hl=ja#verify


購入に関連付けられたユーザー アカウントは、次の識別子で識別できます。サーバーサイドの定期購入は Purchases.subscriptions:get から返される SubscriptionPurchase.obfuscatedExternalAccountId、クライアントサイドの定期購入は Purchase.getAccountIdentifiers() の obfuscatedAccountId です。ただし、購入時に setObfuscatedAccountId で設定しておく必要があります。

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

Google Play Developer API

Method: purchases.subscriptionsv2.getについて

リクエスト

  • リクエストのパラメーターに、tokenを含める
    • 定期購入の通知に含まれる購入トークンというやつだろう。

レスポンス

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

バックエンド処理(更新まわり)

https://developer.android.com/google/play/billing/lifecycle/subscriptions?hl=ja#renewal

Google Play Developer API から返された定期購入リソースにある新しい expiryTime を使用して、定期購入のステータスを更新する必要があります。

猶予期間

  • 猶予期間はまだ使える!

復元期間は、猶予期間とアカウントの一時停止期間で構成されます。猶予期間中、ユーザーが定期購入の利用資格に引き続きアクセスできるようにする必要があります。

Google Play は猶予期間の終了まで expiryTime 値を動的に延長します。

復元期間は、猶予期間とアカウントの一時停止期間で構成されます。猶予期間中、ユーザーが定期購入の利用資格に引き続きアクセスできるようにする必要があります。

ユーザーが猶予期間中に支払い方法を修正しなかった場合、定期購入はアカウントの一時停止ステータスとなり、利用資格が失われます。

一時停止

リアルタイム デベロッパー通知では、定期購入がアカウントの一時停止ステータスになると、タイプ SUBSCRIPTION_ON_HOLD の SubscriptionNotification メッセージが送信されます。定期購入の最新情報を取得するには、安全なバックエンド サーバーから purchases.subscriptionsv2.get を呼び出します。アカウントの一時停止中は、定期購入リソースの expiryTime フィールドは過去のタイムスタンプに設定され、subscriptionState フィールドは SUBSCRIPTION_STATE_ON_HOLD に設定されます。

→一時停止から復活したら

ユーザーが支払い方法を修正して定期購入がアクティブな状態に戻った場合、定期購入されたコンテンツへのアクセスを復元する必要があります。この場合の購入トークンはアカウントの一時停止前と同じものです。同じ購入が再開され、タイプ SUBSCRIPTION_RECOVERED の RTDN を受け取ります。

→一時停止から復活しなかったら

アカウントの一時停止期間が終了する前にユーザーが支払い方法を修正しなかった場合は、代わりにタイプ SUBSCRIPTION_CANCELED の RTDN が送信されます。→「解約」に移行

...これ以上にまだまだあるのでいったんドキュメントに任せる

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

データタイプ整理

トリガー RTDNタイプ expiryTime subscriptionState 状況 次の可能性
新規購入 [S]_PURCHASED next_renewal_date [SS]_ACTIVE 使える
自動更新 [S]_RENEWED next_renewal_date [SS]_ACTIVE 使える
支払いに問題発生 [S]_IN_GRACE_PERIOD 動的に延長(future) [SS]_IN_GRACE_PERIOD 猶予期間: 使える 次はアカ一時停止
猶予中に支払い復活せず [S]_ON_HOLD 過去 [SS]_ON_HOLD アカ一時停止期間: 使えない 解約/再開/再購入
アカ一時停止中に復活 [S]_RECOVERED next_renewal_date [SS]_ACTIVE 使える
アカ一時停止中に復活せず [S]_CANCELED 過去 [SS]_CANCELED 使えない 解約処理が自動で行われる
有効期限切れ [S]_EXPIRED 過去 [SS]_EXPIRED 使えない 利用資格を停止
解約 [S]_CANCELED 過去か未来 [SS]_CANCELED 期限内なら使える 期限過ぎたら利用資格停止
再開 [S]_RESTARTED next_renewal_date [SS]_ACTIVE 使える
  • アカ一時停止中に復活した場合、購入トークンはアカ一時停止前のものと同じ
  • 解約のケース
    • ユーザーが自主的にPlayの定期購入センターから解約したとき
    • アカ一時停止になった後で再開をしなかった場合に、自動的に解約されたとき
    • デベロッパーがpurchases.subscriptions.cancelのAPIで解約をトリガーしたとき
  • アカ一時停止中に復活しなかった場合、すぐに解約処理が自動で行われる。よって[S]_EXPIREDのRTDNも送信される
  • 再開...定期購入が解約されて、まだ期限切れになっていないときに、ユーザーが「再度定期購入」ボタンをタップした時。→解約が取り消され、定期購入が更新される。マスト対応。これはPlay のデベロッパー向けドキュメントと API では「再開」と呼ばれる。
  • 再度定期購入...自動更新による定期購入の有効期限が切れた後、ユーザーが同じ定期購入の基本プランを購入できるようにすることが可能。この操作は、Play のデベロッパー向けドキュメントと API では「再度定期購入」と呼ばれる。
    • RTDNへの通知は[S]_PURCHASEDであり、新規購入として扱われる。
  • 開発者目線での「再開」と「再度定期購入」は別概念だが、ユーザーにとっては同じボタン(再度定期購入)となっていそうで注意!

保留中の取引について

ユーザーが購入を開始してから購入のお支払い方法が処理されるまでに 1 つ以上の追加手順が必要な取引です。このタイプの購入では、ユーザーのお支払い方法への請求が正常に処理されたことを Google が通知するまで、アプリは利用権を付与してはなりません。
たとえば、ユーザーがアプリ内アイテム購入のお支払いに現金を選択したので、PENDING の購入が作成されます。次に、ユーザーは取引を完了する実店舗を選択し、通知とメールの両方でコードを受け取ります。ユーザーは実店舗に足を運び、レジでコードを提示して現金による支払いを行います。Google は、デベロッパーとユーザーの両方に現金の受領を通知します。この時点で、アプリはユーザーに利用権を付与できます。

ステータスが PURCHASED の場合にのみ、利用権を付与してください。

その他特殊なケース

取り消し

  • purchases.subscriptions.revokeを使用したバックエンドによる定期購入の取り消しやチャージバックなど
  • RTDNに[S]_REVOKEDが送信され、定期購入リソースは[SS]_EXPIREDになる。

延長

  • 特別なプロモーションなどで延長したい時
  • purchases.subscriptions.deferのAPIで可能
  • [S]_DEFERRED のRTDNが送信され、定期購入リソースは[SS]_ACTIVEとなり、expiryTimeが更新される
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

定期購入の一時停止

https://developer.android.com/google/play/billing/lifecycle/subscriptions?hl=ja#expiration

  • 支払いの猶予期間を過ぎた後の「アカウントの一時停止」とは別で、「定期購入の一時停止」という概念が存在する。

一時停止機能を有効にすると、契約期間の間隔に応じて、ユーザーは 1 週間から 3 か月の間、定期購入を一時停止できます。
定期購入の一時停止期間中、ユーザーは定期購入にアクセスできず、更新に伴う料金は発生しません。一時停止期間が終了すると、定期購入が再開され、Google は定期購入の更新を試みます。正常に再開できれば、定期購入が再びアクティブになります。

  • デフォルトでこの機能は有効。最初ハンドリングの開発リソースが無いときには無効にしてもいいかも

タイプ SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED の SubscriptionNotification メッセージは、ユーザーが定期購入の一時停止を開始したときに送信されます。この時点では、ユーザーは次回の更新日まで定期購入にアクセスでき、定期購入リソースには autoRenewEnabled = true が含まれています。この時点での subscriptionState フィールドの値は SUBSCRIPTION_STATE_ACTIVE です。
一時停止が有効になると、タイプ SUBSCRIPTION_PAUSED の SubscriptionNotification メッセージが送信されます。メッセージが送信されると、ユーザーは定期購入にアクセスできなくなり、定期購入リソースには autoRenewEnabled = true が含まれ、subscriptionState フィールドは SUBSCRIPTION_STATE_PAUSED に設定されます。

  • →subscriptionStateに従ってればOK!(ACTIVEかどうか)
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

PurchaseTokenが変わるのか?という問い

https://developer.android.com/google/play/billing/lifecycle/subscriptions?hl=ja#renewal

  • アカウント一時停止からの復活 → 変わらない

ユーザーが支払い方法を修正して定期購入がアクティブな状態に戻った場合、定期購入されたコンテンツへのアクセスを復元する必要があります。この場合の購入トークンはアカウントの一時停止前と同じものです。同じ購入が再開され、タイプ SUBSCRIPTION_RECOVERED の RTDN を受け取ります。

  • 有効期限が切れた後のPlayストアからの再購入 → 変わる

自動更新による基本プランが Google Play Console または API を使用して再度定期購入できるように設定されている場合、ユーザーは期限切れの定期購入を Google Play ストアで再購入できます。
この場合は新規購入となるため、Google Play は新しい購入トークンを発行し、バックエンドはタイプ SUBSCRIPTION_PURCHASED の RTDN を受け取ります。このタイプのアプリ外で行われた購入のステータスには、元の購入に関連付けられている linkedPurchaseToken は含まれません。これは元の定期購入が完全に期限切れになっているためです。このような新規購入は他の購入と同様に、バックエンドが処理して承認する必要があります。

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

実際のデータで確認

実験に利用した定期購入アイテムの設定

購入→更新をし続ける

  • サーバー通知タイプ: SUBSCRIPTION_PURCHASEDで購入し、その後サーバー通知タイプ: SUBSCRIPTION_RENEWEDが送られ続ける
  • purchase_tokenはずっと変わらない

購入→解約→期限切れ

  • 解約したタイミングで、サーバー通知タイプ: SUBSCRIPTION_CANCELEDsubscription_state: SUBSCRIPTION_STATE_CANCELEDが送られてくる。この時点ではまだ定期購入にアクセスできるようにする
    • {"auto_renew_enabled"=>true}が消える

購入→解約→期限切れ / その後再度購入

  • 一度expiredしたあと、再度購入している。
  • purchase_tokenは前回の値とは異なるものに変わっている。(これはAppleと違う挙動)

  • planを変えても挙動は同じ

猶予期間 → 復活

  • サーバー通知: SUBSCRIPTION_IN_GRACE_PERIODが届き、expiry_timeが7日延長されている。これは定期購入アイテムの設定通り。
  • 猶予期間のうちに支払いが成功し、サーバー通知: SUBSCRIPTION_RENEWEDが届く。expiry_timeは、猶予期間の7日分が消去されている。
    • もし7日分が消去されなかったら、支払いに失敗する度に猶予期間分(ここでは7日)利用できる期間が増えて、得になってしまう。
  • 猶予期間の後、アカウント一時停止まで行った場合は、そこから復活したときにサーバー通知: SUBSCRIPTION_RECOVEREDが届く。だが今回のようにアカウント一時停止までいかずに復活した場合は、通常の更新と同じように サーバー通知: SUBSCRIPTION_RENEWEDが届く。

猶予期間 → アカウント一時停止期間 → 復活

  • サーバー通知: SUBSCRIPTION_ON_HOLDが届き、アカウント一時停止期間になっている。expiry_timeは過去の日付(猶予期間の終わり)になっている。
  • サーバー通知: SUBSCRIPTION_RECOVEREDが届き、復活している。expiry_timeは、復活した日からそのプランの日数分数えられている。
  • アカウント一時停止期間に復活したので、purchase_tokenは変わっていない

猶予期間 → アカウント一時停止期間 → 復活せず(データ無し)

  • データとして観測していない
  • サーバー通知: SUBSCRIPTION_CANCELEDが届くはず。そのあと、EXPIREDの通知が来ることを確認したい。

定期購入の一時停止 → 一時停止の有効化 → 復活

  • (53行目)サーバー通知: SUBSCRIPTION_PAUSE_SCHEDULE_CHANGEDで、一時停止を申し込んでいる。
    • 次回の更新日(2024/5/19)までは定期購入にアクセスでき、subscription_state: SUBSCRIPTION_STATE_ACTIVEである。
  • (54行目)定期購入の一時停止が有効化されている。サーバー通知: SUBSCRIPTION_PAUSED
    • この時点で定期購入の利用資格をなくすように自社サーバーで処理する
  • (55行目)サーバー通知: SUBSCRIPTION_RECOVEREDで復活している。
    • expiry_timeは、復活した日から数えて期間分になっている。

定期購入の一時停止 → 一時停止の有効化 → 解約 → 終了

  • 一時停止が有効化されたあと、そのまま解約している(167行目)
    • 解約された時、subscription_stateは、SUBSCRIPTION_STATE_PAUSEDではなく、SUBSCRIPTION_STATE_EXPIREDになっている
  • 解約(サーバー通知: SUBSCRIPTION_CANCELED)が届いてすぐ(5秒後)に、サーバー通知: SUBSCRIPTION_EXPIREDが届いている。

定期購入の一時停止 → 一時停止の有効化 → 別で加入

  • PAUSEされている途中で、新しく購入している(234行目)
    • 同じplanであるが、別のpurchase_tokenとなっている
  • 新しい定期購入が購入されてすぐに、以前PAUSEしていた定期購入がEXPIRDとなっている(235行目)

定期購入の「再開」が行われた

  • (435行目)キャンセルされたあと、まだexpiry_timeに到達する前に再度定期購入をしている
    • サーバー通知: SUBSCRIPTION_RESTARTEDが届き、subscription_state: SUBSCRIPTION_STATE_ACTIVEとなっている。
    • {"auto_renew_enabled"=>true}も付与されている。