Google Play 請求サービスでサブスクリプションの変更を実装する

1. はじめに

この Codelab では、Google Play Billing Library(PBL)を使用してサブスクリプション プランの変更を管理する方法を学びます。さまざまな交換モードが料金とユーザーの利用資格に与える影響を理解し、バックエンドのリアルタイム デベロッパー通知(RTDN)を処理する方法を学びます。

対象

この Codelab は Android アプリ デベロッパーを対象としており、高度なサブスクリプション管理機能を実装するためのガイダンスを提供します。このガイダンスは、ユーザーがさまざまな定期購入プランをアップグレード、ダウングレード、移行する際にシームレスなエクスペリエンスを提供するために役立ちます。

学習内容

  • Google Play Console定期購入を作成する方法。
  • アプリのアップグレードとダウングレードのポリシーに合わせて適切な ReplacementModeWITH_TIME_PRORATIONDEFERRED など)を選択する方法。
  • プランの切り替えで Google Play の購入フローをトリガーするように launchBillingFlow 内の BillingFlowParams を構成する方法。
  • リアルタイム デベロッパー通知(RTDN)と purchases.subscriptionsv2 API を使用して、バックエンドで古いアクセス権を安全に取り消し、新しいアクセス権を付与する方法

必要なもの

2. サンプルアプリをビルドする

この Codelab では、サンプルの Android アプリを使用して、PBL でサブスクリプションの置き換えを実装する方法を説明します。このサンプルアプリは、次の側面を示す完全なソースコードを備えた、完全に機能する Android アプリとして設計されています。

  • アプリと PBL の統合
  • サブスクリプションの変更を実装する

定期購入の置き換えと PBL についてすでによくご存じの場合は、サンプルアプリをダウンロードして試してみてください。

次のデモ動画は、サンプルアプリをデプロイして実行した後の外観と動作を示しています。

前提条件

サンプルアプリをビルドしてデプロイする前に、次の操作を行います。

ビルド

Codelab に沿ってサンプルアプリをビルドするには:

  1. GitHub からサンプルアプリをダウンロードします。
  2. サンプルアプリの build.gradle 内の applicationId を更新して、Google Play Console のアプリのアプリケーション ID を反映させます。
  3. サンプルアプリをビルドします。
    : これにより、ローカル テスト用のアプリが正常にビルドされます。ただし、必要な定期購入が Google Play Console でまだ作成されていないため、アプリを実行しても商品と価格は取得されません。次のセクションでは、デベロッパー コンソールで定期購入を作成する方法について説明します。

3. Google Play Console で定期購入を作成する

Google Play の定期購入システムを使用して、定期購入の作成、管理、販売を柔軟に行うことができます。Google Play Console では、複数の基本プラン(それぞれに複数の特典付き)を持つ定期購入を設定できます。定期購入の特典には、さまざまな料金モデルと利用条件のオプションを設定できます。この Codelab では、プレミアム プランベーシック プランライト プランの 3 つの定期購入を作成し、さまざまな価格帯で一般的な定期購入サービスをシミュレートします。それぞれに月単位の繰り返し基本プランが 1 つずつ設定されます。

新しい登録の作成

新しい定期購入を作成するには

  1. Google Play Console を開き、[定期購入] ページ([Google Play で収益化] > [商品] > [定期購入])に移動します。
  2. [サブスクリプションを作成] をクリックします。
  3. 定期購入の詳細を入力します。
    • ProductID : 一意のアイテム ID を入力します。「premium_plan」と入力します。
    • 名前 : サブスクリプションの略称を入力します。例: Premium Plan
  4. [作成] をクリックします。

基本プランを作成する

  1. Google Play Console を開き、[定期購入] ページ([Google Play で収益化] > [商品] > [定期購入])に移動します。
  2. 基本プランを作成する定期購入の横にある右矢印をクリックして、定期購入の詳細を表示します。
  3. [基本プランを追加] をクリックします。
  4. 基本プラン ID を入力します。例: monthly-auto-renewing
  5. タイプとして [自動更新] を選択します。
  6. 自動更新の基本プランの場合は、次の項目を設定します。
    • 請求対象期間: 月単位
    • 猶予期間: 7 日間
    • お支払いプランと特典の変更: 請求日に請求する
    • 再度定期購入: 許可
  7. [価格と在庫状況] セクションで、[価格を設定] をクリックして基本プランの価格を設定します。
  8. すべての国と地域を選択し、[価格を設定] をクリックします。
  9. この基本プランの価格を $10 に設定し、[更新] をクリックします。
  10. 基本プランの価格を設定したら、右下の [保存]、[有効にする] の順にクリックします。

サンプルアプリのサブスクリプションを作成する

この Codelab では、次の構成で 2 つのサブスクリプションを追加で作成します。

  • ベーシック プラン
    • 商品 ID: basic_plan
    • 名前: ベーシック プラン
    • 基本プラン ID: monthly-auto-renewing
    • 価格: $5
  • Lite プラン
    • 商品 ID: lite_plan
    • 名前: Lite プラン
    • 基本プラン ID: monthly-auto-renewing
    • 価格: $3

サンプルアプリは、これらのアイテム ID と基本プラン ID を使用するように構成されています。構成が異なる複数の定期購入を作成できます。その場合は、作成したプロダクト ID を使用するようにサンプルアプリを変更する必要があります。

定期購入の作成動画

次の動画では、Google Play デベロッパー コンソールで定期購入を作成する手順を説明しています。

4. 定期購入の切り替え

