💰

Google Billing Libraryの定期購入や基本プランの切り替え検証

2023/11/09に公開

はじめに

Googleが提供する定期購入の仕組みには定期購入のアップグレード、ダウングレードがある。これは定期購入をしているユーザに対して以下のようなオプションを提供できる仕組みである。

・「基本」定期購入と「プレミアム」定期購入など、複数の定期購入の階層を販売する場合は、ユーザーが別の定期購入の基本プランまたは特典を購入することで、階層を切り替えられるようにできます。
・月間プランから年間プランへの切り替えなど、現在の請求対象期間を変えられるようにできます。
・また、ユーザーが自動更新プランとプリペイド プランを切り替えられるようにもできます。

引用元:ユーザーが定期購入をアップグレード、ダウングレード、変更できるようにする

定期購入の階層の切り合えとは?

・「基本」定期購入と「プレミアム」定期購入など、複数の定期購入の階層を販売する場合は、ユーザーが別の定期購入の基本プランまたは特典を購入することで、階層を切り替えられるようにできます。

ここで言う階層とは「基本」定期購入と「プレミアム」定期購入には機能差があり、「プレミアム」が「基本」の上位版のことを指していると思われる。例えば2023/11月時点のPlayStation Plusだと「エッセンシャル」「エクストラ」「プレミアム」の3つのプランがある。ユーザが下位の「エッセンシャル」を購読中に中位の「エクストラ」に切り替えたいといった場合にアップグレードとして階層を切り替えられるようなオプション。

PlayStation Plusサブスクリプション機能差異

機能 プレミアム
上位
エクストラ
中位
エッセンシャル
下位
毎月のフリープレイ ⭕️ ⭕️ ⭕️
オンラインマルチプレイ ⭕️ ⭕️ ⭕️
加入者限定割引 ⭕️ ⭕️ ⭕️
加入者限定コンテンツ ⭕️ ⭕️ ⭕️
クラウドストレージ ⭕️ ⭕️ ⭕️
シェアプレイ ⭕️ ⭕️ ⭕️
ゲームヘルプ ⭕️ ⭕️ ⭕️
ゲームカタログ ⭕️ ⭕️
Ubisoft + Classics ⭕️ ⭕️
クラシックスカタログ ⭕️
ゲームトライアル ⭕️
ストリーミングゲーム ⭕️

*2023/11月時点

参考:PlayStation Plus

請求対象期間の変更とは?

・月間プランから年間プランへの切り替えなど、現在の請求対象期間を変えられるようにできます。

現在の定期購入の利用期間を変更する場合に当たる。ここでも例としてPlayStation Plusを挙げるが、ユーザが「エッセンシャル」の1ヶ月を定期購入中に同じ「エッセンシャル」の3ヶ月へ期間を変更できるオプション。

PlayStation Plusサブスクリプション期間と値段

期間 プレミアム
上位
エクストラ
中位
エッセンシャル
下位
1ヶ月 ¥1,550 ¥1,300 ¥850
3ヶ月 ¥4,300 ¥3,600 ¥2,150
12ヶ月 ¥13,900 ¥11,700 ¥6,800

*2023/11月時点

参考:PlayStation Plus

検証

定期購入プランの切り替え検証として以下の内容を検証する。また Google Billing Library 6.0.1 を使用する。

  1. 同じ定期購入内での基本プラン切り替え
  2. 定期購入間でプランを切り替え(交換モードのオーバーライド)

検証環境(Google Play Console上での定期購入設定)

検証環境としてGoogle Play Console上にPlayStation Plusと同じような以下のように「エクストラ」と「エッセンシャル」の定期購入を追加・設定した。「エクストラ」が上位、「エッセンシャル」が下位である。

1. 同じ定期購入内での基本プラン切り替え

まず同じ定期購入内で基本プランを切り替えるケース(例:エッセンシャル1ヶ月からエッセンシャル3ヶ月へ切り替え)を対象として以下2つの内容を検証する。どちらもドキュメントの再度定期購入、または同じ定期購入内でのプランの切り替えを行う定期購入間でプランを切り替える、またはデフォルトの交換モードをオーバーライドするに記載されていることだが、改めて確認する。検証は内部アプリテストを使用する。そのため1ヶ月の定期購入であれば5分、3ヶ月の定期購入であれば10分で更新される。

