Guideline 3.1.1

Guideline 3.1.1 rejection: adding a working Restore Purchases to an Expo app with RevenueCat

Last verified by Designated Dev

Short answer

Guideline 3.1.1 asks apps with in-app purchases to have a restore mechanism for anything that can be restored, such as subscriptions and unlocks. App Review looks for a Restore Purchases button and tests that it actually restores access. With RevenueCat in an Expo app, the button calls Purchases.restorePurchases() and checks the returned entitlement. It must be a development or production build, because Expo Go only runs RevenueCat's mock mode.

Any credits or in-game currencies purchased via in-app purchase may not expire, and you should make sure you have a restore mechanism for any restorable in-app purchases.

App Review Guideline 3.1.1, In-App Purchase

At a glance

What you seeWhy it happensThe fix
Rejected: no “Restore Purchases” featureThe paywall has no restore buttonA visible Restore Purchases button that calls restorePurchases()
Rejected: tapping Restore did nothingThe button shows an alert but never calls the SDK or checks the entitlementCall restorePurchases(), check the entitlement, unlock, and tell the user the result
Paywall is empty for the reviewerProducts not submitted with the version, Paid Apps Agreement not active, or metadata incompleteFix App Store Connect first: agreement, product metadata, submit products with the version
Purchases never work in the previewExpo Go runs RevenueCat in a mock modeTest in a development build with sandbox accounts

What the reviewer does

The rejection is usually one of two things. Either there is no restore option at all, or there is one and it doesn’t work. RevenueCat’s community has examples of the second: the reviewer bought a subscription, reinstalled the app, tapped Restore, and “nothing happened.”

So restore has to do real work. It asks the store for the user’s purchases, checks what they’re entitled to, unlocks it, and tells the user what happened.

The fix with RevenueCat in an Expo app

RevenueCat’s Expo guide is explicit: “To use and test RevenueCat with Expo, you’ll need to create an Expo development build.” In Expo Go the SDK runs a preview mode with mock responses, which is why purchases can look fine in a builder’s preview and fail everywhere else.

npx expo install react-native-purchases react-native-purchases-ui

Configure once, early in the app’s life, with the platform’s public SDK key:

import { useEffect } from "react";
import { Platform } from "react-native";
import Purchases from "react-native-purchases";

export function useConfigurePurchases() {
  useEffect(() => {
    Purchases.configure({ apiKey: Platform.OS === "ios" ? "appl_XXXX" : "goog_XXXX" });
  }, []);
}

Then the button. RevenueCat’s docs say restorePurchases “should only be called from some user interaction,” and it returns CustomerInfo, which you check for the entitlement:

import { Alert } from "react-native";
import Purchases from "react-native-purchases";

const ENTITLEMENT_ID = "pro";

// Only call this from the Restore Purchases button's onPress.
export async function onRestorePress(): Promise<boolean> {
  try {
    const customerInfo = await Purchases.restorePurchases();
    const active = typeof customerInfo.entitlements.active[ENTITLEMENT_ID] !== "undefined";
    Alert.alert(active ? "Purchases restored" : "No previous purchases were found for this Apple ID.");
    return active;
  } catch (e) {
    Alert.alert("Restore failed", "Please try again.");
    return false;
  }
}

A restore with nothing to restore still succeeds; the entitlement just isn’t active. Tell the user so. A silent tap is exactly what gets reported as “nothing happened.”

Put the button where a reviewer will look: on the paywall, and again in Settings. Neither Apple nor RevenueCat mandates a location, but a visible button on the paywall is what reviewers look for.

Don’t restore automatically at launch. Apple’s StoreKit documentation says “Don’t automatically restore purchases, especially when your app launches.” If you need a background sync, RevenueCat’s syncPurchases does it without the sign-in prompt, but read its warning about transferring anonymous users first.

If the reviewer sees an empty paywall

That is a store configuration problem, not code, and it’s often cited under guideline 2.1 rather than 3.1.1. Check these in App Store Connect:

  1. Paid Apps Agreement is active, with banking and tax set up. Apple says it “must be Active to test In-App Purchases in the sandbox environment.”
  2. The first product of each type is submitted with a new app version. Apple requires the first consumable, non-consumable and subscription “of each type” to go in with a version.
  3. Every product has complete metadata, including the App Review screenshot. One recent rejection read: “you must provide an App Review screenshot in App Store Connect in order to submit in-app purchases for review.”
  4. Bundle ID and keys match between App Store Connect, RevenueCat and the app, and no StoreKit configuration file is left in the release scheme (from RevenueCat’s troubleshooting guide).
  5. Every product you submit appears on the paywall the reviewer sees. Guideline 2.1(b) asks for purchases that are “visible to the reviewer and functional.”

Changes to product metadata can take up to an hour to reach the sandbox, so wait before resubmitting.

How we handle it

We test purchase, reinstall and restore on a real device with a sandbox account, the same cycle a reviewer runs, fix the restore flow and the entitlement check, and walk through App Store Connect before resubmitting so the reviewer never sees an empty paywall.

Questions

Do I need a Restore button if users have to log in?

Yes, keep one. RevenueCat recommends that all apps give users some way to trigger restorePurchases even when everyone has an account, and Apple asks for a restore mechanism for any restorable purchase.

Can I restore purchases automatically when the app opens?

No. Apple's StoreKit documentation says not to restore automatically, especially at launch, and RevenueCat says restorePurchases should only follow a user action. For a silent sync, RevenueCat provides syncPurchases.

Why is my paywall empty in review but fine on my phone?

Usually the in-app purchases weren't submitted with the app version, the Paid Apps Agreement isn't active, or a product is missing required metadata such as its App Review screenshot.

Does Restore Purchases work in Expo Go?

No. In Expo Go, react-native-purchases runs a preview mode with mock responses, so real purchases and restores need a development or production build.

Do consumable purchases need to be restorable?

The guideline covers restorable purchases. Consumables are used up; RevenueCat notes they can only be restored when you use your own App User IDs.

Sources

  1. Apple: App Review Guidelines, 3.1.1, 3.1.2 and 2.1(b)
  2. Apple: Restoring purchased products
  3. Apple: Overview for configuring in-app purchases
  4. Apple: Submit an in-app purchase
  5. RevenueCat: Restoring purchases
  6. RevenueCat: Installation with Expo
  7. RevenueCat: CustomerInfo and entitlements
  8. RevenueCat: Troubleshooting offerings
  9. RevenueCat: App Store rejections
  10. Expo: In-app purchases

Stuck on this in your own app?

We fix it, test it on real devices, and resubmit.

30 minutes with the engineer who'd do the work. Bring the repo link or the rejection email. Fixed price once scoped, in writing.