Реализация Grace Period при неудачном списании подписки
Grace Period — это временное окно, которое Apple и Google дают пользователю после неудачного списания: карта истекла, недостаточно средств, временный сбой банка. В течение grace period подписка считается активной. Задача разработчика — корректно читать этот статус и не отрезать пользователя от контента раньше времени.
Без реализации grace period приложение блокирует доступ сразу после неудачного списания. Пользователь обновляет карту, но уже ушёл — это прямые потери в retention.
Grace Period на iOS (StoreKit 2)
Apple автоматически активирует grace period если он включён в App Store Connect → Subscriptions → [Subscription Group] → Grace Period. Варианты длительности: 3, 6 или 16 дней.
Для чтения статуса используем Product.SubscriptionInfo.Status:
import StoreKit
func checkSubscriptionStatus(productId: String) async -> SubscriptionAccessLevel {
guard let product = try? await Product.products(for: [productId]).first,
let statuses = try? await product.subscription?.status else {
return .notSubscribed
}
for status in statuses {
switch status.state {
case .subscribed:
return .active
case .inGracePeriod:
// Списание не прошло, но grace period активен
// Показываем мягкое предупреждение, не блокируем контент
return .gracePeriod
case .inBillingRetryPeriod:
// Grace period истёк, Apple продолжает попытки списания (до 60 дней)
// Контент НЕ доступен
return .billingRetry
case .expired, .revoked:
return .notSubscribed
default:
continue
}
}
return .notSubscribed
}
enum SubscriptionAccessLevel {
case active, gracePeriod, billingRetry, notSubscribed
}
Что показывать пользователю в grace period
Ключевой принцип: не блокировать контент, но показывать мягкий баннер с призывом обновить платёжные данные. Агрессивный paywall в grace period — это плохой UX и нарушение гайдлайнов Apple.
// SwiftUI-баннер
if subscriptionStatus == .gracePeriod {
GracePeriodWarningBanner(
message: "Не удалось списать оплату. Обновите данные карты, чтобы сохранить доступ.",
actionTitle: "Управление подпиской",
action: { openManageSubscriptions() }
)
}
// Открываем системный экран управления подписками
func openManageSubscriptions() {
Task {
try? await AppStore.showManageSubscriptions(in: windowScene)
}
}
Grace Period на Android (Google Play Billing)
В Google Play Billing grace period реализуется через purchaseState и isAutoRenewing:
// Через RTDN (Real-Time Developer Notifications) или PurchasesUpdatedListener
// Google отправляет SubscriptionNotification.SUBSCRIPTION_IN_GRACE_PERIOD
fun handleSubscriptionNotification(notification: SubscriptionNotification) {
when (notification.notificationType) {
SubscriptionNotification.SUBSCRIPTION_IN_GRACE_PERIOD -> {
// Контент доступен, показываем предупреждение
showGracePeriodWarning()
}
SubscriptionNotification.SUBSCRIPTION_EXPIRED -> {
// Grace period истёк
revokeAccess()
}
}
}
Серверная обработка предпочтительна: RTDN приходит на backend через Pub/Sub, бэкенд обновляет статус пользователя, мобильный клиент получает актуальный статус при следующем запросе.
Серверная валидация — надёжнее клиентской
Клиентская проверка через StoreKit — удобна для UI, но не должна быть единственным источником истины. Правильная архитектура:
- Сервер получает транзакции через App Store Server Notifications V2 (тип
DID_FAIL_TO_RENEW,GRACE_PERIOD_EXPIRED) - Обновляет поле
subscription_statusв базе - Мобильный клиент при запросе
/me/subscriptionполучает актуальный статус
Это единственный надёжный способ, если пользователь открыл приложение через несколько дней — кэш StoreKit мог не обновиться.
Что входит в работу
- Включение grace period в App Store Connect / Google Play Console
- Клиентское чтение
inGracePeriod/inBillingRetryPeriod(StoreKit 2) - UI-баннер с предупреждением и ссылкой на управление подпиской
- Логика доступа к контенту: grace = разрешено, billingRetry = заблокировано
- Опционально: серверная обработка App Store Server Notifications
Сроки
2–3 дня — клиентская часть с UI-логикой. С серверной обработкой уведомлений: 4–5 дней. Стоимость рассчитывается индивидуально.







