How to Restore Purchases in React Native

Overview

When users reinstall your app, switch devices, or sign in on a new install, they need a way to restore subscriptions and in-app purchases they already paid for. In react-native-purchases, RevenueCat's restorePurchases() method re-syncs the user's store purchase history with RevenueCat and updates the CustomerInfo object with any active entitlements.

This call is user-initiated: it should be wired to a visible "Restore Purchases" button. Apple's App Store Review Guidelines (3.1.1) require apps that sell non-consumable purchases or subscriptions to provide a visible restore mechanism, so a Restore button is effectively mandatory for iOS submission. Common scenarios where restore is needed:

  • User reinstalls the app or sets up a new device
  • User is signed in with the same Apple ID or Google account that made the original purchase
  • Local entitlement state looks wrong and needs to be re-synced from the store
  • App review requires a working Restore Purchases button on the paywall
Prerequisites: The RevenueCat SDK must already be installed and configured with Purchases.configure(...). See the React Native codelab and the Configure the SDK guide for setup.

Basic Restore Call

The core API is a single async method. Purchases.restorePurchases() resolves with a CustomerInfo object that reflects the latest entitlement state after re-syncing with the store. Inspect the active entitlements to decide whether anything was restored:

typescript
import Purchases from 'react-native-purchases';

async function restore() {
  const customerInfo = await Purchases.restorePurchases();
  const isPro = typeof customerInfo.entitlements.active['premium'] !== 'undefined';

  if (isPro) {
    // Unlock premium features.
  } else {
    // No active purchases were found for this account.
  }
}

Replace 'premium' with the identifier of the entitlement you configured in the RevenueCat dashboard. customerInfo.entitlements.active is a map keyed by entitlement identifier, so a typeof ... !== 'undefined' check tells you whether that entitlement is currently active.

A Reusable Hook

In a React Native app it is convenient to wrap the restore flow in a hook that owns the loading state. The useRestorePurchases hook below exposes a restore function and a loading boolean so any screen can drive a button without duplicating logic:

tsx
import { useState, useCallback } from 'react';
import Purchases from 'react-native-purchases';

type RestoreResult = 'restored' | 'nothing' | 'error' | 'cancelled';

export function useRestorePurchases(entitlementId: string = 'premium') {
  const [loading, setLoading] = useState(false);

  const restore = useCallback(async (): Promise<RestoreResult> => {
    setLoading(true);
    try {
      const customerInfo = await Purchases.restorePurchases();
      const isPro =
        typeof customerInfo.entitlements.active[entitlementId] !== 'undefined';
      return isPro ? 'restored' : 'nothing';
    } catch (e: any) {
      // The user dismissing the system dialog is not a real error.
      if (e.userCancelled) {
        return 'cancelled';
      }
      console.warn('Restore failed:', e.message);
      return 'error';
    } finally {
      setLoading(false);
    }
  }, [entitlementId]);

  return { restore, loading };
}

The hook always flips loading back off in the finally block, so the button is re-enabled whether the restore succeeds, finds nothing, is cancelled, or fails. Returning a typed result lets the caller decide how to present each outcome (a success toast, a "no purchases found" alert, and so on).

Error Handling

restorePurchases() rejects when something goes wrong, but a user dismissing the system authentication dialog is a common, expected case that should not be shown as an error. The caught error may expose a userCancelled boolean and a message string. Guard on userCancelled before surfacing anything to the user:

typescript
import Purchases from 'react-native-purchases';
import { Alert } from 'react-native';

async function restore() {
  try {
    const customerInfo = await Purchases.restorePurchases();
    const isPro =
      typeof customerInfo.entitlements.active['premium'] !== 'undefined';

    Alert.alert(
      isPro ? 'Purchases restored' : 'No purchases found',
      isPro
        ? 'Your premium access has been restored.'
        : 'We could not find any purchases for this account.'
    );
  } catch (e: any) {
    // Only show an error when the user did NOT cancel the flow.
    if (!e.userCancelled) {
      Alert.alert('Restore failed', e.message ?? 'Please try again later.');
    }
  }
}
Always check userCancelled first. If the user taps the Restore button and then dismisses the App Store or Google Play sign-in sheet, the promise rejects with userCancelled === true. Showing an error alert in that case is confusing, so swallow that path and only report genuine failures.

