Реалізація Subscription Downgrade/Upgrade у мобільних застосунках
Смена тарифу підписки — одна з найбільш заплутаних частин StoreKit. Apple і Google по-різному обробляють переходи, і «просто купити інший продукт» — це не upgrade. Без правильної реалізації користувач виявиться з двома активними підписками, або перехід не станеться до кінця поточного періоду без жодного сповіщення.
Як це працює на iOS
В App Store усі підписки в межах однієї Subscription Group автоматично управляються Apple: неможливо купити два продукти з однієї групи одночасно. При покупці нового продукту з тієї ж групи Apple застосовує одну з трьох політик:
- Immediate upgrade (перехід на вищий рівень): нова підписка активується негайно, користувачу зараховується пропорційна частина залишку періоду
- Crossgrade at renewal (перехід на аналогічний рівень): нова підписка починається у дату наступного продовження
- Downgrade at renewal (перехід на нижчий рівень): поточна підписка продовжується до кінця періоду, потім активується нова
Рівень продукту встановлюється у App Store Connect → Subscription Group → drag-and-drop порядок продуктів. Вищий = найвищий.
Реалізація на клієнті (StoreKit 2)
Для переходу між тарифами викликаємо 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
}
}
Після успішної покупки нового тарифу перевіряємо поточний активний entitlement:
func getCurrentActivePlan() async -> String? {
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result,
transaction.productType == .autoRenewableSubscription {
return transaction.productID
}
}
return nil
}
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)."
}
}
Модальний екран підтвердження з явним описом умов — обов'язковий.
Google Play Billing: режими пропорційного розрахунку
На 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
// Для downgrade: WITH_TIME_PRORATION
// Для негайного переходу без зачету: CHARGE_FULL_PRICE
)
.build()
)
.build()
Невірний ReplacementMode — частова помилка. CHARGE_PRORATED_PRICE для downgrade викличе негайне списання по новій ціні без компенсації — користувач втратить гроші.
| Сценарій | iOS політика | Android ReplacementMode |
|---|---|---|
| Upgrade | Негайно, пропорційний зачет | CHARGE_PRORATED_PRICE |
| Downgrade | Кінець періоду | WITH_TIME_PRORATION |
| Crossgrade (той же рівень) | Наступне продовження | DEFERRED |
Pending стан
Downgrade створює pending транзакцію — підписка куплена, але ще не активна. На iOS це .pending в результаті purchase(). На Android — PENDING в Purchase.purchaseState. Потрібно зберігати цей стан та сповіщати користувача про запланований перехід.
Що входить у роботу
- Визначення рівнів тарифів у App Store Connect / Google Play Console
- Клієнтська логіка покупки з обробкою immediate / pending
- UI з описом умов переходу для кожного сценарію (up/down/cross)
- Обробка
Transaction.updatesдля відслідковування активації pending - Google Play: коректний
ReplacementModeдля кожного типу переходу - Серверна синхронізація статусу через App Store Server Notifications / RTDN
Терміни
3–5 днів — залежить від кількості тарифів та платформ. Вартість розраховується індивідуально після аналізу вимог.