PBL と統合しているデベロッパーは、既存の定期購入者に、ニーズに合わせて定期購入プランを変更するためのさまざまなオプションを提供できます。

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

ユーザーが定期購入をアップグレード、ダウングレード、変更する場合は、交換モードを指定します。交換モードでは、現在の請求対象期間の比例配分額がどのように適用されるか、またユーザーの利用資格が変更される時期が決定されます。

Play Billing Library には、この動作を制御するための ReplacementMode オプションがいくつか用意されています。

利用可能な交換モード

  • WITH_TIME_PRORATION: 定期購入アイテムは直ちにアップグレードまたはダウングレードされます。残りの期間は価格の差に応じて調整され、次回の請求日が更新されて新しい定期購入に充当されます。これがデフォルトの動作です
  • CHARGE_PRORATED_PRICE: 定期購入アイテムは直ちにアップグレードされますが、請求期間は変わりません。ユーザーには残りの期間の差額が請求されます。
  • CHARGE_FULL_PRICE: 定期購入アイテムは直ちにアップグレードまたはダウングレードされ、新しい利用資格の全額が直ちに請求されます。以前の定期購入の残額は、同じ利用資格に引き継がれるか、別の利用資格への切り替え時に期間内で比例配分されます。
  • WITHOUT_PRORATION: 定期購入アイテムは直ちにアップグレードまたはダウングレードされ、定期購入の更新時に新しい価格が請求されます。請求期間は変わりません。
  • DEFERRED: 定期購入アイテムは、定期購入の更新時にのみアップグレードまたはダウングレードされます。

5. WITH_TIME_PRORATION

この交換モードでは、定期購入アイテムは直ちにアップグレードまたはダウングレードされます。残りの期間は価格の差に応じて調整され、次回の請求日との差分が新しい定期購入に充当されます。これはデフォルトの動作です。

シナリオ例

ユーザーが月額料金 490 円のベーシック プランから月額料金 1,190 円のプレミアム プランに 4 月 15 日に切り替えた場合(月単位の請求期間の半ば)。

次のようになります。

  • ユーザーは Premium プランにすぐにアクセスできます。
  • Google Play では、按分期間が自動的に計算されます。たとえば、ベーシック プランの残り 15 日間が プレミアム プランの 7 日間に相当すると Google Play が判断した場合、次回の請求日は 4 月 21 日に繰り上げられます。
  • ユーザーによるすぐのお支払いは必要ありません。

コード スニペット

// ProductDetails for the plan to be switched to
ProductDetails productDetails = ...;
// The specific offer token for the toBeSwitched plan's base plan
String offerToken = "...";
// The purchase token of the user's current subscription
String oldPurchaseToken = "...";
// The productId for the user's current subscription
String oldProductId = "...";
// The replacementMode to replace the user's subscription
int replacementMode = SubscriptionProductReplacementParams.ReplacementMode.WITH_TIME_PRORATION;

SubscriptionProductReplacementParams subscriptionProductReplacementParams =
    SubscriptionProductReplacementParams.newBuilder()
        .setOldProductId(oldProductId)
        .setReplacementMode(replacementMode)
        .build();

ProductDetailsParams productDetailsParams =
    ProductDetailsParams.newBuilder()
        .setProductDetails(productDetails)
        .setSubscriptionProductReplacementParams(subscriptionProductReplacementParams)
        .setOfferToken(offerToken)
        .build();

List<ProductDetailsParams> productDetailsParamsList = ImmutableList.of(productDetailsParams);

BillingFlowParams billingFlowParams =
    BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(productDetailsParamsList)
        .setSubscriptionUpdateParams(
            SubscriptionUpdateParams.newBuilder().setOldPurchaseToken(oldPurchaseToken).build())
        .build();

billingClient.launchBillingFlow(activity, billingFlowParams);

WITH_TIME_PRORATION でアップグレード

このシナリオをシミュレートするには:

  • サンプルアプリの MainActivity で、コード スニペットの replacementModeSubscriptionProductReplacementParams.ReplacementMode.WITH_TIME_PRORATION に更新します。
  • アプリケーションを再ビルドして起動します。
  • Google Play ストアで既存の定期購入(ある場合)を解約し、有効期限が切れるまで待ちます。
  • ベーシック プランを購入します。
  • Premium プランに切り替えます。

ユーザーの利用資格はすぐに Premium プランにアップグレードされます。お客様がすぐに支払う金額は $0.00 です。ベーシック プランの残りの期間は、プレミアム プランの期間に按分され、次回の更新日が繰り上げられます。お客様は、新たに調整された請求日に更新料の 9.99 ドルを請求されます。

WITH_TIME_PRORATION でダウングレードする

このシナリオをシミュレートするには:

  • サンプルアプリの MainActivity で、コード スニペットの replacementModeSubscriptionProductReplacementParams.ReplacementMode.WITH_TIME_PRORATION に更新します。
  • アプリケーションを再ビルドして起動します。
  • Google Play ストアで既存の定期購入(ある場合)を解約し、有効期限が切れるまで待ちます。
  • ベーシック プランを購入します。
  • Lite プランに切り替えます。

ユーザーの利用資格はすぐに Lite プランにダウングレードされます。即時の支払い金額は 0 米ドルです。ベーシック プランの残りの金額は、ライト プランの時間に按分されるため、次回の更新日が大幅に延長されます。お客様は、新たに調整された請求日に更新料 2.99 ドルを請求されます。

まとめ

このセクションでは、WITH_TIME_PRORATION が価格差に基づいて次の更新までの期間を調整することで、ユーザーの利用資格を即座に課金することなく変更する方法について説明しました。これは、ユーザーをアップグレードまたはダウングレードするための効果的なデフォルト戦略です。

