How to Restore In-App Purchases on iOS
Introduction
Restoring purchases is a fundamental feature for any iOS app that sells subscriptions or in-app purchases. Apple's App Store Review Guidelines require that all apps offering in-app purchases include a way for users to restore those purchases. Without it, your app can be rejected during review.
Common scenarios where users need to restore:
- Reinstalling the app after deleting it
- Setting up a new iPhone or iPad
- Downloading the app on a second device signed in to the same Apple ID
- After an app update that lost local purchase state
Prerequisites
- RevenueCat iOS SDK installed (via Swift Package Manager or CocoaPods)
- SDK configured with your API key in your App entry point
- At least one entitlement configured in the RevenueCat dashboard
The restorePurchases() API
RevenueCat's restorePurchases() method does the following in sequence:
- Fetches the current App Store receipt from the device
- Sends the receipt to RevenueCat's servers for validation with Apple
- Syncs any active subscriptions or purchases to the current customer record
- Returns an updated
CustomerInfoobject with all active entitlements
Here is the basic usage with Swift's modern async/await syntax:
// Basic restore using async/await
do {
let customerInfo = try await Purchases.shared.restorePurchases()
if customerInfo.entitlements["premium"]?.isActive == true {
// User has premium access
print("Purchases restored successfully")
} else {
print("No active purchases found to restore")
}
} catch {
print("Error restoring purchases: \(error.localizedDescription)")
}
The returned CustomerInfo object reflects the user's current subscription state after the restore.
Always check entitlements by their identifier (e.g., "premium") rather than by product ID —
this keeps your feature-gating logic decoupled from specific products.
SwiftUI Implementation
The following is a self-contained, production-ready RestorePurchasesButton SwiftUI view.
It handles the loading state, shows an alert with the result, and disables itself during the async operation
to prevent duplicate taps.
import SwiftUI
import RevenueCat
struct RestorePurchasesButton: View {
@State private var isRestoring = false
@State private var showAlert = false
@State private var alertMessage = ""
var body: some View {
Button(action: restorePurchases) {
if isRestoring {
ProgressView()
.progressViewStyle(.circular)
} else {
Text("Restore Purchases")
}
}
.disabled(isRestoring)
.alert("Restore Purchases", isPresented: $showAlert) {
Button("OK") { }
} message: {
Text(alertMessage)
}
}
private func restorePurchases() {
isRestoring = true
Task {
do {
let customerInfo = try await Purchases.shared.restorePurchases()
if customerInfo.entitlements.active.isEmpty {
alertMessage = "No active purchases found. If you believe this is an error, please contact support."
} else {
alertMessage = "Your purchases have been successfully restored!"
}
} catch {
alertMessage = "Failed to restore purchases: \(error.localizedDescription)"
}
isRestoring = false
showAlert = true
}
}
}
Drop this button into your paywall or settings screen:
struct PaywallView: View {
var body: some View {
VStack(spacing: 16) {
// ... your pricing options ...
RestorePurchasesButton()
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
UIKit Implementation
For apps that haven't migrated to SwiftUI, here is a UIKit implementation using the closure-based callback API:
@IBAction func restorePurchasesTapped(_ sender: UIButton) {
sender.isEnabled = false
Purchases.shared.restorePurchases { customerInfo, error in
sender.isEnabled = true
if let error = error {
self.showAlert(title: "Error", message: error.localizedDescription)
return
}
if customerInfo?.entitlements["premium"]?.isActive == true {
self.showAlert(title: "Success", message: "Purchases restored successfully!")
self.updateUIForPremium()
} else {
self.showAlert(
title: "Not Found",
message: "No active purchases found. Contact support if you believe this is an error."
)
}
}
}
private func showAlert(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
Using RevenueCat's Built-in Restore Button
If you are using RevenueCat's Paywalls (the PaywallView or
PaywallViewController from RevenueCatUI), a "Restore Purchases" button is
included automatically. You do not need to add one yourself.
The built-in button appears at the bottom of every RevenueCat paywall template and behaves identically
to the manual implementation above. If you are building a fully custom paywall UI, use the
RestorePurchasesButton component shown above.
import RevenueCatUI
// RevenueCat's PaywallView includes a restore button automatically
struct AppPaywall: View {
var body: some View {
// The restore purchases button is built in — no extra code needed
PaywallView()
}
}
Important Notes
restorePurchases() with no active purchases, RevenueCat may create a new anonymous ID.
If your app has its own authentication system, log the user in with
Purchases.shared.logIn(appUserID:) before calling restore to ensure purchases are
attributed to the correct account.
restorePurchases() when the user
explicitly taps the restore button. Calling it automatically on every app launch is unnecessary,
triggers App Store network requests, and can slow down your app startup.
Related Guides
- Full iOS In-App Purchases Tutorial — End-to-end RevenueCat iOS integration
- Restore Purchases on Android (Kotlin) — The Android equivalent of this guide
- Check Subscription Status on iOS — How to gate features using CustomerInfo
- Troubleshooting — Common RevenueCat issues and solutions