Смена тарифа подписки: 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 дней в зависимости от количества тарифов и платформ. Стоимость рассчитывается индивидуально после анализа требований. Получите консультацию — свяжитесь с нашими специалистами, чтобы обсудить ваш проект под ключ. Закажите аудит текущей реализации подписки — это займёт не больше часа.