A Restore Button Component

Combine the hook with a simple component. The button is disabled and shows a spinner while a restore is in flight, and it presents the right message for each result:

tsx
import React from 'react';
import { Pressable, Text, ActivityIndicator, Alert, StyleSheet } from 'react-native';
import { useRestorePurchases } from './useRestorePurchases';

export function RestorePurchasesButton() {
  const { restore, loading } = useRestorePurchases('premium');

  const onPress = async () => {
    const result = await restore();
    switch (result) {
      case 'restored':
        Alert.alert('Purchases restored', 'Your premium access is back.');
        break;
      case 'nothing':
        Alert.alert('No purchases found', 'We could not find any purchases for this account.');
        break;
      case 'error':
        Alert.alert('Restore failed', 'Please check your connection and try again.');
        break;
      case 'cancelled':
        // User dismissed the dialog: do nothing.
        break;
    }
  };

  return (
    <Pressable
      style={styles.button}
      onPress={onPress}
      disabled={loading}
      accessibilityRole="button"
      accessibilityLabel="Restore Purchases"
    >
      {loading ? (
        <ActivityIndicator color="#fff" />
      ) : (
        <Text style={styles.label}>Restore Purchases</Text>
      )}
    </Pressable>
  );
}

const styles = StyleSheet.create({
  button: {
    backgroundColor: '#F2545B',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    alignItems: 'center',
  },
  label: { color: '#fff', fontSize: 16, fontWeight: '600' },
});

Place this button somewhere persistent and discoverable, such as the footer of your paywall and a row in your settings screen, so reviewers and returning users can always find it.

Important Notes

Only call on a user tap, not on every launch. restorePurchases() triggers a store query and a RevenueCat API call. Calling it automatically on every app launch is unnecessary (RevenueCat already keeps active subscriptions in sync) and slows down startup. Wire it to the Restore Purchases button only.
A visible Restore button is required on iOS. App Store Review Guideline 3.1.1 requires apps that sell subscriptions or non-consumable products to offer a visible restore mechanism. Missing or hidden Restore buttons are a common cause of App Store rejections.
restorePurchases() vs syncPurchases(). restorePurchases() is the user-initiated method tied to your Restore button: it re-syncs the store purchase history and updates CustomerInfo. syncPurchases() is a lower-level method for migrations and advanced flows that you call programmatically, not from a restore button. Most apps only need restorePurchases(). See the Sync Purchases guide for when syncPurchases() applies.
Restore depends on the store account. Restore re-syncs purchases for the Apple ID or Google account currently signed in on the device. If the original purchase was made on a different store account, restore will not find it. Encourage users to verify they are signed in with the same account that made the purchase.

FAQ

How do I restore purchases in react-native-purchases?
Call await Purchases.restorePurchases() inside a try/catch block. It returns a CustomerInfo object. Check typeof customerInfo.entitlements.active['premium'] !== 'undefined' to see whether an active entitlement was restored, and trigger it only from a visible Restore Purchases button.

When should I call restorePurchases()?
Only when the user explicitly taps a Restore Purchases button, typically on a paywall or settings screen. Do not call it automatically on every app launch.

How do I handle a cancelled restore?
The caught error may have a userCancelled boolean. In your catch block, check if (!e.userCancelled) before showing an error so you do not display a message when the user simply dismissed the system dialog.

What is the difference between restorePurchases and syncPurchases?
restorePurchases() is user-initiated and tied to a visible button. syncPurchases() is a lower-level method for migrations and advanced flows that you call programmatically. Most apps only need restorePurchases().

Related Guides