Реалізація 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 днів. Вартість розраховується індивідуально.







