アプリ内課金

【Googleサブスク】購入レシートをステータス別にハンドリングする

記事内に商品プロモーションを含む場合があります

これまで、Google Play Billing のサブスクリプションの実装について情報をまとめてきました。

【Googleサブスク】レシート検証をPHPとKotlinで実現する前回、GooglePlayBilling(Android)の都度課金についてまとめてみましたが、最近は月額課金のサービスが多くなり、都度...

この手の情報は、実際に Android アプリの開発や課金のシステムに携わらないと調べる機会もなく、別に知らないからといって困るものではありません。

また、実際にアプリで月額課金のサービスをやろうとなったとしても、そのアプリの方針に合わせてサブスクリプションの実装をすればいいだけで、そこまで苦労することはないでしょう。

しかし、GooglePlay の仕様に変更が入ることは珍しくなく、新しい機能の追加や古い API バージョンの廃止など、変更を迫られる場面もしばしば。

今回は実際に商品を新規購読してから、どのように Google 側で管理しているステータスが遷移していくのか、レシートの状態を確認しながら調べてみましたので紹介します。
(時間の経過とともに情報が古くなるので、気付いた部分は随時修正や追記します)

2020 年に Google の公式ドキュメントが整理され、以前よりも必要な情報へのアクセスがしやすくなりました。こちらをベースにまとめた記事を書いてみましたので、以下の記事も参考にしてみてください。

Googleアプリ内課金の導入と運用方法(GooglePlayBilling)

Googleのサブスクリプションの特徴

通常、毎月の決済に問題がなければ「継続課金」となりますが、Google に登録しているクレジットカードの有効期限が切れていたり、何かしら決済情報に不備があると Google が即座に検知して通知をしてきます。

これは、アプリの設定(ストアだったり、商品だったり)に依存する部分もありますが、決済不備によって即座に解約になるというわけではなく、いくつかの保留期間(救済措置)が用意されています。

これについては、デベロッパー通知の記事でも書きましたが、「猶予期間(Grace Period)」と「一時停止(Account Hold)」が該当します。

【Googleサブスク】リアルタイムデベロッパー通知の種類とハンドリング「Google(Androidサブスク)のレシート検証について」で書いたように、Google Play Billing のサブスクリプシ...

また、比較的新しい機能としては、ユーザ自身がおこなう「一時停止(PAUSE)」も、アプリによっては需要がありそうです。

プロ野球などスポーツ系のサブスクリプションモデルであれば、オフシーズンの間はお金を払いたくないユーザもいるでしょうし、わざわざ解約して再度契約するよりは、一定期間だけ使用しない選択肢があるのは嬉しいですよね。

また、ユーザの購読期間が伸びれば、アプリを配信している側にも手数料の恩恵(通常は 30% で、1 年を超えると 15% と聞いた記憶がある)がありますし、Google もなるべくユーザが継続してくれるような施策を考えて提供してくれています。

では、状態別の Google が管理している購入レシートをサンドボックス環境を使って見ていきましょう。

1 回限りのアイテムと定期購入の購入フローは似ていますが、定期購入の場合は、更新の承認や不承認などのシナリオが追加されます。このような状況に関してアプリをテストするために、「テスト支払い方法 – 常に承認」と「テスト支払い方法 – 常に不承認」を使用します。2 つの支払い方法を使用することで、定期購入の購入後のシナリオをテストします。

定期購入固有の機能をテストする

なお、Google のドキュメントは英語のページが最新なので、極力 URL のクエリパラメータに「hl=en」を付けておくと良さそうです。

項目ごとに決まっている値

レシートの項目には、特定の番号が割り振られているものがいくつかあるので、ここにまとめておきます。

ただ、いきなり追加されたりすることも考えられるので、システム側でガチガチに値を持ってしまうと危険な場面も出てきそうです。

定数や ENUM などで管理しておきたい気持ちもありますが、やり過ぎないようにしておくと良さそうですね。

A SubscriptionPurchase resource indicates the status of a user’s subscription purchase.

Purchases.subscriptions

acknowledgementState(承認ステータス)

2019 年の 5 月くらいに追加された項目で、決済の承認ステータスを管理します。

ネイティブアプリ側で「Google Client Library2.0」を使っている場合は、ここの制御(Acknowledge API)が必要になってくるので要注意です。

