What are the subscription renewal states, how do grace period and billing retry differ, and who should keep access?
Predict first
What access does the user have through a payment failure, with and without grace period?
import StoreKit
enum State: String { case subscribed, grace, billingRetry, expired, revoked }
func hasAccess(_ s: State) -> Bool { s == .subscribed || s == .grace }
// renewal-state raw values StoreKit reports (the App Store Server API status uses the same numbers)
let real: [(String, Product.SubscriptionInfo.RenewalState)] = [
("subscribed", .subscribed), ("expired", .expired), ("billingRetry", .inBillingRetryPeriod),
("grace", .inGracePeriod), ("revoked", .revoked)]
print(real.map { "\($0.0)=\($0.1.rawValue)" }.joined(separator: " "))
enum Event { case renewed, paymentFailed, graceEnded, retryGaveUp, recovered, refunded }
func next(_ s: State, _ e: Event, graceEnabled: Bool) -> State {
switch (s, e) {
case (_, .refunded): return .revoked
case (.subscribed, .paymentFailed), (.grace, .paymentFailed): return graceEnabled ? .grace : .billingRetry
case (.grace, .graceEnded): return .billingRetry
case (.billingRetry, .retryGaveUp), (.grace, .retryGaveUp): return .expired
case (.grace, .recovered), (.billingRetry, .recovered), (.subscribed, .renewed), (.expired, .renewed): return .subscribed
default: return s
}
}
for grace in [true, false] {
var s = State.subscribed
var trace = ["subscribed"]
for e in [Event.paymentFailed, .graceEnded, .recovered, .paymentFailed, .graceEnded, .retryGaveUp] {
s = next(s, e, graceEnabled: grace)
trace.append("\(s.rawValue)\(hasAccess(s) ? "+" : "-")")
}
print("grace period \(grace ? "on " : "off"):", trace.joined(separator: " > "))
}
- grace on: subscribed > grace+ > billingRetry- > subscribed+ > grace+ > billingRetry- > expired-; grace off goes straight to billingRetry-
- access is kept during every failure state
- billing retry keeps access and grace does not
Mental model. A gym membership whose card bounced: grace is "come in this week while we sort it", billing retry is "the door is locked, but we keep trying your card".
An auto-renewable subscription reports a Product.SubscriptionInfo.RenewalState: .subscribed, .expired, .inBillingRetryPeriod, .inGracePeriod or .revoked. The check prints the raw values StoreKit returns (1 to 5 in that order), the same numbers the App Store Server API uses for a subscription's status, which lets client and server share one vocabulary. When a renewal payment fails, Apple retries for a limited time (billing retry; Apple documents up to 60 days). If you enabled billing grace period in App Store Connect (a few days, configurable per app), the user keeps access while Apple retries (.inGracePeriod); if not, or once grace ends, the state is .inBillingRetryPeriod and access should stop even though Apple is still trying to collect. Recovery returns the user to .subscribed with a new expiry; giving up leads to .expired; a refund or family-sharing loss leads to .revoked.
The model in the check is a small reducer that encodes this policy: with grace on, a failed payment is grace+ (access kept, + marks access), then billingRetry- after grace; with grace off it goes straight to billingRetry-. It is a model of the documented behaviour, not Apple's code, and the exact day counts and timings were not exercised against a live store. Two details interviewers like: RenewalInfo.willAutoRenew turning false does not end access (the user cancelled renewal but paid until the period end), and expirationReason/gracePeriodExpirationDate let you show "update payment" banners at the right time. On the client read Product.SubscriptionInfo.status (or currentEntitlements); on the server derive the same state from Server Notifications V2 (DID_FAIL_TO_RENEW with subtype GRACE_PERIOD, then EXPIRED with subtype BILLING_RETRY).
Only subscribed and grace keep access; the raw values are StoreKit's own.
Common traps
- Treating "cancelled" as "expired". Turning off auto-renew only stops the next renewal; access lasts until the paid period ends.
- Keeping access during billing retry but not grace (or the reverse). Grace keeps access, billing retry does not.
They might follow up with
- How do I nudge a user in grace? Show an "update payment method" banner using the grace expiry date;
showManageSubscriptionsopens the system sheet. - Is the state per user? It is per subscription group and Apple ID, so a family member can have a different state.
Related questions
- 28.2.2 How do subscription groups and levels govern upgrades, and what are introductory, promotional, offer-code and…
- 28.3.1 How do you compute what a user is entitled to, and which transactions are excluded from…
- 28.4.3 How do App Store Server Notifications V2 behave, and how do you process them idempotently and out of order?
- 17.4.2 How does StoreKit 2 work: products, purchases, transaction verification, entitlements, refunds, and what must…
How do subscription groups and levels govern upgrades, and what are… →