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.
Critical because Apple guideline 3.1.1 explicitly mandates a restore mechanism, and its absence causes immediate rejection — not a warning.
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.
// 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.
app-store-iap-subscriptions.restore-entitlements.restore-purchasescriticalPurchases.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."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"<TouchableOpacity onPress={handleRestore}>
<Text style={styles.restoreText}>Restore Purchases</Text>
</TouchableOpacity>
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.');
}
};