6. CHARGE_PRORATED_PRICE

この切り替えモードでは、定期購入アイテムは直ちにアップグレードされ、請求期間は変わりません。ユーザーには残りの期間の差額が請求されます。

注: このオプションは、時間単位の料金が引き上げられる定期購入アイテムのアップグレードでのみ利用できます。

シナリオ例

ベーシック プラン(月額 $4.99)のユーザーが、月額請求期間の残り約 10 日の 4 月 20 日に、プレミアム プラン(月額 $9.99)にアップグレードすることにしました。

次のようになります。

  • ユーザーは Premium プランにすぐにアクセスできます。
  • ユーザーには、現在の請求期間の残りの 10 日間の差額が日割り計算されてすぐに請求されます。これは約 2.99 ドルに相当し、プレミアム プランの 10 日分を表します。
  • ユーザーの請求日は変更されません。

コード スニペット

// ProductDetails for the plan to be switched to
ProductDetails productDetails = ...;
// The specific offer token for the toBeSwitched plan's base plan
String offerToken = "...";
// The purchase token of the user's current subscription
String oldPurchaseToken = "...";
// The productId for the user's current subscription
String oldProductId = "...";
// The replacementMode to replace the user's subscription
int replacementMode = SubscriptionProductReplacementParams.ReplacementMode.CHARGE_PRORATED_PRICE;

SubscriptionProductReplacementParams subscriptionProductReplacementParams =
    SubscriptionProductReplacementParams.newBuilder()
        .setOldProductId(oldProductId)
        .setReplacementMode(replacementMode)
        .build();

ProductDetailsParams productDetailsParams =
    ProductDetailsParams.newBuilder()
        .setProductDetails(productDetails)
        .setSubscriptionProductReplacementParams(subscriptionProductReplacementParams)
        .setOfferToken(offerToken)
        .build();

List<ProductDetailsParams> productDetailsParamsList = ImmutableList.of(productDetailsParams);

BillingFlowParams billingFlowParams =
    BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(productDetailsParamsList)
        .setSubscriptionUpdateParams(
            SubscriptionUpdateParams.newBuilder().setOldPurchaseToken(oldPurchaseToken).build())
        .build();

billingClient.launchBillingFlow(activity, billingFlowParams);

CHARGE_PRORATED_PRICE でアップグレード

このシナリオをシミュレートするには:

  • サンプルアプリの MainActivity で、コード スニペットの replacementModeSubscriptionProductReplacementParams.ReplacementMode.CHARGE_PRORATED_PRICE に更新します。
  • アプリケーションを再ビルドして起動します。
  • Google Play ストアで既存の定期購入(ある場合)を解約し、有効期限が切れるまで待ちます。
  • ベーシック プランを購入します。
  • Premium プランに切り替えます。

ユーザーはすぐに Premium プランにアップグレードされ、元の更新日は維持されます。すぐに支払う金額は、現在のサイクルの残りの日数について、プレミアム プランとベーシック プランの料金の差額を日割り計算したものです。更新日には、Premium の更新料金である $9.99 が請求されます。

CHARGE_PRORATED_PRICE でダウングレード

このシナリオをシミュレートするには:

  • サンプルアプリの MainActivity で、コード スニペットの replacementModeSubscriptionProductReplacementParams.ReplacementMode.CHARGE_PRORATED_PRICE に更新します。
  • アプリケーションを再ビルドして起動します。
  • Google Play ストアで既存の定期購入(ある場合)を解約し、有効期限が切れるまで待ちます。
  • ベーシック プランを購入します。
  • Lite プランに切り替えます。

この置換モードは、時間単位の料金が引き上げられる定期購入アイテムのアップグレードでのみ利用できるため、ダウングレード時にエラーが発生します。請求フローが失敗し、ダウングレードでは比例配分モードが対象外であることを示すエラーがユーザーに表示されます。

まとめ

このセクションでは、CHARGE_PRORATED_PRICE を使用すると、請求サイクルを変更せずに、残りの請求対象期間の差額をユーザーに請求することで、すぐにアップグレードできる仕組みについて説明しました。これは、請求日を変更せずに、より高額なプランにアップグレードしたい場合に便利です。

7. CHARGE_FULL_PRICE

この切り替えモードでは、定期購入アイテムは直ちにアップグレードまたはダウングレードされ、新しい利用資格の全額が直ちに請求されます。以前の定期購入の残額は、同じ利用資格に引き継がれるか、別の利用資格への切り替え時に期間内で比例配分されます。

シナリオ例

ユーザーは Basic プラン(4 月 1 日より月額 $4.99)を利用しています。4 月 20 日に、お客様は Premium プラン(月額 $9.99)に切り替えることを希望しています。

次のようになります。

  • Premium プランの全額($9.99)が直ちに請求されます。
  • ベーシック プランの残りの値(10 日分など)は、プレミアム プランの同等の時間に変換されます。この例では、ベーシックの 10 日間はプレミアムの 5 日間に相当します。
  • ユーザーの次回の更新日は、この日割り計算された期間を含めるように調整されます。そのため、更新日は 5 月 25 日(4 月 20 日 + 1 か月 + 5 日)になります。
  • その後の更新は、5 月 25 日から毎月行われます。

コード スニペット

// ProductDetails for the plan to be switched to
ProductDetails productDetails = ...;
// The specific offer token for the toBeSwitched plan's base plan
String offerToken = "...";
// The purchase token of the user's current subscription
String oldPurchaseToken = "...";
// The productId for the user's current subscription
String oldProductId = "...";
// The replacementMode to replace the user's subscription
int replacementMode = SubscriptionProductReplacementParams.ReplacementMode.CHARGE_FULL_PRICE;