A. 同じ定期購入内で基本プランを切り替える場合に SubscriptionUpdateParams をセットしない通常の購入フロー起動で基本プランが切り替わること(実装が特に不要なのかを確認する意図)。この時Google Play Console上で設定した交換モードが使用されること

B. 同じ定期購入内で基本プランを切り替える場合にGoogle Play Console上で設定した交換モードを実装によりオーバライドする。この時に SubscriptionUpdateParams に指定する交換モードに CHARGE_FULL_PRICE または WITHOUT_PRORATION 以外の交換モードが使えないこと

Google Play Console上の設定というのは以下のことを指す。「すぐに請求」が CHARGE_FULL_PRICE 、「次回の請求日に請求」がWITHOUT_PRORATION と同等なもの。

検証パターン&結果

検証パターンとそれぞれの結果は以下の通り。No.1~4が検証内容Aのものであり、No.5~9が検証内容Bのもの。

結果としては検証Aについて、同じ定期購入内で基本プランを切り替える場合は実装不要(コードによる交換モード指定不要)であった。ただし、Google Play Consoleで設定した交換モードが使用される。検証Bについては期待通り CHARGE_FULL_PRICE または WITHOUT_PRORATION 以外使えないことが確認できた。

No 定期購入中アイテム 切り替え先 Console上の交換モード設定 コード上の交換モード設定
1 Essential-1ヶ月 Essential-3ヶ月 すぐに請求(CHARGE_FULL_PRICE) なし
2 Essential-1ヶ月 Essential-3ヶ月 次回の請求日に請求(WITHOUT_PRORATION) なし
3 Essential-3ヶ月 Essential-1ヶ月 すぐに請求(CHARGE_FULL_PRICE) なし
4 Essential-3ヶ月 Essential-1ヶ月 次回の請求日に請求(WITHOUT_PRORATION) なし
5 Essential-1ヶ月 Essential-3ヶ月 すぐに請求(CHARGE_FULL_PRICE) WITHOUT_PRORATION
6 Essential-1ヶ月 Essential-3ヶ月 次回の請求日に請求(WITHOUT_PRORATION) CHARGE_FULL_PRICE
7 Essential-3ヶ月 Essential-1ヶ月 すぐに請求(CHARGE_FULL_PRICE) WITHOUT_PRORATION
8 Essential-3ヶ月 Essential-1ヶ月 次回の請求日に請求(WITHOUT_PRORATION) CHARGE_FULL_PRICE
9 Essential-1ヶ月 Essential-3ヶ月 すぐに請求(CHARGE_FULL_PRICE) WITH_TIME_PRORATION
No 切り替え画面 切り替え先購入画面 結果
1 18:32 1ヶ月を購入

18:34 3ヶ月へ切り替え

すぐに請求され、1ヶ月の残りの期間(3分)が3ヶ月(10分)に追加され、期間が合計13分になったこと*からCHARGE_FULL_PRICEが適用された
2 12:40 1ヶ月を購入

12:42 3ヶ月へ切り替え

12:45(12:40から5分後) 3ヶ月へ切り替え後の請求が来た

12:55 3ヶ月の更新請求が来た

1ヶ月が終了してから3ヶ月へ切り替わったためWITHOUT_PRORATIONが適用された
3 1:05 3ヶ月を購入

1:07 1ヶ月へ切り替え

1:20 1ヶ月の更新請求が来た

3ヶ月の残りの期間10-2=8分に加えて1ヶ月分の5分の合計13分が1:07から加算されて1:20に更新になる*

1:25 1ヶ月更新請求が来る

短い期間への変更の場合もCHARGE_FULL_PRICEが適用された
4 1:31 3ヶ月を購入

1:33 1ヶ月へ切り替え

1:41(1:31から10分後)1ヶ月へ切り替え後の請求が来た

1:46 1ヶ月の更新請求が来る

3ヶ月が終了してから1ヶ月へ切り替わったためWITHOUT_PRORATIONが適用された
5 19:29 1ヶ月を購入

19:31 3ヶ月へ切り替え

19:34(19:29から5分後) 3ヶ月へ切り替え後の請求が来た

19:44 3ヶ月の更新請求が来た

1ヶ月が終了してから3ヶ月へ切り替わったためWITHOUT_PRORATIONが適用された
6 19:56 1ヶ月を購入

