How to Implement Restore Purchases with RevenueCat on iOS

App Review requires a Restore Purchases control for apps selling subscriptions or non-consumables. Here is how to implement it correctly with RevenueCat and check entitlement status on iOS.

Why Restore Purchases Is Required

Apple's App Review Guidelines require that any app selling non-consumable in-app purchases or auto-renewable subscriptions provide a clearly accessible way for users to restore previously bought content. The reason is straightforward: a user who paid on one device, reinstalled the app, or signed in on a new phone must be able to regain access without paying again. If your app lacks a visible Restore Purchases control, it will very likely be rejected during review, regardless of how well your purchase flow works. RevenueCat makes implementing this simple because its backend already tracks a user's transactions, but you still must expose the control in your UI and wire it to the SDK. This is not optional polish; it is a compliance requirement and a genuine user need. Treat it as a first-class part of your paywall and settings, not an afterthought, so both App Review and your paying customers are satisfied on the first submission. Getting it right also reduces support load, because restore-related confusion is one of the most common reasons paying users contact developers.

How Restore Works with RevenueCat

When a user taps Restore Purchases, RevenueCat asks StoreKit to surface the user's transaction history from their Apple ID, validates it against RevenueCat's backend, and returns an updated CustomerInfo object describing which entitlements are now active. If the user has an active subscription or a previously purchased non-consumable tied to that Apple ID, the relevant entitlement flips to active and your app unlocks the corresponding features. Because RevenueCat maintains a server-side record keyed to an app user identifier, restore also helps consolidate purchases when a user logs into their account on a new device. Under the hood this uses Apple's restore mechanism, so it operates on whatever Apple ID is signed into the device, not on your app's own login. That distinction matters: restore recovers purchases made with the current Apple ID, while your app user identifier is how RevenueCat associates those purchases with your account system across platforms and devices. Understanding this split explains most restore edge cases, because the Apple ID and your app's account are two independent identities that restore has to reconcile.

Add a Visible Restore Control

Place a Restore Purchases button where users naturally look for it: on your paywall and in a settings or account screen. Label it clearly with text like "Restore Purchases" so App Review and users recognize it immediately; do not hide it behind an obscure icon or bury it several taps deep, since reviewers specifically look for an accessible control. In SwiftUI, this is simply a Button whose action calls your restore function. Because a restore may take a moment while it contacts Apple and RevenueCat, show a loading indicator and disable the button during the operation to prevent duplicate taps. Provide clear feedback for all outcomes: success with entitlements restored, success but nothing to restore, and failure due to a network or Apple error. A user who taps restore and sees no response will assume the app is broken and may leave a poor review. Making the control prominent and responsive is both a compliance requirement and a basic usability expectation for any paid app, so design its states as carefully as you design the purchase button itself.

Call restorePurchases in Swift

The RevenueCat SDK exposes a single async call for this. In your button's action, invoke it and inspect the returned CustomerInfo: do { let customerInfo = try await Purchases.shared.restorePurchases(); if customerInfo.entitlements["pro"]?.isActive == true { /* unlock and confirm */ } else { /* nothing to restore */ } } catch { /* show an error */ }. The restorePurchases method returns the same CustomerInfo type you get from purchases and from fetching customer info, so your entitlement-checking logic is identical everywhere. Always handle three branches explicitly. When an entitlement is active, unlock features and tell the user their purchase was restored. When the call succeeds but no relevant entitlement is active, inform the user there was nothing to restore, perhaps because they are signed into a different Apple ID than the one used to purchase. When the call throws, present a friendly error and let them retry. Keeping this logic centralized makes restore and normal purchase paths converge on one code path, which means you test and maintain a single entitlement-evaluation routine rather than duplicating the same checks in several places.

Check Entitlement Status Anywhere in the App

Restore is really about arriving at an accurate entitlement state, and you check that same state throughout your app to gate features. Rather than re-running restore, read the current CustomerInfo with try await Purchases.shared.customerInfo(), which returns cached data quickly and refreshes from the backend as needed. Then evaluate customerInfo.entitlements["pro"]?.isActive. A clean pattern is a small entitlement manager, an observable object in SwiftUI, that holds a published isPro flag and updates it whenever CustomerInfo changes. Your views observe that flag and show or hide premium features accordingly, so the entire UI reacts consistently to a single source of truth. Check entitlement status on launch and whenever premium content is accessed, but rely on the SDK's cache rather than forcing network calls on every screen. This keeps the app responsive offline, since RevenueCat caches the last known entitlement state, while still converging on the correct answer once connectivity returns. Centralizing the check also means that when you later add a second entitlement or a new tier, you extend one manager rather than hunting for scattered isActive checks across many views.

