Реалізація Google Play Billing (підписки) для Android
З Google Play Billing Library 5 модель підписок повністю переробилася. З'явилися базові плани та пропозиції — це не просто маркетингове перейменування, а нова ієрархія об'єктів: один ProductDetails містить кілька SubscriptionOfferDetails, кожен з власним offerToken. Старий код, який передавав skuDetails.sku прямо в BillingFlowParams, не компілюється з Billing 5+.
Як улаштована нова модель
Subscription Product
├── Base Plan (місячний)
│ ├── Offer: "free-trial-7days" (offerToken_1)
│ └── Offer: "default" (offerToken_2)
└── Base Plan (річний)
└── 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 з одного базового плану, а користувач уже має підписку на інший — це 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 = платіж очікує, 1 = платіж отримано, 2 = безпалатний період, 3 = pending відстрочене оновлення.
На клієнті — через 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 при зміні тарифу
При переході між базовими планами — вказуємо ProrationMode:
val updateParams = BillingFlowParams.SubscriptionUpdateParams.newBuilder()
.setOldPurchaseToken(currentPurchaseToken)
.setSubscriptionReplacementMode(
BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION
)
.build()
WITH_TIME_PRORATION — найчесніший для користувача: залишок поточного періоду перераховується на дні нового тарифу. IMMEDIATE_WITHOUT_PRORATION — миттєвий перехід без повернення.
Процес роботи
Налаштування базових планів та пропозицій у Play Console → інтеграція Billing Library 6+ → обробка всіх purchaseState на клієнті → налаштування RTDN через Google Cloud Pub/Sub → серверна синхронізація статусів → тестування через ліцензійних тестерів з імітацією закінчення.
Терміни — 3–5 днів залежно від кількості тарифних планів та наявності серверної частини для RTDN.







