iOS In-App Purchases Implementation (Subscriptions)
Auto-renewable subscriptions — most complex IAP type. Not because StoreKit is complex itself, but ecosystem around it grows: grace periods, billing retry, downgrade/upgrade between tiers, promo offers, introductory pricing, and finally — processing churn via expirationIntent.
Subscription Lifecycle and Where It Breaks
Subscription in iOS exists not only while active. Apple automatically renews 24 hours before expiration. If payment fails — billing retry period starts (up to 60 days). During this time subscription status expired, but Apple continues charge attempts. Most apps block access immediately after expirationDate — this is wrong.
Correct logic: check renewalInfo.isInBillingRetryPeriod. If true — give grace period (configured in App Store Connect, usually 6 days for annual, 3 for monthly). User with card problem shouldn't lose access immediately.
StoreKit 2 makes this transparent:
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()
}
}
}
Tiers and Transitions Between Them
If app has several tiers (Basic, Pro, Enterprise) — need subscription group in App Store Connect. All tiers in one group, user can have only one active subscription per group simultaneously.
Upgrade (to more expensive tier) — takes effect immediately, Apple recalculates remainder. Downgrade — takes effect at next billing period. Crossgrade (same price, different tier) — depends on setting: can be made immediate or deferred.
Track on client via originalTransactionID and subscriptionGroupID. On server — store full transaction history and handle Apple Server Notifications v2 (App Store Server Notifications). Event types to mandatory handle: DID_RENEW, DID_FAIL_TO_RENEW, EXPIRED, GRACE_PERIOD_EXPIRED, REFUND.
Introductory and Promotional Offers
Introductory pricing (first N periods at reduced price) configured in App Store Connect and automatically applied for new subscribers. Problem — user who unsubscribed and wants to return doesn't get intro price again by default. For this promotional offers exist — can be issued by your logic (win-back campaigns).
Promotional offer signature generated on server with private key from App Store Connect:
// On client create paymentDiscount
let discount = SKPaymentDiscount(
identifier: "winback_3months",
keyIdentifier: keyID,
nonce: nonce, // UUID from server
signature: signature, // signature from server
timestamp: timestamp
)
payment.paymentDiscount = discount
Without server signature promotional offer invalid — Apple checks signature on its side.
Work Process
Current implementation audit → subscription group structure design → StoreKit 2 integration with grace period and billing retry support → App Store Server Notifications setup on backend → win-back and promo offer logic implementation → Sandbox testing with subscription expiration simulation (Sandbox shortens periods: month = 5 minutes).
Timeline — 3–5 days depending on tier count, backend presence, and churn analytics requirements.