SubscriptionProductReplacementParams subscriptionProductReplacementParams =
    SubscriptionProductReplacementParams.newBuilder()
        .setOldProductId(oldProductId)
        .setReplacementMode(replacementMode)
        .build();

ProductDetailsParams productDetailsParams =
    ProductDetailsParams.newBuilder()
        .setProductDetails(productDetails)
        .setSubscriptionProductReplacementParams(subscriptionProductReplacementParams)
        .setOfferToken(offerToken)
        .build();

List<ProductDetailsParams> productDetailsParamsList = ImmutableList.of(productDetailsParams);

BillingFlowParams billingFlowParams =
    BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(productDetailsParamsList)
        .setSubscriptionUpdateParams(
            SubscriptionUpdateParams.newBuilder().setOldPurchaseToken(oldPurchaseToken).build())
        .build();

billingClient.launchBillingFlow(activity, billingFlowParams);

CHARGE_FULL_PRICE でアップグレードする

このシナリオをシミュレートするには:

  • サンプルアプリの MainActivity で、コード スニペットの replacementModeSubscriptionProductReplacementParams.ReplacementMode.CHARGE_FULL_PRICE に更新します。
  • アプリケーションを再ビルドして起動します。
  • Google Play ストアで既存の定期購入(ある場合)を解約し、有効期限が切れるまで待ちます。
  • ベーシック プランを購入します。
  • Premium プランに切り替えます。

ユーザーはすぐに Premium プランにアップグレードされます。すぐに支払う金額は、Premium プランの正規料金である $9.99 です。ベーシック プランの残りの価値は、新しいプレミアム プランの時間に変換され、最初の更新日がわずかに延長されます。その後は、1 サイクルあたり $9.99 の更新料金が適用されます。

CHARGE_FULL_PRICE でダウングレード

このシナリオをシミュレートするには:

  • サンプルアプリの MainActivity で、コード スニペットの replacementModeSubscriptionProductReplacementParams.ReplacementMode.CHARGE_FULL_PRICE に更新します。
  • アプリケーションを再ビルドして起動します。
  • Google Play ストアで既存の定期購入(ある場合)を解約し、有効期限が切れるまで待ちます。
  • ベーシック プランを購入します。
  • Lite プランに切り替えます。

ユーザーはすぐに Lite プランにダウングレードされ、新しい請求期間が開始されます。すぐに支払う金額は、目標価格の $2.99 全額です。ベーシック プランの未使用分は、新しい Lite プランの時間に日割り計算され、最初の更新日が延長されます。その後は、1 サイクルあたり $2.99 で更新されます。

まとめ

このセクションでは、CHARGE_FULL_PRICE が切り替え当日にユーザーに全額を自己負担で請求し、すぐに新しい請求期間を開始する方法について説明しました。以前のプランの残高は、次回の更新日まで均等に適用されます。

8. WITHOUT_PRORATION

この切り替えモードでは、定期購入アイテムは直ちにアップグレードまたはダウングレードされ、定期購入の更新時に新しい価格が請求されます。

シナリオ例

ユーザーは Basic プラン(4 月 1 日より月額 $4.99)を利用しています。4 月 20 日に、お客様は Premium プラン(月額 $9.99)に切り替えることを希望しています。

次のようになります。

  • ユーザーは Premium プランにすぐにアクセスできます。
  • ユーザーは、次の定期購入更新日(5 月 1 日)まで、990 円の料金を支払う必要はありません。

コード スニペット

// ProductDetails for the plan to be switched to
ProductDetails productDetails = ...;
// The specific offer token for the toBeSwitched plan's base plan
String offerToken = "...";
// The purchase token of the user's current subscription
String oldPurchaseToken = "...";
// The productId for the user's current subscription
String oldProductId = "...";
// The replacementMode to replace the user's subscription
int replacementMode = SubscriptionProductReplacementParams.ReplacementMode.WITHOUT_PRORATION;

SubscriptionProductReplacementParams subscriptionProductReplacementParams =
    SubscriptionProductReplacementParams.newBuilder()
        .setOldProductId(oldProductId)
        .setReplacementMode(replacementMode)
        .build();

ProductDetailsParams productDetailsParams =
    ProductDetailsParams.newBuilder()
        .setProductDetails(productDetails)
        .setSubscriptionProductReplacementParams(subscriptionProductReplacementParams)
        .setOfferToken(offerToken)
        .build();

List<ProductDetailsParams> productDetailsParamsList = ImmutableList.of(productDetailsParams);

BillingFlowParams billingFlowParams =
    BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(productDetailsParamsList)
        .setSubscriptionUpdateParams(
            SubscriptionUpdateParams.newBuilder().setOldPurchaseToken(oldPurchaseToken).build())
        .build();

billingClient.launchBillingFlow(activity, billingFlowParams);

WITHOUT_PRORATION でアップグレードする

このシナリオをシミュレートするには:

  • サンプルアプリの MainActivity で、コード スニペットの replacementModeSubscriptionProductReplacementParams.ReplacementMode.WITHOUT_PRORATION に更新します。
  • アプリケーションを再ビルドして起動します。
  • Google Play ストアで既存の定期購入(ある場合)を解約し、有効期限が切れるまで待ちます。
  • ベーシック プランを購入します。
  • Premium プランに切り替えます。