19:58 3ヶ月へ切り替え

すぐに請求され、1ヶ月の残りの期間(3分)が3ヶ月(10分)に追加され、期間が合計13分になったこと*からCHARGE_FULL_PRICEが適用された

スクショ上は12分と表示されているのは操作で発生した誤差です
7 20:41 3ヶ月を購入

20:43 1ヶ月へ切り替え

20:51(20:41から10分後) 1ヶ月の請求が来た

3ヶ月が終了してから1ヶ月へ切り替わったためWITOU_PRORATIONが適用された
8 20:13 3ヶ月を購入

20:15 1ヶ月へ切り替え

20:28 1ヶ月の更新請求が来た

3ヶ月の残り期間の10-2=8分に加えて1ヶ月分の5分の合計13分が20:15から加算されて20:28に更新になる*

20:33 1ヶ月の更新請求が来た

短い期間への変更の場合もCHARGE_FULL_PRICEが適用された
9 DeveloperErrorで切り替えできない

*CHARGE_FULL_PRICEの細かい計算は省略しています。

コード上の交換モード設定がない場合の実装

公式ドキュメントの購入フローを起動するから引用

val productDetailsParamsList = listOf(
    BillingFlowParams.ProductDetailsParams.newBuilder()
        // retrieve a value for "productDetails" by calling queryProductDetailsAsync()
        .setProductDetails(productDetails)
        // to get an offer token, call ProductDetails.subscriptionOfferDetails()
        // for a list of offers that are available to the user
        .setOfferToken(selectedOfferToken)
        .build()
)

val billingFlowParams = BillingFlowParams.newBuilder()
    .setProductDetailsParamsList(productDetailsParamsList)
    .build()

// Launch the billing flow
val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams)

コード上の交換モード設定がある場合の実装

公式ドキュメントのアプリ内で定期購入の変更をトリガーするから引用

val offerToken = productDetails
        .getSubscriptionOfferDetails(selectedOfferIndex)
        .getOfferToken()

val billingParams = BillingFlowParams.newBuilder().setProductDetailsParamsList(
       listOf(
           BillingFlowParams.ProductDetailsParams.newBuilder()
               .setProductDetails(productDetails)
               .setOfferToken(offerToken)
               .build()
       )
       ).setSubscriptionUpdateParams(
           BillingFlowParams.SubscriptionUpdateParams.newBuilder()
               .setOldPurchaseToken("old_purchase_token")
               // これが交換モードの指定
               .setSubscriptionReplacementMode(
                 BillingFlowParams.ReplacementMode.CHARGE_FULL_PRICE
               )
               .build()
       ).build()

billingClient.launchBillingFlow(
    activity,
    billingParams
   )
// ...

launchBillingFlow の購入フローの起動で BillingFlowParamsSubscriptionUpdateParams をセットするかが差異となる。setSubscriptionReplacementMode が交換モードの指定。

2. 定期購入間でプランを切り替え(交換モードのオーバーライド)

今度は異なる定期購入間で基本プランを切り替えるまたは購入するケースとして以下の2つの内容を検証する。
同じく検証は内部アプリテストを使用する。そのため1ヶ月の定期購入であれば5分、3ヶ月の定期購入であれば10分で更新される。

A. 別の定期購入のプランに切り替える場合に SubscriptionUpdateParams をセットしない通常の購入フロー起動で別の定期購入のプランも定期購読されること

B. 別の定期購入のプランに切り替える場合にGoogle Play Console上で設定した交換モードを実装によりオーバライドする。この時に SubscriptionUpdateParams に指定する交換モードに CHARGE_FULL_PRICE または WITHOUT_PRORATION 以外の交換モードも使用できること

検証パターン&結果

検証パターンとそれぞれの結果は以下の通り。No.1が検証内容Aのものであり、No.2~5が検証内容Bのもの。

交換モード指定理由は以下の通り。CHARGE_FULL_PRICEWITHOUT_PRORATION は Google Play Consoleから設定できるモードなためスキップした。

  • No.2 は時間単位の料金が引き上げられる時のみ利用可能な CHARGE_PRORATED_PRICE を指定
  • No.3 は請求期間が短いものから長いものに引き上げられるケースとして CHARGE_FULL_PRICE を指定
  • No.4,5 は下位のものへのダウングレードなため DEFERRED を指定

