# "Restore Purchases" button accessible and functional

- **Pattern:** `ab-000406` (`app-store-iap-subscriptions.restore-entitlements.restore-purchases`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000406
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000406)

## Why it matters

Apple guideline 3.1.1 explicitly requires a "Restore Purchases" mechanism for all non-consumable IAP and subscriptions. Its absence is a rejection reason — but the practical impact compounds post-launch: users who delete and reinstall the app, upgrade to a new device, or restore from backup lose access to content they paid for. These users generate support tickets, file chargebacks with their bank or credit card company, and leave one-star reviews. Chargebacks damage the developer's payment processor standing even though Apple actually processed the payment. A functional restore path prevents all of this at minimal implementation cost.

## Severity rationale

Critical because Apple guideline 3.1.1 explicitly mandates a restore mechanism, and its absence causes immediate rejection — not a warning.

## Remediation

Add a "Restore Purchases" button to your paywall and/or settings screen. The button must be reachable without completing a purchase — users who reinstalled the app have no other path to reclaim their access.

```tsx
// PaywallScreen.tsx or SettingsScreen.tsx
const handleRestore = async () => {
  try {
    const customerInfo = await Purchases.restorePurchases();
    if (customerInfo.entitlements.active['premium']) {
      unlockPremium();
      Alert.alert('Purchases Restored', 'Your subscription has been restored.');
    } else {
      Alert.alert('No Purchases Found', 'No active subscription was found for this account.');
    }
  } catch (e) {
    Alert.alert('Restore Failed', 'Could not restore purchases. Please try again.');
  }
};

<TouchableOpacity onPress={handleRestore}>
  <Text>Restore Purchases</Text>
</TouchableOpacity>
```

Place the restore button in both the paywall and the settings/account screen. Verify that after restore, the entitlement state is refreshed and the premium UI unlocks immediately.

## Detection

- **ID:** `restore-purchases`
- **Severity:** `critical`
- **What to look for:** Count all relevant instances and enumerate each. Search for restore-related calls in source files: `Purchases.restorePurchases()` (RevenueCat), `restoreCompletedTransactions()` (StoreKit 1), `AppStore.sync()` (StoreKit 2), `restorePurchases()` (react-native-iap), `InAppPurchase.completePurchase()` with restore flag (Flutter), `BillingClient.queryPurchasesAsync()` for query-based restore (Play Billing). Find where this function is called — trace back to the UI element that triggers it. Verify: (1) A "Restore Purchases" button exists and is rendered. (2) It is accessible on the paywall OR in a settings/account screen reachable without completing a purchase (users who deleted and reinstalled the app cannot make a new purchase to access settings). (3) The button is visible — not hidden behind a menu with no visual affordance. (4) After restore, entitlements are refreshed and premium UI is unlocked if applicable. Fail pattern: restore logic exists in code but the button is commented out, behind a `__DEV__` guard, or inside a screen only accessible after purchasing.
- **Pass criteria:** A "Restore Purchases" button (or equivalent link) is present in the paywall and/or settings screen, is accessible to logged-in and guest users alike, and calls the platform's restore API which then updates the local entitlement state. At least 1 implementation must be verified.
- **Fail criteria:** No restore button visible in paywall or settings; restore button exists but is unreachable without first purchasing; restore call is stubbed or does nothing after triggering; restore button hidden behind a debug flag.
- **Skip (N/A) when:** No IAP detected in the app.
- **Detail on fail:** `"No 'Restore Purchases' button found in PaywallScreen.tsx or Settings — Apple guideline 3.1.1 requires restore to be available"` or `"restorePurchases() is called in Settings but Settings screen is only accessible after subscribing — new users who reinstalled the app cannot restore"`
- **Remediation:** Apple explicitly requires a "Restore Purchases" mechanism for all non-consumable and subscription IAP. Absence causes rejection under guideline 3.1.1.
  1. Add a "Restore Purchases" button to your paywall:
     ```tsx
     <TouchableOpacity onPress={handleRestore}>
       <Text style={styles.restoreText}>Restore Purchases</Text>
     </TouchableOpacity>
     ```
  2. For RevenueCat:
     ```tsx
     const handleRestore = async () => {
       try {
         const customerInfo = await Purchases.restorePurchases();
         if (customerInfo.entitlements.active['premium']) {
           unlockPremium();
           Alert.alert('Purchases Restored', 'Your subscription has been restored.');
         } else {
           Alert.alert('No Purchases Found', 'No active subscription was found for this account.');
         }
       } catch (e) {
         Alert.alert('Restore Failed', 'Could not restore purchases. Please try again.');
       }
     };
     ```
  3. Place the restore button in both the paywall and the settings/account screen for discoverability

## External references

- external apple-guideline-3.1.1 — https://developer.apple.com/app-store/review/guidelines/#in-app-purchase

Taxons: regulatory-conformance, user-experience

HTML version: https://auditbuffet.com/patterns/ab-000406