ユーザーは、既存の更新日を維持したまま、すぐに Premium プランにアップグレードされます。即時の支払い金額は 0 米ドルです。ユーザーは、次の請求日に新しい更新料金の $9.99 に切り替わるまで、指定された期間の残りの間 Premium プランを利用できます。

WITHOUT_PRORATION でダウングレードする

このシナリオをシミュレートするには:

  • サンプルアプリの MainActivity で、コード スニペットの replacementModeSubscriptionProductReplacementParams.ReplacementMode.WITHOUT_PRORATION に更新します。
  • アプリケーションを再ビルドして起動します。
  • Google Play ストアで既存の定期購入(ある場合)を解約し、有効期限が切れるまで待ちます。
  • ベーシック プランを購入します。
  • Lite プランに切り替えます。

ユーザーは直ちに Lite プランにダウングレードされ、購入した Basic 機能を利用できなくなります。即時の支払い金額は 0 米ドルです。請求期間は変更されず、ユーザーは次回の更新日に新しい値下げ料金の $2.99 を支払います。

まとめ

このセクションでは、WITHOUT_PRORATION がユーザーの利用資格を直ちに切り替え、請求期間を変更せずにチェックアウト料金を請求しない方法について説明しました。

9. DEFERRED

この置き換えモードでは、定期購入アイテムは定期購入の更新時にのみアップグレードまたはダウングレードされますが、新しい購入は直ちに発行されます。既存のアイテムは更新不可に設定され、現在の請求期間の終了時に期限切れになります。一方、新たにリクエストされた利用資格は、その直後に開始されます。

シナリオ例

ユーザーは Basic プラン(4 月 1 日より月額 $4.99)を利用しています。4 月 20 日に、お客様は Premium プラン(月額 $9.99)に切り替えることを希望しています。

次のようになります。

  • お客様に直ちに請求が発生することはありません。
  • 現在の請求期間(4 月 30 日)が終了するまで、ベーシック機能を引き続きご利用いただけます。
  • サブスクリプション プランは、次回の更新日(5 月 1 日)に自動的に Premium にアップグレードされます。

コード スニペット

// ProductDetails for the plan to be switched to
ProductDetails productDetails = ...;
// The specific offer token for the toBeSwitched plan's base plan
String offerToken = "...";
// The purchase token of the user's current subscription
String oldPurchaseToken = "...";
// The productId for the user's current subscription
String oldProductId = "...";
// The replacementMode to replace the user's subscription
int replacementMode = SubscriptionProductReplacementParams.ReplacementMode.DEFERRED;

SubscriptionProductReplacementParams subscriptionProductReplacementParams =
    SubscriptionProductReplacementParams.newBuilder()
        .setOldProductId(oldProductId)
        .setReplacementMode(replacementMode)
        .build();

ProductDetailsParams productDetailsParams =
    ProductDetailsParams.newBuilder()
        .setProductDetails(productDetails)
        .setSubscriptionProductReplacementParams(subscriptionProductReplacementParams)
        .setOfferToken(offerToken)
        .build();

List<ProductDetailsParams> productDetailsParamsList = ImmutableList.of(productDetailsParams);

BillingFlowParams billingFlowParams =
    BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(productDetailsParamsList)
        .setSubscriptionUpdateParams(
            SubscriptionUpdateParams.newBuilder().setOldPurchaseToken(oldPurchaseToken).build())
        .build();

billingClient.launchBillingFlow(activity, billingFlowParams);

DEFERRED でアップグレードする

このシナリオをシミュレートするには:

  • サンプルアプリの MainActivity で、コード スニペットの replacementModeSubscriptionProductReplacementParams.ReplacementMode.DEFERRED に更新します。
  • アプリケーションを再ビルドして起動します。
  • Google Play ストアで既存の定期購入(ある場合)を解約し、有効期限が切れるまで待ちます。
  • ベーシック プランを購入します。
  • Premium プランに切り替えます。

お客様は、現在の請求期間が終了するまで ベーシック プランのままとなります。即時の支払い金額は 0 米ドルです。更新日になると、利用資格が Premium プランにアップグレードされ、新しい更新額である 9.99 ドルが請求されます。

DEFERRED でダウングレード

このシナリオをシミュレートするには:

  • サンプルアプリの MainActivity で、コード スニペットの replacementModeSubscriptionProductReplacementParams.ReplacementMode.DEFERRED に更新します。
  • アプリケーションを再ビルドして起動します。
  • Google Play ストアで既存の定期購入(ある場合)を解約し、有効期限が切れるまで待ちます。
  • ベーシック プランを購入します。
  • Lite プランに切り替えます。

お客様は、現在の請求期間が終了するまで ベーシック プランのままとなります。即時の支払い金額は 0 米ドルです。更新日に、利用資格が Lite プランにアップグレードされ、新しい更新料金($2.99)が請求されます。

まとめ

このセクションでは、DEFERRED 置換モードで、アクティブ ユーザーの有料期間が終了するまでアップグレードまたはダウングレードが延期される仕組みについて説明しました。そのため、すでに購入した機能を失わないようにダウングレードする場合に特に適しています。

10. バックエンドとクライアントサイドの処理

ユーザーが定期購入の置き換えに成功したら、アプリとバックエンドの両方で変更が正しく処理されていることを確認し、サービスの中断や二重請求などの問題が発生しないようにしてください。

  • ユーザーは ベーシック月額プラン(product_id basic_plan、purchase_token basic_purchase_token_123)を利用しています。
  • ユーザーが即時交換モード(WITHOUT_PRORATIONWITH_TIME_PRORATIONCHARGE_PRORATED_PRICECHARGE_FULL_PRICE のいずれか)を使用して Premium プランに切り替える
  • サブスクリプションの切り替えが成功すると、Google はそれを新しいサブスクリプションとして扱い、プレミアム プラン(product_id premium_plan、purchase_token premium_purchase_token_123)の新しい購入トークンを作成します。