結果は検証Aについては期待通り、定期購入中に別の定期購入を購入するとどちらも定期購入中になることが確認できた。検証Bについても期待通り、CHARGE_FULL_PRICE または WITHOUT_PRORATION 以外の交換モードも使用できることが確認できた。

No 定期購入中アイテム 切り替え先 Console上の設定 コード上の設定
1 Essential-1ヶ月 Extra-1ヶ月 次回の請求日に請求(WITHOUT_PRORATION) なし
2 Essential-1ヶ月 Extra-1ヶ月 次回の請求日に請求(WITHOUT_PRORATION) CHARGE_PRORATED_PRICE
3 Essential-1ヶ月 Extra-3ヶ月 次回の請求日に請求(WITHOUT_PRORATION) CHARGE_FULL_PRICE
4 Extra-1ヶ月 Essential-1ヶ月 次回の請求日に請求(WITHOUT_PRORATION) DEFERRED
5 Extra-3ヶ月 Essential-1ヶ月 次回の請求日に請求(WITHOUT_PRORATION) DEFERRED
No 切り替え画面 切り替え先購入画面 結果
1 なし Essential-1ヶ月とExtra-1ヶ月の2つが定期購入中となる
2 10:24 Essential-1ヶ月を購入

10:26 Extra-1ヶ月へ切り替え

Essential-1ヶ月とExtra-1ヶ月の差額が請求される(計算は補足1へ)

10:29(10:24から5分後) Extra-1ヶ月更新の請求が来た

請求日が変わらないことと差額発生したことからCHARGE_PRORATED_PRICEが適用されている
3 2:16 Esseantial-1ヶ月を購入

2:18 Extra-3ヶ月に切り替え

2:29 Extra-3ヶ月の更新請求が来た(計算は補足2へ)

2:39 Extra-3ヶ月の更新請求が来た

請求日が変わっていることからCHARGE_FULL_PRICEが適用されている
4 2:56 Extra-1ヶ月を購入

2:58 Essential-1ヶ月へ切り替え

Essential-1ヶ月に切り替わっているが承認はされていない状態
(補足3を参照)
5 3:04 Extra-3ヶ月を購入

3:06 Essential-1ヶ月へ切り替え

Essential-1ヶ月に切り替わっているが承認はされていない状態
(補足3を参照)

補足1: No.2の清算

メールにてEssential1ヶ月とExtra1ヶ月の差額が請求されることを確認した。
計算式は以下。経過時間は正確に何秒までは計測していなかったため、0.5分(30秒)はおおよそ。
メールの請求金額に大体近い金額になった。

Essential1ヶ月 = ¥850/5分 = ¥170/分
Extra1ヶ月 = ¥1300/5分 = ¥260/分

Essential1ヶ月の使用分 = ¥170/分 x 2.5分(経過時間) = ¥425
Essential1ヶ月の残高 = ¥850 - ¥425 = ¥425

Extra1ヶ月を使用する分 = ¥260/分 x (5分 - 2.5分(経過時間)) = ¥650
¥650 - ¥425 = ¥225

補足2:No.3の追加期間の計算

Essential1ヶ月 = ¥850/5分 = ¥170/分
Extra3ヶ月 = ¥3600/10分 = ¥360/分

Essential1ヶ月の使用分 = ¥170/分 x 2分(経過時間) = ¥340
Essential1ヶ月の残高 = ¥850 - ¥340 = ¥510
Essential1ヶ月の残高で支払えるExtra3ヶ月の日数 = ¥510 ➗ ¥360/分 = 1.41分

Extra3ヶ月 = 10分(Extra3ヶ月分) + 1.41(Essential1ヶ月残高で追加された分) = 11.41 ≒ 11分

補足3:No.4, 5のDEFERREDの挙動について

交換延期を処理するに記載のある通り、バックエンド側が必須になる模様。少なくともアプリではDEFERRED指定で定期購入を切り替えるとGoogle Playの定期購読では以下のように承認されていないものとして扱われ、時間経過で自動キャンセルとなってしまう。そのためバックエンド側で処理してもらう必要がある。

3. GracePeriod(猶予期間)やAccountHold(アカウント一時停止期間)中のプラン切り替え

