Реализация In-App Purchases (подписки) для iOS
Auto-renewable subscriptions — самый сложный тип IAP. Не потому что StoreKit сложен сам по себе, а потому что вокруг него вырастает целая экосистема: grace periods, billing retry, downgrade/upgrade между тирами, promo offers, introductory pricing, и наконец — обработка churn через expirationIntent.
Жизненный цикл подписки и где он ломается
Подписка в iOS существует не только пока активна. Apple автоматически продлевает её за 24 часа до истечения. Если оплата не прошла — начинается billing retry period (до 60 дней). В это время статус подписки expired, но Apple продолжает попытки списания. Большинство приложений блокируют доступ сразу после expirationDate — это неверно.
Правильная логика: проверять renewalInfo.isInBillingRetryPeriod. Если true — даём grace period (настраивается в App Store Connect, обычно 6 дней для годовых, 3 дня для месячных). Пользователь с проблемой карты не должен терять доступ немедленно.
StoreKit 2 делает это прозрачным:
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else { continue }
if transaction.productType == .autoRenewable {
let renewalInfo = try? await transaction.subscriptionStatus.first?.renewalInfo
let isInGracePeriod = renewalInfo?.gracePeriodExpirationDate != nil
let isRetrying = renewalInfo?.isInBillingRetryPeriod == true
if transaction.revocationDate == nil &&
(transaction.expirationDate ?? .distantPast > .now || isInGracePeriod || isRetrying) {
unlockPremium()
}
}
}
Тиры и переходы между ними
Если в приложении несколько тиров (Basic, Pro, Enterprise) — нужна subscription group в App Store Connect. Все тиры в одной группе, пользователь может иметь только одну активную подписку в группе одновременно.
Upgrade (переход на более дорогой тир) — действует сразу, Apple пересчитывает остаток. Downgrade — вступает в силу в следующий расчётный период. Crossgrade (одна цена, другой тир) — зависит от настройки: можно сделать immediate или deferred.
Отслеживать это на клиенте через originalTransactionID и subscriptionGroupID. На сервере — хранить полную историю транзакций и обрабатывать Apple Server Notifications v2 (App Store Server Notifications). Типы событий, которые обязательно обрабатывать: DID_RENEW, DID_FAIL_TO_RENEW, EXPIRED, GRACE_PERIOD_EXPIRED, REFUND.
Introductory и promotional offers
Introductory pricing (первые N периодов по сниженной цене) настраивается в App Store Connect и автоматически применяется для новых подписчиков. Проблема — пользователь, который отписался и хочет вернуться, по умолчанию не получает интро-цену снова. Для этого существуют promotional offers — их можно выдавать по своей логике (win-back кампании).
Подпись для promotional offer генерируется на сервере с приватным ключом из App Store Connect:
// На клиенте создаём paymentDiscount
let discount = SKPaymentDiscount(
identifier: "winback_3months",
keyIdentifier: keyID,
nonce: nonce, // UUID с сервера
signature: signature, // подпись с сервера
timestamp: timestamp
)
payment.paymentDiscount = discount
Без серверной подписи promotional offer недействителен — Apple проверяет подпись на своей стороне.
Процесс работы
Аудит текущей реализации → проектирование структуры подписочных групп и тиров → интеграция StoreKit 2 с поддержкой grace period и billing retry → настройка App Store Server Notifications на бэкенде → реализация логики win-back и промо-офферов → тестирование в Sandbox с имитацией истечения подписки (Sandbox сокращает периоды: месяц = 5 минут).
Сроки — 3–5 дней в зависимости от количества тиров, наличия бэкенда и требований к аналитике отписок.