Google Play では、アプリの内側(アプリ内)またはアプリの外側(アプリ外)からアイテムを購入することができます。ユーザーがどちらでアイテムを購入したかにかかわらず、Google Play で一貫した購入エクスペリエンスを確保するには、ユーザーに権利を付与した後できるだけ早く Google Play Billing Library を介して受信したすべての購入を確認する必要があります。3 日以内に購入を承認しない場合、ユーザーは自動的に払い戻しを受け、Google Play は購入を取り消します。

Google Play Billing Library release notes

内容
0未承認(Yet to be acknowledged)
1承認済(Acknowledged)

cancelReason(自動更新されない理由)

直訳すると解約理由ですが、これはユーザではなく Google のシステム的な情報だと思っておいて良さそうです。

2 については、主にプラン変更のケースが該当しそうですね。

内容
0ユーザが解約した(User canceled the subscription)
1決済に問題があった(Subscription was canceled by the system, for example because of a billing problem)
2新しいサブスクリプションに変わった(Subscription was replaced with a new subscription)
3運営側による解約(Subscription was canceled by the developer)

cancelSurveyReason(解約理由)

解約理由はアプリの評価が確認できるチャンスでもあるので、簡易的なものですが受け取っておいて損はないでしょう。

「その他」を選択した場合のみ、自由入力欄(最大300文字)からコメントをもらうこともできるので、多くはクレームなのでしょうがサービスとしては参考になる情報だと思います。

自由入力欄の内容は「userInputCancelReason」の項目に入ってきます。

内容
0その他(Other)
1このサービスを十分に活用していない(I don’t use this service enough)
2技術的な問題(Technical issues)
3料金上の理由(Cost-related reasons)
4もっと良いアプリを見つけた(I found a better app)

paymentState(決済ステータス)

決済ステータスで、主に 0 と 1 のステータスを目にすることが多いと思います。

3 は商品のアップグレードやダウングレードを DEFERRED モードで行うと確認できると思ったのですが、実際に 3 になることはありませんでした。さらに別の条件が必要なのかも。

内容
0支払い保留(Payment pending)
1支払い済(Payment received)
2無料トライアル(Free trial)
3保留中の遅延アップグレード/ダウングレード(Pending deferred upgrade/downgrade)

新規購読

初めて購読する場合や、過去に解約をしている場合は、新規購読の扱いになります。

購入直後のレシートを確認してみると以下の情報を持っています。
(必要そうな情報のみ抜粋します)

サンドボックス環境だと、月額の課金を 5 分ごとに行ってくれますが、ここではわかりやすく購読開始日時と有効期限を 1 日あけて「2019-12-01 00:00:00」から「2019-12-02 00:00:00」としておきます。

orderId の先頭の GPA は「Google Play Order Number Associated」の略なんだろうか?

新規購入後は有効期限まで購読できる状態なので、paymentState が 1(Payment received)になっています。

この章の冒頭でも触れましたが、「Google Client Library2.0」を利用している場合は acknowledgementState が 0 になります。

レシートの内容に問題がなく、サービス側としてユーザを購読状態にした場合は、3 日以内に Acknowledge の API で承認処理をしておきましょう。

ちなみにサンドボックス(Googleのテストレシート)だと、新規購読の次の更新(5分後かな)の時点で未承認のものはキャンセル扱いになるので、ここも意識しておくといいかもしれません。

承認が成功すると、acknowledgementState が 1 になって返ってくるようになります。

ちょっとここで補足ですが、レシートの更新をリアルタイムに自分のコンテンツに反映する方法は大きく 2 つ考えられます。

1 つは新規購読時(ネイティブアプリからのリクエスト)や有効期限の前にレシートを確認して、更新の有無があるかどうかを確認して反映する方法。

もう 1 つは、デベロッパー通知を利用する方法です。

デベロッパー通知を受け取ったタイミングで、レシートの内容を確認して、自分のサイトに状態を反映すれば整合性がとれます。

新規購読時のデベロッパー通知は、決済に成功したタイミングで「SUBSCRIPTION_PURCHASED(4)」が届きます。

【Googleサブスク】リアルタイムデベロッパー通知の種類とハンドリング「Google(Androidサブスク)のレシート検証について」で書いたように、Google Play Billing のサブスクリプシ...