次にGracePeriodやAccountHold期間中に基本プランを切り替えるケースとして以下の2つの内容を検証する。

A. GracePeriodやAccountHold期間中に同じ定期購入内のプラン切り替えができること

B. GracePeriodやAccountHold期間中に別の定期購入のプランへ切り替えができること

検証パターン&結果

検証パターンと結果は以下の通り。No.1, 2が検証内容Aのもの。No.3, 4が検証内容Bのもの。今回確認したい内容としてはプラン切り替えができるかどうかを確認したいため、交換モードは一律同じモードとした。

No 状態 定期購入中アイテム 切り替え先 Console上の設定 コード上の設定
1 GracePeriod Essential-1ヶ月 Essential-3ヶ月 次回の請求日に請求(WITHOUT_PRORATION) なし
2 AccountHold Essential-1ヶ月 Essential-3ヶ月 次回の請求日に請求(WITHOUT_PRORATION) なし
3 GracePeriod Essential-1ヶ月 Extra-1ヶ月 次回の請求日に請求(WITHOUT_PRORATION) WITHOUT_PRORATION
4 AccountHold Essential-1ヶ月 Extra-1ヶ月 次回の請求日に請求(WITHOUT_PRORATION) WITHOUT_PRORATION
No 切り替え画面 切り替え先購入画面 結果
1 Essential-3ヶ月へ切り替えられ、GracePeriodが解除された
2 Essential-3ヶ月へ切り替えられ、AccountHoldが解除された
3 Extra-1ヶ月へ切り替えられ、GracePeriodが解除された
4 なし Extra-1ヶ月へ切り替えられなかった(Extra-1ヶ月がEssential-1ヶ月とは別として購入された)
(補足参照)

補足:No.4 AccountHold中に別の定期購入のプランへ切り替えについて

Essential-1ヶ月からExtra-1ヶ月へのアップデートができなかった。Google Playの定期購入画面には以下のようにEssential-1ヶ月とExtra-1ヶ月が別で購入されていることを確認した。Essential-1ヶ月はAccountHold期間が終わると自動キャンセルされ、Extra-1ヶ月のみが残った。

理由としてはNo.4のケースではアップグレードする際にEssential-1ヶ月を購入したときのPurchaseTokenを取得する必要がある。しかし、queryPurchasesで購入をフェッチすることはAccountHold中はできない。またqueryPurchaseHistoryであれば、AccountHold中でも購入履歴からPurchaseTokenの取得は可能であるが、その購入自体が有効なのか、はたまたGracePeriodなのか、AccountHoldなのかといった状態を判別できない。

よって、AccountHold中のみqueryPurchaseHistoryを使って以前の購入からPurchaseTokenを取得するというロジックが組めない。そのためAccountHold中に別の定期購入へのプランの切り替えを実施した場合は、実質プランの切り替えではなく、新たな定期購入となる。(No.2のケースではプラン切り替えの判断をGoogle Play側に任せているためNo.4のようなケースは発生しないが、コードで交換モードを指定する場合はNo.4同様にPurchaseTokenがAccountHold中は取得できないため、新たな定期購入となると考える。)

まとめ

検証を通して以下のことを改めて確認できた。

  • 同じ定期購入内で基本プランを切り替える際はコード上の交換モードの指定せずに切り替えが可能。この時の交換モードは実質Google Play Console上で設定した交換モードが使われる。コード上で指定した場合はコード上で指定した交換モードでオーバライドされる。
  • 同じ定期購入内で基本プランを切り替える時に使える交換モードはコード上、Google Play Console ともに WITHOUT_PRORATION または CHARGE_FULL_PRICE
  • 定期購入中に別の定期購入をする際にコード上で SubscriptionUpdateParams をセットしない(交換モードを指定しない)場合は現在の定期購入中のものに加えて別の定期購入も購入中となる
  • 定期購入中に別の定期購入へ切り替える際にCHARGE_FULL_PRICE または WITHOUT_PRORATION 以外の交換モードが使用できることが確認できた。この時コード上で交換モードを指定している場合はGoogle Play Console上で設定した交換モードがオーバライドされる。
    • DEFERRED についてはアプリだけで完結できるものではなかった
  • (実装に依存するが)GracePeriodやAccountHold期間中にプランのアップデート・ダウングレードするとGracePeriodやAccountHoldが解除される

参考

Discussion