Реалізація 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 днів залежно від кількості тирів, наявності бекенду та вимог до аналітики відписок.