定期購入の更新

サンドボックス(Googleのテストレシート)で 1 ヶ月 の商品でテストする場合、公式のドキュメントにも書かれている通り、更新の間隔は 5 分になります。

ストアの商品設定テストレシートの更新
1週間5分
1か月5分
3か月10分
6か月15分
1年30分

正常に決済が完了して更新できたレシートは、以下の状態になります。

購読中の状態から変更のあった項目は以下の通りです。

・expiryTimeMillis
・orderId

expiryTimeMillis についてはテスト環境だと通常は 5 分だけ延長されますが、ここではわかりやすく 1 日延ばして「2019-12-03 00:00:00」としています。

orderId は、初回購入時の orderId の最後に連番が付与されていきます。

次回の更新では「GPA.9999-9999-9999-99999..1」になると予想できますね。

デベロッパー通知は、更新のタイミングで「SUBSCRIPTION_RENEWED(2)」が届きます。

定期購入の解約

解約は、大きく 2 つのタイミングが考えられます。

・次の更新のタイミングで更新しない
・決済不備の状態から即時解約

次の更新のタイミングで更新しない

まずは前者のパターンを考えていきたいと思います。

このパターンに該当するのは、サブスクリプションが購読中の時です。

既に有効期限までの購読の権利を持っているので、解約の希望を出したからといって、すぐにコンテンツを見れなくしてしまうと大問題になります。

ユーザには最後までコンテンツを楽しんでもらって、気分よく解約してもらわないといけません。じゃないと、日割りで返金しろってことになっちゃいますよね。

よって有効期限までユーザには閲覧してもらっていいのですが、解約の希望を出した時点で Google のレシート情報には変更が生じます。

仮に 12/2 のお昼の 12 時に、ユーザが解約処理を行ったと仮定します。

購読中の状態から変更のあった項目は以下の通りです。

・autoRenewing
・cancelReason
・userCancellationTimeMillis
・cancelSurveyResult

有効期限で継続する意思がないので、autoRenewing は false になっています。

また cancelReason と userCancellationTimeMillis が新たに設定されますが、こちらのキャンセル理由はシステム的な解約情報になるので、ユーザアンケートは cancelSurveyResult を参照しましょう。

この辺の細かい仕様は、冒頭の「項目ごとに決まっている値」の章にまとめてありますので、忘れたら見直しておくのがベターです。

デベロッパー通知は、この時点で「SUBSCRIPTION_CANCELED(3)」が届き、実際に有効期限を過ぎると「SUBSCRIPTION_EXPIRED(13)」が届きます。

状況に合わせてコンテンツ側で閲覧や引き止めなどの制御を行うといいですね。

なお、有効期限が過ぎた時のレシートは以下の内容になります。期限が過ぎただけで、特にレシートの内容に変化があるわけではありません。

決済不備の状態から即時解約

このパターンに該当するのは、サブスクリプションが「猶予中」または「アカウントの一時停止中」の時です。

決済が滞っているので、ここで解約したからといって既に有効期限は切れている状態です。

よって、ユーザが解約を実行した時点で即解約となります。

即解約した際のレシートは以下の通り。

デベロッパー通知は、この時点で「SUBSCRIPTION_CANCELED(3)」が届きます。

定期購入の決済不備

次に決済不備が発生した時の挙動を確認していきます。

決済不備があった場合は、ストアや商品の設定によって、ステータスの遷移先が変わってきます。

決済不備が発生した際、GooglePlay Console における設定ごとの状態遷移は以下の通りです。

猶予(商品ごと)一時停止(アプリごと)結果
設定なし設定なし解約
設定あり設定なし猶予(猶予期間を過ぎると解約)
設定なし設定あり一時停止
設定あり設定あり猶予(猶予期間を過ぎると一時停止)

ここでは、商品ごとの猶予設定も、アカウントの一時停止設定も有効にしている前提で説明していきます。

設定ポリシーはコンテンツによって異なってくると思うので、状況に合わせて置き換えてください。

なお、ここでいう「一時停止」は「Account Hold」のことを指していて、ユーザが自発的に設定できる「ユーザの一時停止(Pause)」とは異なるので注意してください。

「Pause」はもう少し後半で紹介します。

猶予(Grace Period)

まずは猶予です。

