How do you compute what a user is entitled to, and which transactions are excluded from Transaction.currentEntitlements?
Predict first
Which products are entitled at day 30 and at day 500?
import Foundation
enum Kind { case consumable, nonConsumable, subscription }
struct Tx {
let id: Int, product: String, kind: Kind
var expires: Int? = nil, revoked: Bool = false, upgraded: Bool = false
}
func entitled(_ txs: [Tx], now: Int) -> [String] {
txs.filter { t in
if t.revoked { return false }
switch t.kind {
case .consumable: return false
case .nonConsumable: return true
case .subscription: return !t.upgraded && (t.expires ?? 0) > now
}
}.map(\.product).sorted()
}
let history = [
Tx(id: 1, product: "coins100", kind: .consumable),
Tx(id: 2, product: "lifetime", kind: .nonConsumable),
Tx(id: 3, product: "pro.monthly", kind: .subscription, expires: 40, upgraded: true),
Tx(id: 4, product: "pro.annual", kind: .subscription, expires: 400),
Tx(id: 5, product: "extras", kind: .nonConsumable, revoked: true),
Tx(id: 6, product: "old.sub", kind: .subscription, expires: 20),
]
print("at day 30:", entitled(history, now: 30))
print("at day 500:", entitled(history, now: 500))
- day 30: annual and lifetime; day 500: lifetime only
- day 30: coins, lifetime, monthly, annual; day 500: all
- day 30: lifetime only; day 500: nothing
Mental model. A ticket scanner at the door does not remember who bought a ticket; it reads the ticket now and checks it is valid today.
The rule of thumb: derive access, never store it. At launch, after every Transaction.updates element, and after a purchase, iterate Transaction.currentEntitlements, verify each result, and rebuild a set of owned product ids; the UI observes that set. The sequence contains non-consumables and auto-renewable subscriptions that are currently valid, and excludes consumables, expired subscriptions, revoked transactions (refunds, family sharing removed) and subscription transactions that were upgraded to another product in the group (isUpgraded), so a user who upgraded monthly to annual shows only the annual one. Non-renewing subscriptions are the exception: the store has no expiry for them, so you compute it from purchaseDate plus the term you promised, and you keep your own record.
The check is a model of those rules on a hand-made history: at day 30 the entitlements are the lifetime unlock and the annual plan (monthly was upgraded, coins are consumables, extras are revoked, old.sub expired on day 20) and at day 500 only the lifetime unlock remains. Because it is a pure function of the signed history and the clock, you can unit-test it without a store, and the same function can run on the server over transactions from the App Store Server API. Practical points: cache the last result for offline launches but re-derive when the store answers; for subscriptions inside a group use Product.SubscriptionInfo.status when you need renewal state (grace, retry) rather than only a yes/no; make the entitlement a small struct (tier, expiry, source) so the UI can show "renews on" or "expired on"; and gate features through one place, not scattered if purchased checks.
Consumables, revoked, upgraded and expired transactions are excluded.
Common traps
- Persisting "isPro = true" at purchase time. A refund, expiry or family-sharing change never clears it; re-derive from entitlements.
- Expecting consumables in
currentEntitlements. They are not there; keep the balance yourself.
They might follow up with
- What if the user is offline at launch? Entitlements are signed and cached on the device, so StoreKit usually still answers; keep your own last-known state with a sensible grace limit.
- How is this shared with a server? Send the verified JWS or an
appAccountToken-linked identifier and let the server derive the same set.
Related questions
- 28.1.4 Why start a Transaction.updates listener at launch, and what do finish() and idempotent delivery guarantee?
- 28.3.2 How do restore, Family Sharing and refunds work in StoreKit 2, and how should the app remove access?
- 28.2.1 What are the subscription renewal states, how do grace period and billing retry differ, and who should keep…
- 17.4.2 How does StoreKit 2 work: products, purchases, transaction verification, entitlements, refunds, and what must…
How do restore, Family Sharing and refunds work in StoreKit 2, and… →