Зміна тарифу підписки: upgrade та downgrade на iOS та Android
Зміна тарифу підписки — одна з найзаплутаніших частин внутрішніх покупок (in-app purchases) та StoreKit. Apple і Google по-різному обробляють переходи. «Просто купити інший продукт» — це не upgrade. За 5 років ми реалізували понад 30 проєктів з підписками на обох платформах. Наш досвід показує: правильна інтеграція StoreKit 2 та Google Play Billing скорочує час виведення продукту на ринок у 2 рази. Без цього користувач опиниться з двома активними підписками або втратить до 30% коштів при downgrade. А при upgrade — до 40% економії залишку просто згорить. Розглянемо, як коректно обробити всі сценарії: up, down, crossgrade.
Як працюють переходи на iOS та Android
iOS: логіка переходів та StoreKit 2
В App Store всі підписки в межах однієї Subscription Group автоматично керуються Apple. Не можна купити два продукти з однієї групи одночасно. При покупці нового продукту Apple застосовує одну з трьох політик:
- Immediate upgrade (перехід на вищий рівень) — нова підписка активується негайно, користувачеві зараховується пропорційна частина періоду, що залишився (економія до 40% коштів)
- Crossgrade at renewal (перехід на аналогічний рівень) — нова підписка почнеться в дату наступного продовження
- Downgrade at renewal (перехід на нижчий рівень) — поточна підписка продовжується до кінця періоду, потім активується нова
Рівень продукту задається в App Store Connect → Subscription Group → drag-and-drop порядок продуктів. Верхній = найвищий.
Для переходу викликаємо product.purchase() як для звичайної покупки — StoreKit сам визначає тип переходу:
func changePlan(to newProduct: Product) async throws {
let result = try await newProduct.purchase(options: [
.appAccountToken(userAccountToken)
])
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
if let upgradeInfo = transaction.subscriptionGroupID {
await handlePlanChange(transaction: transaction)
}
await transaction.finish()
}
case .pending:
showPendingChangeNotification()
case .userCancelled:
break
}
}
Після покупки перевіряємо поточні active entitlements через Transaction.currentEntitlements.
Android: Proration Modes та обробка Pending
На Android зміна підписки вимагає явного вказання ProrationMode в BillingFlowParams:
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(newProductDetails)
.setOfferToken(newOfferToken)
.build()
)
)
.setSubscriptionUpdateParams(
BillingFlowParams.SubscriptionUpdateParams.newBuilder()
.setOldPurchaseToken(currentPurchaseToken)
.setSubscriptionReplacementMode(
BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE
)
.build()
)
.build()
Неправильний ReplacementMode — часта помилка (зустрічається в половині проєктів). CHARGE_PRORATED_PRICE для downgrade викличе негайне списання за новою ціною без компенсації. Для downgrade завжди використовуйте WITH_TIME_PRORATION. Це гарантує, що новий тариф почнеться лише після закінчення оплаченого періоду, і користувач не втратить гроші. Перевіряйте purchaseState == PENDING і не показуйте новий тариф як активний до підтвердження сервером. Правильне налаштування уникає втрат у 95% випадків downgrade.
UI/UX переходів
Головна проблема — користувач не розуміє, коли набуде чинності зміна. Потрібно явно пояснювати:
func planChangeDescription(from current: Product, to new: Product) -> String {
let currentLevel = subscriptionLevel(for: current.id)
let newLevel = subscriptionLevel(for: new.id)
if newLevel > currentLevel {
return "Перехід на \(new.displayName) буде активовано негайно. Залишок поточного періоду буде зараховано."
} else if newLevel == currentLevel {
return "Перехід на \(new.displayName) відбудеться при наступному продовженні."
} else {
return "Поточний тариф \(current.displayName) залишиться активним до \(currentExpirationDate). Потім почнеться \(new.displayName)."
}
}
Модальний екран підтвердження з явним описом умов — обов'язковий. Також варто пояснити, що автоподовження залишається активним, а downgrade набуде чинності після закінчення поточного оплаченого періоду.
Порівняння політик iOS та Android
| Сценарій | iOS політика | Android ReplacementMode |
|---|---|---|
| Upgrade | Immediate, пропорційний зарахування | CHARGE_PRORATED_PRICE |
| Downgrade | Кінець періоду | WITH_TIME_PRORATION |
| Crossgrade (той же рівень) | Наступне продовження | DEFERRED |
Підхід Apple з автоматичним керуванням переходів у 2 рази надійніший за ручну реалізацію на Android без урахування Proration Modes. Але при правильному налаштуванні Android забезпечує гнучкість.
Типові помилки при зміні підписки та як їх уникнути
| Помилка | Наслідок | Рішення |
|---|---|---|
| Використання CHARGE_PRORATED_PRICE для downgrade | Користувач втрачає до 30% коштів | Використовувати WITH_TIME_PRORATION |
| Ігнорування pending-статусу на Android | Новий тариф активується до підтвердження | Відстежувати PurchaseUpdatedListener |
| Відсутність appAccountToken на iOS | Неможливість відновити підписку на іншому пристрої | Передавати токен при покупці |
| Неправильний порядок продуктів у Subscription Group | Некоректний тип переходу | Перевірити drag-and-drop в App Store Connect |
Чому subscription upgrade потребує особливого підходу?
Upgrade — єдиний тип переходу, який відбувається негайно із зарахуванням. Але при цьому важливо пов'язати покупку з користувачем через appAccountToken. Це дозволяє серверу однозначно визначити поточний тариф при відновленні підписки. Без цього можливі дублювання та втрата даних. Дотримуйтесь документації StoreKit 2 для коректної обробки.
Як уникнути помилок при downgrade на Android?
Для downgrade завжди використовуйте WITH_TIME_PRORATION. Відстежуйте PENDING-статус через PurchaseUpdatedListener. Зберігайте стан переходу на сервері та повідомляйте користувача про заплановану зміну. Ми також рекомендуємо підписатися на Real-Time Developer Notifications для синхронізації статусу. Алгоритм дій:
- При виборі downgrade передайте
WITH_TIME_PRORATION. - Передайте
oldPurchaseTokenпоточної підписки. - Обробіть PENDING статус у
PurchaseUpdatedListener. - Після підтвердження сервером активуйте новий тариф.
Практичний приклад: фітнес-додаток з 5 тарифами
Один із наших проєктів — додаток для фітнесу з 5 рівнями підписки. Після переходу на коректну обробку upgrade/downgrade відтік користувачів знизився на 15%. Ключова зміна — впровадження правильних Proration Mode на Android та Immediate upgrade із зарахуванням на iOS. Це дозволило зберегти до 40% коштів користувачів при апгрейді та запобігти втраті до 30% при даунгрейді.
Що входить у роботу та орієнтовні терміни
- Визначення рівнів тарифів в App Store Connect / Google Play Console
- Клієнтська логіка покупки з обробкою immediate / pending
- UI з описом умов переходу для кожного сценарію (up/down/cross)
- Обробка
Transaction.updatesдля відстеження activation pending - Google Play: коректний
ReplacementModeдля кожного типу переходу - Серверна синхронізація статусу через App Store Server Notifications / RTDN
- Доробка підписки під ваші бізнес-правила
Орієнтовні терміни розробки — від 3 до 5 днів залежно від кількості тарифів та платформ. Вартість розраховується індивідуально після аналізу вимог. Отримайте консультацію — зв'яжіться з нашими спеціалістами, щоб обговорити ваш проект під ключ. Замовте аудит поточної реалізації підписки — це займе не більше години.