Android の月額課金の手数料を考えると、ユーザの購読期間はなるべく延ばしておきたいものです。

そこで、決済不備があった時に即時解約してもらうのではなく、「決済情報に不備があるから、数日以内に修正ください。期間内に修正すればそのまま継続できます」とユーザに通知を行います。

猶予設定をしていれば、通知は Google からもユーザに届きますし、コンテンツ側でも促すことができます。

1 ヶ月のサブスクリプション商品なら、デフォルトでは「7日間」の猶予が設定されていますが、これを変更することも可能です。

猶予期間
なし
3日間
7日間
14日間
30日間

実際に猶予になった際のレシートは以下の通りです。

特徴としては、paymentState が 0 になることと、猶予期間の間、レシートを取得する度に 24 時間程度先の日時が expiryTimeMillis に設定されることです。

これはコンテンツ側が expiryTimeMillis でサービス提供を制御していたらユーザに影響が出るので、アカウントの一時停止(または解約)までの猶予期間は有効なまま確保してくれます。

コンテンツ側としては、猶予期間中はユーザにコンテンツを提供しつつ、数日以内に決済の修正を期待する流れになります。

例えば猶予期間が 7 日あれば、「2019-12-03 00:00:00」の時点では「2019-12-04 00:00:00」まで expiryTimeMillis が更新されます。

どうせなら猶予期限の値をレシートに設定してくれた方が対応しやすいのですが、ここは現状の Google の仕様として受け入れるしかありません。

もしかするとストアの商品情報を設定変更したら(例えば 7 日から 3 日にするなど)、猶予期間が即時に反映されてしまうのかもしれませんね。それなら納得です。

ちなみにデベロッパー通知は、この時点で「SUBSCRIPTION_IN_GRACE_PERIOD(6)」が届きます。

どちらにしても、以下の 3 つの項目で猶予期間の判断はできると思います。

内容
autoRenewingtrue
paymentState0
expiryTimeMillis未来の日時

実際にユーザが決済情報を修正した場合は、デベロッパー通知は「SUBSCRIPTION_RENEWED(2)」が届きます。

この通知が届いたら決済が完了しているので、コンテンツ側で猶予中から購読中にステータスを変更しておきましょう。

猶予期間でユーザが決済情報を改善しなかった場合は、章の冒頭の表の通り、設定によって「アカウントの一時停止」または「解約」となります。

アカウントの一時停止(Account Hold)

次に一時停止です。この章の冒頭でも書きましたが、「Account Hold」の一時停止なのでご注意ください。

猶予と同じく、Android の月額課金の手数料を考えると設定を有効にしておきたいところですが、猶予とは状態が大きく異なるので注意が必要です。

まず、一時停止の状態はユーザにコンテンツを提供しないことを想定しています。

ただし、裏では Google が 30 日間、ユーザの決済不備が解消されていないかチェックを行っています。

この期間がアカウントの一時停止(Account Hold)に該当します。

実際に一時停止になった際のレシートは以下の通りです。

猶予の時とレシートの内容は同じなのですが、「expiryTimeMillis」が過去になっているのが特徴です。

内容
autoRenewingtrue
paymentState0
expiryTimeMillis過去の日時

ちなみにデベロッパー通知は、この時点で「SUBSCRIPTION_ON_HOLD(5)」が届きます。

コンテンツ側としてはユーザが解約している状態とほぼ同じなので、このまま 30 日が過ぎて解約を待つ身という悲しい状態ですが、かすかな望みがあるだけラッキーかもしれません。

このアカウントの一時停止中にユーザが決済情報を修正した場合は、デベロッパー通知は「SUBSCRIPTION_RECOVERED(1)」が届きます。

この通知が届いたら決済が完了しているので、コンテンツ側で一時停止中から購読中にステータスを変更しておきましょう。

一時停止期間でユーザが決済情報を改善しなかった場合は、章の冒頭の表の通り「解約」となります。

ユーザの自発的な一時停止

記憶が正しければ 2019 年から新しく追加された機能になります。

冒頭でも軽く触れましたが、「時期によっては利用したいから解約するか悩むけど、とりあえずこの先 2 ヶ月くらいは使うことがないな」っという場面にマッチしそうですね。

コンテンツ提供側としても継続してもらえるし、ユーザも解約や再契約の手間が省けるところにメリットがあります。

