App Store のサブスクリプション最適化に注目が集まりがちですが、Google Play にもApp Storeに劣らない強力なオファー機能があります。そして多くの個人開発者が、この機能を十分に活用できていません。
Rorkで開発したAndroidアプリに、Google Playのサブスクリプションオファーを実装する方法を整理します。設定からコードまで、一通り解説します。
Google Play サブスクリプションオファーの種類
Google Play Consoleでは、1つのサブスクリプション商品に複数の「オファー」を設定できます。主なオファー種別:
1. 無料トライアル(Free Trial)
最もポピュラーな形式。7日・14日・30日の無料試用期間を設定できます。注意点は、同じユーザーが同じアプリで何度も無料トライアルを利用できないことです(Google側で制御されます)。
2. 入門価格(Introductory Price)
最初の数ヶ月を通常より安い価格で提供します。たとえば「初月$0.99、2ヶ月目から$4.99」のような設定です。無料トライアルより転換率は下がりますが、有料転換ユーザーの長期維持率が高い傾向があります(最初から課金する意欲のあるユーザーだから)。
3. アップグレードオファー
無料プランや下位プランのユーザーに、上位プランへの特別価格でのアップグレードを提案します。既存ユーザー向けのため、プロモーションが有効な対象に絞り込みやすい。
4. ウィンバックオファー(Win-back Offer)
過去にサブスクリプションを解約したユーザーに再サブスクを促すオファーです。Google Playが自動的に「過去の購読者」を対象として設定でき、割引価格を提示できます。個人的に最も活用が不足しているオファー種別だと感じています。
Play Console でのオファー設定手順
- Play Console → 対象アプリ → 「収益化」→「製品」→「サブスクリプション」
- 対象サブスクリプションを選択 → 「オファーを追加」
- オファーの基本情報を設定
オファーID(重要): コード内でオファーを参照する際のIDです。後から変更できないので、命名規則を決めておきましょう。
例:
trial-7days— 7日間無料トライアルintro-1month-half— 初月50%オフ入門価格winback-30pct— 解約者向け30%オフウィンバック
対象設定: オファーを誰に見せるかを設定します。
- 新規ユーザーのみ(未購入者)
- 既存ユーザー(購入済み)
- 解約済みユーザー(ウィンバック)
Android(Kotlin)での実装
Google Play Billing Library v7 でオファーを実装するコードです。
// BillingManager.kt
import com.android.billingclient.api.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
class BillingManager(private val activity: Activity) {
private lateinit var billingClient: BillingClient
private val _subscriptionOffers = MutableStateFlow<List<ProductDetails.SubscriptionOfferDetails>>(emptyList())
val subscriptionOffers: StateFlow<List<ProductDetails.SubscriptionOfferDetails>> = _subscriptionOffers
fun initialize() {
billingClient = BillingClient.newBuilder(activity)
.setListener { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
purchases?.forEach { handlePurchase(it) }
}
}
.enablePendingPurchases()
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) {
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
loadSubscriptionOffers()
}
}
override fun onBillingServiceDisconnected() {
// 再接続ロジック
}
})
}
private fun loadSubscriptionOffers() {
val queryParams = QueryProductDetailsParams.newBuilder()
.setProductList(
listOf(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("premium_monthly") // Play Consoleで設定したID
.setProductType(BillingClient.ProductType.SUBS)
.build()
)
)
.build()
billingClient.queryProductDetailsAsync(queryParams) { result, productDetailsList ->
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
val product = productDetailsList.firstOrNull() ?: return@queryProductDetailsAsync
val offers = product.subscriptionOfferDetails ?: emptyList()
// オファーをログ出力(デバッグ用)
offers.forEach { offer ->
println("Offer ID: ${offer.offerId}")
println("Tags: ${offer.offerTags}")
offer.pricingPhases.pricingPhaseList.forEach { phase ->
println(" Phase: ${phase.formattedPrice} for ${phase.billingCycleCount} cycles")
}
}
_subscriptionOffers.value = offers
}
}
}
fun getOfferForUser(
offers: List<ProductDetails.SubscriptionOfferDetails>,
isReturningUser: Boolean,
hasUsedTrial: Boolean,
): ProductDetails.SubscriptionOfferDetails? {
return when {
// 過去の購読者 → ウィンバックオファーを優先
isReturningUser -> offers.firstOrNull { offer ->
offer.offerTags.contains("winback")
}
// 新規ユーザーでトライアル未使用 → 無料トライアル
!hasUsedTrial -> offers.firstOrNull { offer ->
offer.offerTags.contains("trial")
}
// トライアル使用済み → 入門価格
else -> offers.firstOrNull { offer ->
offer.offerTags.contains("introductory")
}
} ?: offers.firstOrNull() // フォールバック: 通常価格
}
fun launchPurchaseFlow(
productDetails: ProductDetails,
selectedOffer: ProductDetails.SubscriptionOfferDetails,
) {
val offerToken = selectedOffer.offerToken
val productDetailsParamsList = listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken) // 選択したオファーのトークンを指定
.build()
)
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
}
private fun handlePurchase(purchase: Purchase) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
// 購入確認(Acknowledgement)— 3日以内に必須
if (!purchase.isAcknowledged) {
val acknowledgeParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(acknowledgeParams) { result ->
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
println("Purchase acknowledged: ${purchase.orderId}")
}
}
}
// バックエンドに購入情報を送信して会員ステータスを更新
updateSubscriptionStatus(purchase.purchaseToken)
}
}
private fun updateSubscriptionStatus(purchaseToken: String) {
// RevenueCatまたは自前バックエンドに通知
}
}オファーの効果を最大化するUIデザイン
ペイウォールでのオファー表示のポイントを整理します。
無料トライアルは「残り期間」で訴求する
「7日間無料」より「7日間、完全無料でお試しください」の方が、ユーザーの心理的ハードルが下がります。さらに「今日開始 → 1週間後にキャンセルしなければ$4.99/月」のタイムラインを視覚的に示すと、透明性が高まり転換率が上がることが多いです。
ウィンバックオファーは「理由」を添える
「おかえりなさい。お戻りいただける方に、最初の2ヶ月を30%オフでご用意しました」という文脈がある方が、単なる割引表示より効果的です。
オファーのA/Bテスト(RevenueCat Experimentsとの組み合わせ)
RevenueCatのExperiments機能とGoogle Playのオファーを組み合わせることで、どのオファーが最も長期的な収益に貢献するか(LTVで評価)を計測できます。転換率だけでなくLTVで評価することが重要で、無料トライアルより入門価格の方がLTVが高いケースもあります。
全体を振り返って
Google Playのサブスクリプションオファーは、設定さえすれば自動的に機能する強力なツールです。特にウィンバックオファーは、既に「一度興味を持ってくれた」ユーザーへのアプローチとして高い費用対効果があります。
最小限の実装として、まず「7日間無料トライアル」と「ウィンバックオファー(30%オフ)」の2種類を設定することをお勧めします。この2つだけで、多くのアプリでサブスクリプション収益の改善が見込めます。
RevenueCat を使った高度なサブスクリプション分析・実装については、プレミアム記事「Rork + Stripe で Web SaaS の決済基盤を完全構築する」もあわせてご参照ください。