Реализация 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
}
}
После успешной покупки нового тарифа проверяем текущий active 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: proration modes
На 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 | Immediate, пропорциональный зачёт | CHARGE_PRORATED_PRICE |
| Downgrade | Конец периода | WITH_TIME_PRORATION |
| Crossgrade (тот же уровень) | Следующее продление | DEFERRED |
Pending state
Downgrade создаёт pending transaction — подписка куплена, но ещё не активна. На iOS это .pending в результате purchase(). На Android — PENDING в Purchase.purchaseState. Нужно хранить это состояние и уведомлять пользователя о запланированном изменении.
Что входит в работу
- Определение уровней тарифов в 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 дней — зависит от количества тарифов и платформ. Стоимость рассчитывается индивидуально после анализа требований.