良さそうな雰囲気ですが、一定の制限もあるので、先にそこを紹介しておきます。

・ストアでユーザの一時停止を有効にする必要がある
・アカウントの一時停止も有効にする必要がある
・最大で 3 ヶ月まで一時停止できる
・1 年に 3 回まで利用できる

また、Google のレシート情報にも特徴が大きく現れています。早速、自発的な一時停止を行った場合のレシートを見てみましょう。

わかりやすいように、3 ヶ月後の「2020-03-01 00:00:00」まで一時停止することとします。

この自発的な一時停止は、最大で 3 ヶ月まで設定できると説明しました。

よって、一時停止をする時点で期限が決まるので、「autoResumeTimeMillis」に自動再開の日時が設定されます。

この日時までは一時停止となり、ユーザはコンテンツを閲覧できません。

ただし購読中の解約(予約)と同じく、この一時停止も開始のタイミングが即時ではないので注意が必要です。

この一時停止を行ったタイミングでは、また既存の購読期間が存在しているので、その期限までは購読中のままになります。

この購読の期限を過ぎたタイミングで、初めて一時停止期間がスタートします。

Google のデベロッパー通知はそこを厳密に制御してくれていて、タイミングによって以下の通知を振り分けてくれます。

状態通知の種類
自発的な一時停止を実行SUBSCRIPTION_PAUSED(11)
一時停止がスタートするSUBSCRIPTION_PAUSE_SCHEDULE_CHANGED(10)

よって、「SUBSCRIPTION_PAUSED」の通知を受けた段階では、コンテンツ側で何かする必要はありません。

実際に一時停止がスタートした後のレシートにも変更はありませんが、「expiryTimeMillis」が過去になっているのでコンテンツ提供しないようにしましょう。

違いをまとめると以下の通りです。

一時停止(予約)一時停止(開始直後)
autoRenewingtruetrue
paymentState11
expiryTimeMillis未来の日時過去の日時
autoResumeTimeMillis未来の日時未来の日時

なお、自動再開日時に近づくと、Google から「SUBSCRIPTION_RECOVERED(1)」のデベロッパー通知が送られてきてレシートの内容も以下に変わります。

再開日時が「2020-03-01 00:00:00」なので、有効期限は仮に「2020-03-02 00:00:00」としています。

「autoResumeTimeMillis」の項目がなくなり、「orderId」も連番がインクリメントされていますね。

定期購入のアップグレードとダウングレード

1 つのサブスクリプション商品を取り扱っているアプリが多いと思いますが、最近は提供する機能に優越をつけて複数商品を選択してもらうサービスも増えました。

そうなると、

「もっと多くの機能を使いたい」

「低価格版の方がコスパ良さそう」

みたいな判断をユーザがするようになります。

そうなると、商品のアップグレードとダウングレードの出番ですよね。

もちろん、Android のサブスクリプションにアップグレードとダウングレードの機能は用意されています。

以前は、アップグレードもダウングレードも即時に切り替えだったのですが、2018 年くらいから遅延が可能となりました。

要は、次の更新のタイミングからプラン変更させるというものです。これが「DEFERRED」モードに該当するのですが、ここで Google が用意しているモードを洗い出しておきましょう。

モード大まかな内容
IMMEDIATE_WITH_TIME_PRORATION現在の定期購入は直ちに終了して新しくなる
IMMEDIATE_AND_CHARGE_PRORATED_PRICE現在の定期購入は直ちに終了して新しくなる
(アップグレードのみ対応)
IMMEDIATE_WITHOUT_PRORATION現在の定期購入は直ちに終了して新しくなる
DEFERRED現在の定期購入は有効期限が切れるまで継続する
次の更新のタイミングで新しくなる

大きくは、即時か遅延(予約というイメージ)の 2 パターンになりますが、ネイティブアプリで発行できるレシートの状態やデベロッパー通知の挙動は大きく異なるので注意が必要です。

ここでは即時と予約に分けて見ていきましょう。

即時アップグレードとダウングレード

Apple と違い、Google ではアップグレード・ダウングレードに関わらず、金額や購読期間を調整して即時に商品変更が行われてきました。

このタイミングで既存の購読商品が無効となり、新しく商品が契約されます。

