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
App Store Requirement: Apple's guidelines (Section 3.1.1) mandate that any app with in-app purchases must include a "Restore Purchases" mechanism that is easy for users to find. Apps without this can be rejected.

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:

  1. Fetches the current App Store receipt from the device
  2. Sends the receipt to RevenueCat's servers for validation with Apple
  3. Syncs any active subscriptions or purchases to the current customer record
  4. Returns an updated CustomerInfo object with all active entitlements

Here is the basic usage with Swift's modern async/await syntax:

swift
// 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.

swift
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:

swift
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:

swift
@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.

swift
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

App Store Requirement: Apple requires the restore button to be easily accessible to users. Place it on your paywall screen or in your app's Settings/Account section. Do not hide it behind multiple taps.
Anonymous Users: If your app uses RevenueCat's anonymous user IDs and a user calls 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.
Do Not Auto-Restore on Launch: Only call 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