How to Check User Subscription Status on iOS
Introduction
The CustomerInfo object is RevenueCat's central source of truth for a user's subscription
state. It holds every active entitlement, expiry date, trial status, and renewal flag for the current
user. Any time you need to determine whether to show a paywall or unlock a premium feature, you read
from CustomerInfo.
There are three ways to get CustomerInfo:
- On-demand: Call
Purchases.shared.getCustomerInfo()when you need it. - Reactively: Listen for
Purchases.customerInfoUpdatedNotificationto get push updates. - After a transaction: The result of
purchase(package:)andrestorePurchases()both return an updatedCustomerInfo.
Basic Entitlement Check
The most common pattern is a simple async function that returns a boolean. Call this before showing premium content:
// Check if the user has an active "premium" entitlement
func checkPremiumStatus() async -> Bool {
do {
let customerInfo = try await Purchases.shared.getCustomerInfo()
return customerInfo.entitlements["premium"]?.isActive == true
} catch {
print("Error fetching customer info: \(error)")
// Fail safely — do not grant access on error
return false
}
}
// Usage
Task {
let isPremium = await checkPremiumStatus()
if isPremium {
showPremiumContent()
} else {
showPaywall()
}
}
"premium")
is configured in your RevenueCat dashboard and remains stable as you add, remove, or change products.
Gating on product IDs creates brittle code that breaks whenever you update your product catalog.
Reactive Updates with ObservableObject
For a SwiftUI app, the recommended pattern is an ObservableObject that holds subscription
state as a @Published property and listens for RevenueCat update notifications. This keeps
your entire UI in sync automatically without polling.
import SwiftUI
import RevenueCat
class SubscriptionManager: ObservableObject {
@Published var isPremium: Bool = false
@Published var isLoading: Bool = true
init() {
fetchCustomerInfo()
// Listen for real-time updates (renewals, cancellations, etc.)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleCustomerInfoUpdate),
name: Purchases.customerInfoUpdatedNotification,
object: nil
)
}
func fetchCustomerInfo() {
Task { @MainActor in
do {
let customerInfo = try await Purchases.shared.getCustomerInfo()
self.isPremium = customerInfo.entitlements["premium"]?.isActive == true
self.isLoading = false
} catch {
// Keep isLoading false so the UI doesn't hang
self.isLoading = false
print("Error fetching customer info: \(error)")
}
}
}
@objc private func handleCustomerInfoUpdate(_ notification: Notification) {
if let customerInfo = notification.userInfo?["customerInfo"] as? CustomerInfo {
DispatchQueue.main.async {
self.isPremium = customerInfo.entitlements["premium"]?.isActive == true
}
}
}
}
Instantiate this manager once at the app level and inject it via the SwiftUI environment so every screen in your app reads from the same source:
@main
struct MyApp: App {
@StateObject private var subscriptionManager = SubscriptionManager()
init() {
Purchases.configure(withAPIKey: "appl_your_key_here")
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(subscriptionManager)
}
}
}
Gating Features in SwiftUI
With SubscriptionManager in the environment, any view can read subscription status
and conditionally show content:
struct ContentView: View {
@EnvironmentObject var subscriptionManager: SubscriptionManager
@State private var showPaywall = false
var body: some View {
Group {
if subscriptionManager.isLoading {
ProgressView("Loading...")
} else if subscriptionManager.isPremium {
PremiumContentView()
} else {
FreeContentView()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Upgrade") {
showPaywall = true
}
}
}
}
}
.sheet(isPresented: $showPaywall) {
PaywallView()
}
}
}
// A deeply nested view can also access subscription state
struct PremiumFeatureButton: View {
@EnvironmentObject var subscriptionManager: SubscriptionManager
@State private var showPaywall = false
var body: some View {
Button("Export to PDF") {
if subscriptionManager.isPremium {
exportToPDF()
} else {
showPaywall = true
}
}
.sheet(isPresented: $showPaywall) {
PaywallView()
}
}
private func exportToPDF() { /* ... */ }
}
Expiry Dates, Trials, and Grace Periods
The EntitlementInfo object (accessed via customerInfo.entitlements["premium"])
exposes rich metadata beyond just isActive. Use these properties to build smarter UI:
let customerInfo = try await Purchases.shared.getCustomerInfo()
if let premium = customerInfo.entitlements["premium"] {
print("Is active: \(premium.isActive)")
// Expiry date — nil for lifetime purchases
if let expiry = premium.expirationDate {
print("Expires: \(expiry)")
}
// Will the subscription auto-renew?
print("Will renew: \(premium.willRenew)")
// Period type: .normal, .trial, .intro
switch premium.periodType {
case .trial:
print("User is in a free trial")
case .intro:
print("User is in an introductory pricing period")
case .normal:
print("User is on a regular paid subscription")
default:
break
}
// Which store did the purchase come from?
print("Store: \(premium.store)") // .appStore, .playStore, .stripe, etc.
// Is the subscription in a billing retry / grace period?
print("Unsubscribe detected: \(premium.unsubscribeDetectedAt?.description ?? "no")")
print("Billing issue detected: \(premium.billingIssueDetectedAt?.description ?? "no")")
}
Use periodType == .trial to show a "Trial Active" badge in your UI, or
billingIssueDetectedAt != nil to nudge users to update their payment method.
Best Practices
isPremium to
UserDefaults or your own database. RevenueCat's SDK caches CustomerInfo
on disk and keeps it fresh automatically.
getCustomerInfo() in your
sceneDidBecomeActive or .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification))
handler to ensure the state is current when users switch back to your app after managing subscriptions
in the App Store or Settings.
getCustomerInfo() throws a network error,
fall back to the RevenueCat SDK's cached value by catching the error and reading from a previously
known state. Do not block the user from accessing features they already paid for due to a transient
network failure.
Related Guides
- Full iOS In-App Purchases Tutorial — End-to-end RevenueCat iOS integration
- Restore Purchases on iOS — How to implement the restore button
- Implement Free Trials on iOS — Configure and display introductory offers
- Troubleshooting — Common RevenueCat issues and solutions