ネイティブアプリで取得できるレシートを見ればわかりますが、「purchaseToken」が変更になります。

アップグレードを例に見ていきましょう。

アップグレード前と後の商品で「purchaseToken」が変わってしまうと、「切り替えができないんじゃない?」っと心配しちゃうところですが、そこはさすがに Google も考慮していました(まあ、当然ですかね)

具体的には、新しい購読商品のレシートの「linkedPurchaseToken」項目に、前回購読商品の「purchaseToken」が記載されています。

購読商品purchaseTokenlinkedPurchaseToken
アップグレード前の購読商品自身の購入トークン項目なし
アップグレード後の購読商品自身の購入トークンアップグレード前の購読商品の購入トークン

よって、アップグレード時にネイティブアプリで取得したレシートを使って、Google の「Purchases.subscriptions API」を叩けば、「linkedPurchaseToken」がついてきているハズです。

これを手掛かりに、アップグレード前の購読商品の閲覧権限を無効化して、新しい購読商品の閲覧権限を有効化してあげれば問題ありません。

ちなみに、このアップグレード時は「SUBSCRIPTION_PURCHASED(4)」のデベロッパー通知のみが届きます。

このデベロッパー通知には、アップグレード後の「purchaseToken」が入っているので、ネイティブアプリからレシートを受け取らなくても対応は可能ですね。

この辺りは、コンテンツによって実装方針は異なると思うので、ここでは細かく触れないようにしておきます。どれが正解というものでもないですしね。

なお、ダウングレードも同様に考えておけば問題ありませんが、「purchaseToken」が切り替わるので、再度「Acknowledge API」で購読の承認が必要になります。

決済や購読期間の調整は Google が担当する部分なので、コンテンツ側が意識する必要がないのがいいですね。

遅延アップグレードとダウングレード

章の冒頭でも書きましたが、ここにきて Apple のダウングレードと同じく、Google にも遅延の商品変更が導入されました。

次の更新タイミングまでは既存のグレードのサービスを提供し、更新される際に購読商品を変更しちゃうというものです。

どのような挙動をするのかというと、公式ドキュメントには以下のように記載されています。

切り替えは次回契約期間に有効になります。このモードを使用した場合、ユーザーの Tier 1 定期購入は 4 月 30 日に有効期限が切れるまで継続します。5 月 1 日に Tier 2 定期購入が有効になり、新しい階層の定期購入の 300 円が課金されるようになります。

比例配分モードを設定する – 定期購入のアップグレードやダウングレードを許可する

言っていることはわからなくないのですが、まったく動作については触れられていません・・・。

要は実際に試せということですね。サンドボックス環境と本番環境で挙動が変わらないことを祈りつつ調査します。

実際には、アップグレードした時点では何の変更もなく、次回の期限がくるタイミングで新しい購読商品でサブスクリプションが更新という扱いになります。

このタイミングで「SUBSCRIPTION_RENEWED(2)」のデベロッパー通知のみが届きます。

本来なら「purchaseToken」が切り替わるので、再度「Acknowledge API」で購読の承認をしたいのですが、なぜか Google の API から返されるレシートでは承認済になっています。
(数日前は未承認で返ってきた気もするのですが)

ネイティブの方で取得するレシートは未承認なのに・・・。

また Google ストアアプリの定期購入のページが「DF-DFERH-01」のエラーになります。

挙動が怪しいので、後日、また検証してみたいと思います。

acknowledge しようとすると下記のエラーが返ってきたり。Google のサーバから返されるレシートは未承認の状態なんですけどね。

まとめ

Google(Android)のサブスクリプションにおける購入レシートの状態について調査してみました。

今回はサンドボックス環境を使って試したので、本番環境と挙動の違う部分も少なからずあると思います。

初めて Android アプリで課金を行う場合は、ドキュメントが頼りになりますが、やはり Google の公式ドキュメントだけを眺めていても不明点が多くなります。

実際にテストアプリを使って、トライアンドエラーをすることが近道ではないでしょうか。

Apple(iOS)や Amazon のアプリ課金も同様ですが、プラットフォームが違えば細かい部分の違いはどうしても発生してしまいます。

課金の実装が正しく行えないと売り上げに直で影響してしまうので、じっくり時間と予算をかけて取り組んでいきたいですね。