クライアントサイド処理

onPurchasesUpdated

交換品の購入が完了すると、PurchasesUpdatedListener がトリガーされます。これは切り替えですが、Google Play では Premium プランは新規購入として扱われます。

アプリは、premium_purchase_token_123 購入トークンと product_id premium_plan を含む Purchase オブジェクトを受け取ります。このユーザーは新規購読者として扱い、トークンを検証してアクセス権を付与する準備をします。

@Override
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
    if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
        for (Purchase purchase : purchases) {
            // purchase.getPurchaseToken() = premium_purchase_token_123
            // purchase.getProducts() will contain premium_plan
            // Verify the purchase and grant entitlement
            handleNewPurchase(purchase);
        }
    }
}

queryPurchasesAsync

queryPurchasesAsync は、アプリから購入された有効な定期購入のみを返します。ユーザーに表示する利用資格を判断するには、このメソッドを使用する必要があります。即時交換の場合、queryPurchasesAsync() は以前の BASIC 購入トークンの返却を停止し、新しい PREMIUM 購入トークンのみを返却するようになります。

アプリが再開されたときや購入が完了したときは、必ずこのメソッドを呼び出してください。Premium トークンが存在する場合は、Premium 機能をすぐに付与し、Basic 機能を削除します。

バックエンド処理(RTDN)

交換が発生すると、Google Play は構成された Pub/Sub トピックにリアルタイム デベロッパー通知(RTDN)を送信します。

  • 即時交換の場合、Google は新しい購入トークンを含む SUBSCRIPTION_PURCHASED RTDN を送信します。RTDN ペイロードのサンプル
    {
      "version":"1.0",
      "packageName":"com.google.play.billing.samples.subscriptions",
      "eventTimeMillis":"...",
      "subscriptionNotification":
      {
        "version":"1.0",
        "notificationType":4, // SUBSCRIPTION_PURCHASED
        "purchaseToken":"premium_purchase_token_123" //purchase token for the new subscription
      }
    }
    
  • サーバーが RTDN から新しい購入トークンを受け取ったら、新しい購入トークンを使用して purchases.subscriptionsV2 API を呼び出し、購入の詳細情報を取得します。API レスポンスには linkedPurchaseToken フィールドが含まれています。このフィールドは、購入トークンが新しい定期購入の購入を指すか、定期購入の交換を指すかを判断するために使用されます。
  • 定期購入の交換の場合、linkedPurchaseToken は古い定期購入の購入トークンを指します。このシナリオでは basic_purchase_token_123 になります。GET purchases.subscriptionsV2 レスポンスのサンプル
    curl \
    'https://androidpublisher.googleapis.com/androidpublisher/v3/applications/<application_id>/purchases/subscriptionsv2/tokens/premium_purchase_token_123' \
    --header 'Authorization: Bearer [YOUR_ACCESS_TOKEN]' \
    --header 'Accept: application/json'
    
    {
      "kind": "androidpublisher#subscriptionPurchaseV2",
      "startTime": "...",
      "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
      "latestOrderId": "GPA.<order_id>",
      "linkedPurchaseToken": "basic_purchase_token_123", // The purchase token of the subscription that was replaced (Basic Plan in this case)
      "acknowledgementState": "ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED",
      "lineItems": [
        {
          "productId": "premium_plan", // productID of the new subscription (Premium Plan in this case)
          "expiryTime": "....",
          "autoRenewingPlan": {...},
          "offerDetails": {
            "basePlanId": "monthly-auto-renewing" // base plan ID of the new subscription
          },
          "itemReplacement": { // Details about the subscription replacement
            "productId": "subscription_basic", // productID of the old subscription (Basic Plan in this case)
            "replacementMode": "CHARGE_PRORATED_PRICE", // Replacement strategy used for this subscription change
            "basePlanId": "monthly-auto-renewing" // base plan ID of the old subscription 
          },
          "offerPhase": {...}
        }
      ],
      "etag": "<etag_value>"
    }
    
  • 新しい Premium の購入を承認する必要があります。これは、アプリ内またはバックエンドで行うことができます。3 日以内に購入を承認しなかった場合、払い戻しが行われ、利用資格が取り消されます。購入の処理と承認について詳しくは、デベロッパー向けドキュメントをご覧ください。

まとめ

このセクションでは、クライアントとバックエンドの両方で定期購入の即時交換を処理する手順について説明しました。Google Play は新しいプランを新規購入として扱い、新しい購入トークンを発行します。クライアント側では、PurchasesUpdatedListener を使用してこの新しい購入を処理し、queryPurchasesAsync からのレスポンスに基づいて利用資格を更新する必要があります。バックエンドでは、新しいトークンの SUBSCRIPTION_PURCHASED RTDN をリッスンし、purchases.subscriptionsv2 API を使用して古い定期購入の linkedPurchaseToken を特定し、新しい利用資格を付与すると同時に、古いトークンに関連付けられたアクセス権を速やかに取り消す必要があります。新しい購入は必ず承認してください。

11. DEFERRED Replacements(延期された交換)の手続き

