Реализация Google Play Billing (подписки) для Android
С Google Play Billing Library 5 модель подписок полностью переработана. Появились base plans и offers — это не просто маркетинговое переименование, а новая иерархия объектов: один ProductDetails содержит несколько SubscriptionOfferDetails, каждый со своим offerToken. Старый код, который передавал skuDetails.sku напрямую в BillingFlowParams, не компилируется с Billing 5+.
Как устроена новая модель
Subscription Product
├── Base Plan (monthly)
│ ├── Offer: "free-trial-7days" (offerToken_1)
│ └── Offer: "default" (offerToken_2)
└── Base Plan (annual)
└── Offer: "default" (offerToken_3)
При запуске покупки выбираем конкретный offerToken:
val productDetails = // из queryProductDetailsAsync
val offerToken = productDetails.subscriptionOfferDetails
?.firstOrNull { it.offerTags.contains("default") }
?.offerToken ?: return
val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build()
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productDetailsParams))
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
Если передать offerToken от одного base plan, а пользователь уже имеет подписку на другой — это upgrade/downgrade, Google обрабатывает автоматически при указании setSubscriptionUpdateParams.
Grace period и account hold
В отличие от iOS, Google Play имеет два состояния после истечения оплаты:
- Grace period (1–3 дня) — подписка технически активна, Google пытается списать
- Account hold (до 30 дней) — после grace, подписка на паузе, Google продолжает попытки
Правильно обрабатывать оба через purchases.subscriptions.get в Google Play Developer API. Поле paymentState: 0 = payment pending, 1 = payment received, 2 = free trial, 3 = pending deferred upgrade.
На клиенте — через purchase.purchaseState и дополнительно через Real-Time Developer Notifications (Pub/Sub):
// Пример RTDN payload при account hold
{
"subscriptionNotification": {
"notificationType": 5, // SUBSCRIPTION_ON_HOLD
"purchaseToken": "...",
"subscriptionId": "premium_monthly"
}
}
Типы уведомлений, которые обязательно обрабатывать: SUBSCRIPTION_RENEWED (1), SUBSCRIPTION_CANCELED (3), SUBSCRIPTION_ON_HOLD (5), SUBSCRIPTION_IN_GRACE_PERIOD (6), SUBSCRIPTION_RESTARTED (7), SUBSCRIPTION_REVOKED (12), SUBSCRIPTION_EXPIRED (13).
Proration при смене тарифа
При переходе между base plans — указываем ProrationMode:
val updateParams = BillingFlowParams.SubscriptionUpdateParams.newBuilder()
.setOldPurchaseToken(currentPurchaseToken)
.setSubscriptionReplacementMode(
BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION
)
.build()
WITH_TIME_PRORATION — самый честный для пользователя: остаток текущего периода пересчитывается в дни нового тарифа. IMMEDIATE_WITHOUT_PRORATION — мгновенный переход без возврата.
Процесс работы
Настройка base plans и offers в Play Console → интеграция Billing Library 6+ → обработка всех purchaseState на клиенте → настройка RTDN через Google Cloud Pub/Sub → серверная синхронизация статусов → тестирование через лицензионных тестеров с имитацией expiry.
Сроки — 3–5 дней в зависимости от количества тарифных планов и наличия серверной части для RTDN.







