Android におけるサブスクリプションのマネタイズは、ここ数年で大きく進化してきました。2022 年にベースプランとオファーが導入されたことで、Google は開発者に対し、サブスクリプション商品をより柔軟に設計できる仕組みを提供しました。そして今回、Google は Subscription with Add-ons(マルチラインサブスクリプション、またはサブスクリプション向けマルチプロダクトチェックアウトとも呼ばれます)という新たな大きな一歩を踏み出しました。この機能により、複数のサブスクリプション商品を 1 回の購入にまとめて提供でき、ユーザ ーと開発者の双方にとって、よりシンプルで分かりやすい体験を実現できます。
この記事では、マルチラインサブスクリプションとは何かを整理したうえで、その効果的な活用戦略を紹介し、Play Billing Library を直接使った実装方法を順を追って解説します。最後に、RevenueCat を使うことで、この一連のプロセスをどのようにシンプルにできるのかを見ていきます。
マルチラインサブスクリプションとは?
Subscription with Add-ons とは、複数のサブスクリプション商品を 1 つにまとめ、単一のサブスクリプションとして購入・請求・管理できるようにする機能です。ベースとなるサブスクリプションと追加のプレミアム機能を、それぞれ別々に購入させる必要はなく、すべてを 1 回のチェックアウトフローで提供で きます。
たとえば、ベースとなる「Premium」プランを提供している音楽ストリーミングアプリを考えてみましょう。これまでは、「HiFi オーディオ」や「オフライン再生」といった追加機能を有料アドオンとして提供する場合、ユーザーはそれぞれを個別に購入・管理する必要がありました。その結果、決済は複数回に分かれ、更新日もバラバラになり、サブスクリプション管理画面には複数の項目が並ぶことになります。Subscription with Add-ons を使えば、ユーザーは「Premium」プランをベースに、必要なアドオンを自由に組み合わせて選択し、1 回の購入で完了できます。表示される価格は 1 つにまとまり、チェックアウトも 1 回だけ、更新日はすべて同一の日付に同期されます。
なお、用語は少し分かりづらいかもしれません。Google では正式名称として 「Subscription with Add-ons」 を使っていますが、1 回の購入に複数の明細(ラインアイテム)が含まれることから 「マルチラインサブスクリプション」、あるいはチェックアウト体験に焦点を当てて 「マルチプロダクトチェックアウト」 と呼ばれることもあります。いずれも、同じ機能を指しています。
バンドルの仕組み
ユーザーが Subscription with Add-ons を購入すると、商品リストの先頭にあるアイテムがベースアイテムとなり、それ以降に続くすべてのアイテムはアドオンとして扱われます。この区別は重要で、ベースアイテムがバンドル全体の挙動を決定します。たとえば、ベースとなるサブスクリプションが解約されると、紐づいているすべてのアドオンも自動的に解約されます。アドオンは、ベースサブスクリプションなしでは単独で存在することはできません。
請求サイクルの調整といった複雑な処理は、Google Play が自動的に行います。既存のサブスクリプションに新しいアドオンを追加した場合、Google Play はアドオンの更新日がベースアイテムと一致するように、日割り計算による請求額を算出します。その結果、最初の調整期間が終わると、バンドル内のすべてのアイテムが同じ更新日にまとめて更新されるようになります。同様に、ユーザーがアドオンを削除した場合でも、そのアドオンは現在の請求期間の終了までは引き続き利用できますが、次回の更新は行われません。
理解しておくべき主な制約
実装に進む前に、この機能の使い方を左右する重要な制約がいくつかあります。
- すべてのアイテムは同一の請求期間である必要があります。Subscription with Add-ons に含めるすべてのアイテムは、同じ請求期間でなければなりません。年額のベースサブスクリプションに月額のアドオンを組み合わせる、またはその逆はできません。ベースプランが月額課金の場合、すべてのアドオンも月額である必要があります。これは、Google Play がすべてのアイテムの更新日を同期させる必要があるためです。
- 自動更新サブスクリプションのみ対応しています。この機能は自動更新型サブスクリプションでのみ利用できます。期間が固定され、自動更新されないプリペイドサブスクリプションは、ベースアイテムとしてもアドオンとしても使用できません。
- 1 回の購入あたり最大 50 アイテムまでという上限があります。Subscription with Add-ons の 1 回の購入に含められるアイテム数は最大 50 件です。多くのアプリではこの上限に達することはありませんが、高度にモジュール化されたサブスクリプション設計を検討している場合は把握しておく必要があります。
- すべての地域で利用できるわけではありません。現時点のドキュメントでは、インドと韓国では Subscription with Add-ons がサポートされていません。これらの地域のユーザーには、代替となる購入フローを用意する必要があります。
- 一時停止と再開はサポートされていません。アドオンを含むサブスクリプションでは、サブスクリプションの一時停止および再開機能を利用できません。アプリがこの機能に大きく依存している場合、アドオンによるメリットがこの制約を上回るかどうかを慎重に検討する必要があります。
マルチラインサブスクリプションの活用戦略
仕組みを理解することと、この機能を効果的に活用することは別物です。実際に価値を最大化するには、マネタイズ戦略全体を踏まえて考える必要があります。ここでは、Subscription with Add-ons を活用するうえで参考になる、いくつかのアプローチを紹介します。
モジュール型機能戦略
最も分かりやすい活用方法のひとつが、ユーザーが必要な機能だけを選んで構成できるモジュール型サブスクリプションです。Basic / Standard / Premium といった事前定義された複数のプランを用意する代わりに、ベースとなるサブスクリプションと、ユーザーが自由に組み合わせられるアドオンの一覧を提供します。
たとえば、生産性向上アプリであれば、タスク管理や基本的なコラボレーション機能を含むベースサブスクリプションを用意し、アドオンとして高度なレポート機能、チーム管理機能、外部サービス連携、ストレージ容量の追加などを提供できます。高度なレポートだけが必要なユーザーはその機能のみを追加し、パワーユーザーは複数のアドオンを組み合わせる、といった使い方が可能です。このアプローチでは、「本当に必要なものにだけ支払っている」という感覚をユーザーに与えられるため、コンバージョン率の向上が期待できます。
この戦略を成功させるうえで重要なのは、ベースサブスクリプション単体でも十分な価値を提供できていることです。ベースが物足りなかったり不完全に感じられると、アドオンが柔軟性ではなく「小刻みな追加課金(ニッケル・アンド・ダイム)」として受け取られてしまう可能性があります。
プレミアムアップグレード戦略
もうひとつのアプローチは、アドオンを使ってユーザーを段階的にアップグレードしていく戦略です。まずはベースサブスクリプションで利用を開始してもらい、その後、利用状況やユーザー体験の節目に応じて、アドオンをアップセルとして提案します。
たとえば写真編集アプリの場合、ベースサブスクリプションには標準的な編集ツールを含め、利用が進むにつれて、プロ向けプリセット、高度なレタッチ機能、編集後の写真を保存するためのクラウドストレージといったアドオンを提供できます。従来のプラン階層を切り替えるアップグレードと異なり、アドオンであれば、ユーザーは既存の機能を維持したまま新しい機能を追加できます。そのため、「今のプランを失う」という感覚が生まれにくい点が大きな利点です。
この戦略は、パーソナライズされたレコメンデーションと組み合わせることで、特に効果を発揮します。ユーザー行動を分析することで、そのユーザーが価値を感じやすいタイミングで、適切なアドオンを提示できるようになります。
バンドル割引戦略(Bundle and Save)
Subscription with Add-ons では機能を個別に選択できる一方で、魅力的なバンドルを提供するという使い方も可能です。価格設定を工夫することで、アドオンを単体で購入するよりも、まとめて購入したほうがお得に感じられる構成を作れます。
たとえば、ベースサブスクリプションが月額 $9.99、アドオンがそれぞれ月額 $4.99 のものが3つある場合、個別に購入すると合計は $14.97 になります。これを、3つのアドオンをまとめたバンドルとして月額 $11.99 で提供すれば、複数の機能を求めるユーザーにとっては明確にお得な選択肢になります。このように、ユーザーはより多くの機能を割安で利用でき、事業者側は ARPU(ユーザーあたりの平均収益)を引き上げる ことができます。また、1つだけアドオンを購入するユーザーよりも高い収益を確保できる点も、この戦略のメリットです。
複雑さの管理
アドオンの活用におけるリスクのひとつは、選択肢が多くなりすぎてしまうことです。あまりにも多くのオプションが並ぶと、ユーザーは何を選べばよいのか分からなくなり、意思決定ができずに購入そのものをやめてしまう可能性があります。そのため、アドオンの数は通常 3〜5個程度の扱いやすい範囲に絞ることを検討するとよいでしょう。また、それぞれのアドオンについて「何が含まれているのか」「どのようなユーザーに向いているのか」を明確に説明することが重要です。さらに、おすすめのバンドルや「すべて選択」といったオプションを用意すれば、フル機能を求めるユーザーにとって、選択の手間を減らしつつ、スムーズに購入へ進めるようになります。
RevenueCat を使わずに Subscription with Add-ons を実装する
ここからは、Google Play Billing Library を直接使用して Subscription with Add-ons を実装する方法を順に見ていきましょう。この実装には Play Billing Library v5 以上 が必要です。ただし、すべての機能とセキュリティアップデートに確実にアクセスするために、最新バージョン(現時点では v8) を使用することをおすすめします。
Google Play Console でプロダクトの設定を行う
コードを書く前に、Google Play Console でサブスクリプションプロダクトを設定する必要があります。朗報なのは、既存のサブスクリプシ ョンプロダクトは特別な設定なしでアドオンとして提供できるという点です。別途「アドオン用」のプロダクトタイプを作成する必要はありません。自動更新サブスクリプションであれば、ベースアイテムにもアドオンにもなり得ます。
プロダクトを作成する際は、バンドルに含めたいすべてのアイテムが同じ請求期間でそろっている必要があることを忘れないでください。月額と年額の両方を提供したい場合は、請求期間ごとに別々のベースプランを作成する必要があります。
プロダクト詳細を取得する
実装の最初のステップは、Google Play から利用可能なプロダクトを問い合わせることです。提供したいすべてのサブスクリプションプロダクトの詳細を取得するために、 queryProductDetailsAsync メソッドを使用します。
1class BillingManager(private val context: Context) {
2 private lateinit var billingClient: BillingClient
3
4 private val productIds = listOf(
5 "premium_base_monthly",
6 "hifi_addon_monthly",
7 "offline_addon_monthly",
8 "family_addon_monthly"
9 )
10
11 fun initialize() {
12 billingClient = BillingClient.newBuilder(context)
13 .setListener { billingResult, purchases ->
14 handlePurchasesUpdated(billingResult, purchases)
15 }
16 .enablePendingPurchases()
17 .build()
18
19 billingClient.startConnection(object : BillingClientStateListener {
20 override fun onBillingSetupFinished(billingResult: BillingResult) {
21 if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
22 querySubscriptionProducts()
23 }
24 }
25
26 override fun onBillingServiceDisconnected() {
27// Implement retry logic here
28 }
29 })
30 }
31
32 private fun querySubscriptionProducts() {
33 val productList = productIds.map { productId ->
34 QueryProductDetailsParams.Product.newBuilder()
35 .setProductId(productId)
36 .setProductType(BillingClient.ProductType.SUBS)
37 .build()
38 }
39
40 val params = QueryProductDetailsParams.newBuilder()
41 .setProductList(productList)
42 .build()
43
44 billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
45 if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
46// Store product details for later use when launching purchase flow
47 handleProductDetails(productDetailsList)
48 }
49 }
50 }
51}
このクエリは、各サブスクリプションプロダクトに対応する ProductDetails オブジェクトを返します。これ らのオブジェクトには、ユーザーに価格を表示したり、購入フローを開始したりするために必要なすべての情報が含まれています。具体的には、利用可能なベースプランやオファー、そのオファーに紐づくオファートークンなどが含まれます。
複数アイテムを含む購入フローの開始
ユーザーが希望するサブスクリプション構成(ベースサブスクリプション+選択したアドオン)を選択したら、複数の ProductDetailsParams オブジェクトを指定して購入フローを起動します。ここで重要なのは、リストの先頭にあるアイテムがベースアイテムとして扱われるという点です。そのため、必ずベースとなるサブスクリプションを最初に追加する ようにしてください。
1fun launchSubscriptionWithAddons(
2 activity: Activity,
3 baseProductDetails: ProductDetails,
4 baseOfferToken: String,
5 addonProductDetailsList: List<Pair<ProductDetails, String>>
6) {
7 val productDetailsParamsList = mutableListOf<BillingFlowParams.ProductDetailsParams>()
8
9// Add the base subscription first - this is crucialval baseParams = BillingFlowParams.ProductDetailsParams.newBuilder()
10 .setProductDetails(baseProductDetails)
11 .setOfferToken(baseOfferToken)
12 .build()
13 productDetailsParamsList.add(baseParams)
14
15// Add each selected add-onfor ((addonDetails, offerToken) in addonProductDetailsList) {
16 val addonParams = BillingFlowParams.ProductDetailsParams.newBuilder()
17 .setProductDetails(addonDetails)
18 .setOfferToken(offerToken)
19 .build()
20 productDetailsParamsList.add(addonParams)
21 }
22
23 val billingFlowParams = BillingFlowParams.newBuilder()
24 .setProductDetailsParamsList(productDetailsParamsList)
25 .build()
26
27 val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams)
28
29 if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
30// Handle error - perhaps show a message to the user
31 handleBillingError(billingResult)
32 }
33}
リスト内の各アイテムは 一意である必要があります。同じプロダクト ID を持つ ProductDetailsParams オブジェクトを複数含めることはできません。また、オファートークンは、そのアイテムに対してどのベースプランまたはオファーを使用するかを指定するものです。 ProductDetails.subscriptionOfferDetails() メソッドから取得した 有効なオファートークン を必ず指定する必要があります。
購入の処理
ユーザーが購入フローを完了すると、 PurchasesUpdatedListener が結果を受け取ります。Subscription with Add-ons の処理は、基本的には単一のサブスクリプションを処理する場合と同じですが、ひとつ重要な違いがあります。それは、この購入によって 複数のアイテム分のエンタイトルメントが付与される という点です。
1private fun handlePurchasesUpdated(
2 billingResult: BillingResult,
3 purchases: List<Purchase>?
4) {
5 when (billingResult.responseCode) {
6 BillingClient.BillingResponseCode.OK -> {
7 purchases?.forEach { purchase ->
8 processPurchase(purchase)
9 }
10 }
11 BillingClient.BillingResponseCode.USER_CANCELED -> {
12// User canceled the purchase flow
13 }
14 else -> {
15// Handle other error codes
16 }
17 }
18}
19
20private fun processPurchase(purchase: Purchase) {
21// For Subscription with Add-ons, getProducts() returns all product IDsval purchasedProductIds = purchase.products
22
23// Verify the purchase with your backend server
24 verifyPurchaseWithBackend(purchase) { isValid ->
25 if (isValid) {
26// Grant entitlements for all purchased productsfor (productId in purchasedProductIds) {
27 grantEntitlement(productId)
28 }
29
30// Acknowledge the purchase if not already acknowledgedif (!purchase.isAcknowledged) {
31 acknowledgePurchase(purchase)
32 }
33 }
34 }
35}
36
37private fun acknowledgePurchase(purchase: Purchase) {
38 val params = AcknowledgePurchaseParams.newBuilder()
39 .setPurchaseToken(purchase.purchaseToken)
40 .build()
41
42 billingClient.acknowledgePurchase(params) { billingResult ->
43 if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
44// Handle acknowledgment failure
45 }
46 }
47}
purchase.products プロパティには、その購入に含まれるすべてのプロダクト ID のリストが返されます。それぞれのプロダクトに対してエンタイトルメントを付与するとともに、必ずバックエンドサーバーで購入の検証を行ったうえで処理するようにしてください。
既存の Subscription with Add-ons を変更する
ユーザーは、既存のサブスクリプションに 新しいアドオンを追加したり、不要になったアドオンを削除したりしたい場合があります。既存の Subscription with Add-ons を変更する際には、現在の購入トークン(purchase token)を指定し、あわせて 置き換えモード(replacement mode) を設定する必要があります。
1fun modifySubscriptionAddons(
2 activity: Activity,
3 currentPurchaseToken: String,
4 baseProductDetails: ProductDetails,
5 baseOfferToken: String,
6 newAddonsList: List<Pair<ProductDetails, String>>
7) {
8 val productDetailsParamsList = mutableListOf<BillingFlowParams.ProductDetailsParams>()
9
10// Include the base subscription
11 productDetailsParamsList.add(
12 BillingFlowParams.ProductDetailsParams.newBuilder()
13 .setProductDetails(baseProductDetails)
14 .setOfferToken(baseOfferToken)
15 .build()
16 )
17
18// Include all add-ons (both existing ones to keep and new ones to add)for ((addonDetails, offerToken) in newAddonsList) {
19 productDetailsParamsList.add(
20 BillingFlowParams.ProductDetailsParams.newBuilder()
21 .setProductDetails(addonDetails)
22 .setOfferToken(offerToken)
23 .build()
24 )
25 }
26
27// Configure the subscription updateval subscriptionUpdateParams = BillingFlowParams.SubscriptionUpdateParams.newBuilder()
28 .setOldPurchaseToken(currentPurchaseToken)
29 .setSubscriptionReplacementMode(
30 BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE
31 )
32 .build()
33
34 val billingFlowParams = BillingFlowParams.newBuilder()
35 .setProductDetailsParamsList(productDetailsParamsList)
36 .setSubscriptionUpdateParams(subscriptionUpdateParams)
37 .build()
38
39 billingClient.launchBillingFlow(activity, billingFlowParams)
40}