即時交換モードとは異なり、ReplacementMode.DEFERRED では、サブスクリプションの変更と利用資格の更新は現在の請求期間の終了まで延期されます。遅延交換を処理するには、ユーザーが適切なタイミングで正しい利用資格を受け取れるようにするための特定のロジックが必要です。

  • ユーザーは、4 月 15 日に更新される月額プラン(商品 ID basic_plan、購入トークン basic_purchase_token_123)のベーシック プランを利用しています。
  • 4 月 1 日に、ユーザーは ReplacementMode.DEFERRED を使用して Premium プランに切り替えることにしました。
  • Google は、Premium プラン(product_id premium_plan、purchase_token premium_purchase_123)の新規購入トークンをすぐに作成しますが、ユーザーに請求する金額と利用資格は 4 月 15 日に設定されます。

交換延期の手続きを行う

1. 購入フロー完了直後(アプリ)

  • 購入フローが完了すると、PurchasesUpdatedListener が呼び出されます。アプリは、新しい購入トークン premium_purchase_token_123 を含む Purchase オブジェクトを受け取りますが、ユーザーは ベーシック プランの利用資格のみを有しているため、product_id は以前の basic_plan を参照します。これは新規購入とまったく同じように扱い、トークンを承認する必要があります。
    @Override
    public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
        if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {
            for (Purchase purchase : purchases) {
                // purchase.getPurchaseToken() = premium_purchase_token_123
                // purchase.getProducts() will contain basic_plan
                // Verify and acknowledge the purchase
                handleNewPurchase(purchase);
            }
        }
    }
    
  • queryPurchasesAsync は、直ちに新しい購入トークン(premium_purchase_token_123)と、それに関連付けられている元の利用資格(basic_plan)を持つ購入を返します。このため、ユーザーに Basic プランの利用資格を付与し続けることができます。

2. 購入フロー完了直後(バックエンド)

  • 購入フロー直後、新しい購入トークン(premium_purchase_token_123)について SUBSCRIPTION_PURCHASED RTDN が送信されます。RTDN ペイロードのサンプル
    {
      "version":"1.0",
      "packageName":"com.google.play.billing.samples.subscriptions",
      "eventTimeMillis":"...",
      "subscriptionNotification":
      {
        "version":"1.0",
        "notificationType":4, // SUBSCRIPTION_PURCHASED
        "purchaseToken":"premium_purchase_token_123" //purchase token for the new subscription
      }
    }
    
  • 新しい購入トークンを使用して GET purchases.subscriptionsv2 を呼び出し、購入の詳細を取得します。レスポンスには 2 つの項目が含まれます。
    • 以前の定期購入(基本プラン)を表す項目。expiryTime は将来の日時です。古い定期購入は更新されず、新しい定期購入(プレミアム プラン)を含む deferredItemReplacement があります。これは、古い利用資格の期限切れ時に保留中の置き換えがあることを示します。
    • 1 つは新たに購入したサブスクリプションを表します。expiryTime' に値が設定されていません
    API レスポンスのサンプル
    {
      "kind": "androidpublisher#subscriptionPurchaseV2",
      "startTime": "2026-05-07T15:50:11.383Z",
      "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
      "latestOrderId": "GPA.<order_id>",
      "linkedPurchaseToken": "basic_purchase_token_123",
      "acknowledgementState": "ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED",
      "lineItems": [
        {
          "productId": "premium_plan", // Premium Plan has no expiry time
          "autoRenewingPlan": {...},
          "offerDetails": {...},
          "itemReplacement": {. // Subscription replacement details
            "productId": "basic_plan",
            "replacementMode": "DEFERRED",
            "basePlanId": "monthly-auto-renewing"
          },
          "offerPhase": {}
        },
        {
          "productId": "basic_plan", // Subscription to be replaced
          "expiryTime": "2026-05-07T15:54:34.768Z", // Expiry time in the future
          "autoRenewingPlan": {},
          "offerDetails": {...},
          "deferredItemReplacement": { // identifier indicating this subscription will be replaced upon renewal
            "productId": "subscription_premium"
          },
          "latestSuccessfulOrderId": "GPA.<order_id>",
          "itemReplacement": {...},
        }
      ],
      "etag": "<etag>"
    }
    
  • 新しい購入トークンを承認する必要があります。これは、アプリ内またはバックエンドで行うことができます。購入の処理と承認について詳しくは、デベロッパー向けドキュメントをご覧ください。
  • 以前の購入トークン(basic_purchase_token_123)について SUBSCRIPTION_EXPIRED RTDN が送信されます。RTDN ペイロードのサンプル
    {
      "version":"1.0",
      "packageName":"com.google.play.billing.samples.subscriptions",
      "eventTimeMillis":"...",
      "subscriptionNotification":
      {
        "version":"1.0",
        "notificationType":13, // SUBSCRIPTION_EXPIRED
        "purchaseToken":"basic_purchase_token_123" //purchase token for the old subscription
      }
    }
    
  • 以前の購入トークンを使用して GET purchases.subscriptionsv2 API を呼び出すと、期限切れと表示されます(SUBSCRIPTION_STATE_EXPIRED)。以前のプランの利用資格の残りの期間分は、新しい購入に移行されます。

3. 交換日 - 購入フロー後の初回更新(アプリ)

  • queryPurchasesAsync は、新しい購入トークン(premium_purchase_token_123)と、それに関連付けられている新しい定期購入(premium_plan)を持つ購入を返します。
  • 新しい購入は、購入フロー完了時にすでに処理されているはずです。そのため、アプリでは、ユーザーに正しい定期購入へのアクセス権が付与されていることを確認する以外、特別なアクションは行わないでください。