Listen for Automatic Updates

You do not always need an explicit restore, because RevenueCat can push entitlement changes to your app as they happen. The SDK provides a way to observe CustomerInfo updates, either through an async stream, Purchases.shared.customerInfoStream, or a delegate, depending on your architecture. Subscribe to it once, typically in your entitlement manager, and update your published state whenever a new CustomerInfo arrives. This means that if a subscription renews, expires, or is restored on another device, your UI reflects the change without the user manually tapping anything. It also smooths the common case where a fresh install signs into an existing account and RevenueCat recognizes the user's entitlements. Automatic observation complements the explicit Restore Purchases button rather than replacing it; you still need the visible control for App Review and for users on a different Apple ID than the one currently active on the device. Together they give you both a compliant manual path and a responsive, automatic experience for everyday use across devices, which is the combination that feels correct to users and satisfies reviewers at the same time.

Handle Common Restore Edge Cases

A few situations trip up restore implementations and deserve deliberate handling. First, the user may be signed into a different Apple ID than the one that made the purchase; restore only recovers purchases for the currently signed-in Apple ID, so your "nothing to restore" message should gently suggest checking the Apple ID in Settings. Second, if you use your own account system with app user identifiers, calling logIn before restore ensures purchases attach to the right account, avoiding purchases stranded on an anonymous ID. Third, network failures are normal on mobile, so make restore retryable and never leave the button stuck in a loading state after an error. Fourth, expired subscriptions will restore as inactive entitlements, which is correct behavior, not a bug; the user simply needs to resubscribe, and your messaging should say so rather than implying something failed. Anticipating these cases with clear messaging prevents support tickets and confused reviews, and it makes the difference between a restore flow that merely passes review and one that genuinely serves users on the first try across devices and accounts.

Test Restore Before Submitting

Test restore end to end before you submit, because a broken restore is a common rejection reason. Using a sandbox Apple ID, make a purchase, then delete and reinstall the app, or sign the sandbox account into a second device, and confirm that tapping Restore Purchases reactivates the entitlement and unlocks features. Verify all three outcomes: a successful restore, a restore with nothing to recover using a fresh sandbox account, and a graceful failure with connectivity disabled. Watch the Xcode console with debug logging to confirm the returned CustomerInfo shows the expected active entitlement. Also confirm the button is easy to find, since App Review specifically looks for an accessible restore option and will exercise it. If you rely on your own login, test logging in on a new install and confirm purchases follow the account rather than staying on the anonymous user. Thorough testing here is inexpensive compared to a rejected submission and the multi-day review turnaround it costs, so treat it as mandatory rather than optional, and repeat it whenever you change your purchase or account code.

Frequently Asked Questions

Is Restore Purchases actually required by Apple?

Yes. Apple's App Review Guidelines require a clearly accessible restore mechanism for apps selling non-consumable in-app purchases or auto-renewable subscriptions. Omitting a visible Restore Purchases control is a common cause of rejection, so implement and surface it before submitting.

Which RevenueCat method restores purchases?

Call Purchases.shared.restorePurchases(), an async method that returns an updated CustomerInfo. Inspect customerInfo.entitlements to see which entitlements became active. It uses the same CustomerInfo type as normal purchase and customer-info calls, so your entitlement checks stay consistent.

What if the user has nothing to restore?

The call still succeeds and returns a CustomerInfo with no active relevant entitlement. Handle this as a distinct branch and tell the user there was nothing to restore, possibly because they are signed into a different Apple ID than the one used to purchase.

Do I need restore if I listen for CustomerInfo updates?

Yes. Automatic CustomerInfo observation improves the everyday experience, but App Review still requires an explicit, visible Restore Purchases control, and it is needed for users signed into a different Apple ID. Use both together.

How do I test restore before release?

Use a sandbox Apple ID to make a purchase, then reinstall the app or sign in on another device and tap Restore Purchases, confirming the entitlement reactivates. Also test the nothing-to-restore case and a network-failure case, watching the Xcode debug logs to verify CustomerInfo.