4. 交換日 - 購入フロー後の初回更新(バックエンド)

  • ReplacementMode.DEFERRED を使用すると、初回更新は SUBSCRIPTION_RENEWED RTDN を処理する他の更新の標準的な動作に従います。この場合、交換のための特別なロジックは必要ありません。
  • 新しい購入トークンを使用して GET purchases.subscriptionsv2 を呼び出し、購入の詳細を取得します。レスポンスには 2 つの項目が含まれます。
    • 以前の定期購入(基本プラン)を表す項目。expiryTime は過去の日時です。古い定期購入では、deferredItemReplacement フィールドに値が設定されなくなります。
    • expiryTime が将来の日付で、autoRenewEnabled フィールドが true に設定されている新しい定期購入を表す項目。
    API レスポンスのサンプル
    {
      "kind": "androidpublisher#subscriptionPurchaseV2",
      "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
      "latestOrderId": "GPA.<order_id>..0",
      "linkedPurchaseToken": "basic_purchase_token_123", // purchase token of the old subscription
      "acknowledgementState": "ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED",
      "lineItems": [
        {
          "productId": "premium_plan", // New subscription
          "expiryTime": "2026-05-07T16:00:09.437Z", // Expiry time set in the future
          "autoRenewingPlan": {
            "autoRenewEnabled": true, // Auto Renewing Flag set to True
            "recurringPrice": {...}
          },
          "offerDetails": {...},
          "latestSuccessfulOrderId": "GPA.<order_id>..0",
          "itemReplacement": {. // Details of the subscription replacement
            "productId": "basic_plan",
            "replacementMode": "DEFERRED",
            "basePlanId": "monthly-auto-renewing"
          },
          "offerPhase": {...}
        },
        {
          "productId": "basic_plan", // Old subscription, Does not contains the deferredItemReplacement field
          "expiryTime": "2026-05-07T15:54:34.768Z", // Expiry time set in the past
          "autoRenewingPlan": {},
          "offerDetails": {...},
          "latestSuccessfulOrderId": "GPA.<order_id>..0",
          "itemReplacement": {...},
        }
      ],
      "etag": "<etag>"
    }
    

まとめ

このセクションでは、ReplacementMode.DEFERRED に必要な独自の処理について詳しく説明します。即時モードとは異なり、利用資格の変更は現在の請求期間の終了時にのみ発生します。このセクションでは、アプリとバックエンドの両方で、初回購入を正しく処理し、新しいトークンを承認し、古い定期購入の有効期限が切れて新しい定期購入が有効になったときに利用資格の切り替えを管理するために必要な手順について説明しました。

12. Subscription Replacement Playground

サンプルアプリの置換 Playground 機能を使用すると、Google Play Console アカウントで設定した定期購入アイテムの定期購入のアップグレードとダウングレードをテストできます。このセクションでは、交換プレイグラウンド機能の使用方法について説明します。

セットアップ

置換プレイグラウンド機能を使用するには、次のことを確認してください。

  • build.gradle ファイルの packageId が、Google Play Console で構成されたアプリと一致している。
  • テストユーザー アカウントが Google Play Console でライセンス テスターとして登録されている。ライセンス テストの詳細については、アプリの課金の実装をテストするをご覧ください。

Subscription Replacement Playground

サンプルアプリには [Replacement Playground] タブが含まれており、定期購入の変更をシミュレートできます。Google Play Console で定義した定期購入をクエリし、さまざまな交換モードを使用して定期購入の切り替えをテストできます。このプレイグラウンドでは、さまざまなモードがサブスクリプションの請求サイクルと利用資格にどのように影響するかを把握できるため、ビジネスニーズに最適なオプションを判断できます。

プレイグラウンドを使用して置換をシミュレートする手順は次のとおりです。

  1. [プレイグラウンド] タブに移動します。
  2. 有効な定期購入がある場合: ハイライト表示されます。この定期購入が置き換えられます。

Playground ホーム

  1. 有効な定期購入がない場合: まず定期購入を購入する必要があります。
    • プレイグラウンドには、この Codelab 用に作成された BasicPremiumLite の各プランがデフォルトで表示されます。
    • Google Play Console で設定した他のプランでテストするには、[カスタムプランを追加] をクリックし、productIdbasePlanId で検索します。
    • 選択した定期購入を購入します。
    • 新しく購入した有効な定期購入が表示されます。カスタム サブスクリプションを追加する
  2. お客様が切り替えを希望されている切り替え先の定期購入を選択します。
  3. 移行の [置換モード] を選択します。

交換モードを選択する

  1. [Test Replacement] ボタンをクリックして、定期購入の置き換えをシミュレートします。
  2. Google Play 請求サービスのボトムシートが表示され、サブスクリプションの変更の詳細(即時請求や請求期間の調整など)が計算されて表示されます。

定期購入の交換請求カート

  1. トランザクションを完了します。
  2. Google Play ストア アプリの [定期購入の管理] ページにアクセスすると、有効な定期購入の変更内容と、更新日と料金の更新に関する詳細を確認できます。

Google Play ストアの定期購入の管理

13. 次のステップ

リファレンス ドキュメント

14. 完了

おめでとうございます!さまざまな日割り計算モードで定期購入の交換を実装し、プラン移行のバックエンド処理を構成しました。

学習した内容

  • 特定の置換モードで SubscriptionProductReplacementParams を構成する方法。
  • 即時アップグレードと遅延ダウングレードの違い。
  • RTDN を使用して linkedPurchaseToken を使用して古いサブスクリプション トークンを廃止する方法。

アンケート

この Codelab に関するフィードバックをお待ちしております。簡単なアンケートにご協力ください。