Your rejection cites Guideline 3.1.1 with language like “the app does not allow users to restore previously purchased non-consumable in-app purchases or subscriptions.” Your paywall has crisp Subscribe and Buy Now buttons but no Restore. Or you do have a Restore action, but it’s buried under Settings → Account → Subscriptions → tap twice, and the reviewer can’t find it from the paywall they hit on launch. Or your app uses an email-and-password login that you considered “the restore path,” but Apple wants a separate StoreKit-call button anyway.
For any non-consumable IAP or auto-renewable subscription, Apple expects a visible, working Restore Purchases entry point. The fix is mechanical: add a labeled button to your paywall and to a top-level Settings entry, wire it to a StoreKit restore call, surface success/failure state, and the next submission moves through.
Common causes
Ordered by hit rate.
1. Paywall has only Buy buttons, no Restore
The original design optimized for conversion: every pixel on the paywall says buy. Restore was treated as a “later” feature and never added.
How to spot it: Open your paywall on a fresh build. If you cannot count a “Restore Purchases” link or button without scrolling, this is your case. Reviewer-side: their rejection cites a paywall screenshot.
2. Restore exists but is buried in deep navigation
Restore lives at Settings → Account → Subscription → Manage → Restore. A reviewer with 60 seconds will not find it. Apple wants restore reachable in one or two taps from the paywall or main settings.
How to spot it: Count the taps from launch to the Restore button. If it’s more than 2, it’s too deep.
3. The app treats account login as restore
You ship an email login. Users log into their account, and the server tells them they’re Pro. You assume this is “restore.” Apple disagrees — restore must call StoreKit and reconcile with Apple’s purchase history, not just your account database.
How to spot it: Search your code for restoreCompletedTransactions or Transaction.currentEntitlements. If neither is called from a UI button, you’re missing the StoreKit restore primitive.
4. Restore exists on iOS but not on a new redesigned screen
You redesigned the paywall recently. The old paywall had Restore; the new one launches without it because the engineer porting the design missed the small text link. iOS gets the missing UI in production while Android keeps working.
How to spot it: Diff the current paywall view against the previous version’s git history. If a Restore Purchases view or button was removed, that’s the regression.
5. Restore action runs but doesn’t surface results
You have the button. Tapping it calls StoreKit. But nothing happens visibly — no loading spinner, no success alert, no failure message. Reviewer taps, sees nothing, assumes broken, rejects.
How to spot it: Manually tap Restore on a clean install. If you don’t see explicit feedback within 5 seconds, the reviewer didn’t either.
6. Restore is behind a login wall on a free tier
The paywall is reachable but the Restore button is only visible after the user creates a free account. Apple expects restore to be reachable without any sign-up; otherwise a returning paid user has to create a new free account just to restore.
How to spot it: Cold install, do not sign up, navigate to the paywall. If Restore isn’t visible there, fix.
Information to collect
- The reviewer’s rejection text and which Guideline section it cites.
- Screenshots of your current paywall, settings, and any restore flow.
- Your StoreKit version (SK1 vs SK2) and current restore implementation.
- Whether your app has free-tier users who shouldn’t see paywall on launch.
- Analytics on how often legitimate users currently tap Restore (helps you UX-tune the new placement).
Shortest path to fix
Step 1: Add a Restore Purchases button to the paywall
In your paywall view, place a clear “Restore Purchases” text link or button. Standard placement: below the Buy / Subscribe primary CTA, with smaller (but readable, ≥14pt) text. Don’t visually downplay it.
// SwiftUI example
VStack(spacing: 16) {
Button("Subscribe — $9.99/month") { /* buy */ }
.buttonStyle(.borderedProminent)
Button("Restore Purchases") {
Task { await store.restore() }
}
.font(.subheadline)
.foregroundColor(.secondary)
}
Step 2: Add a second entry in Settings
Even users who never hit your paywall (e.g., returning subscribers on a fresh install) need a path to restore. Add Settings → Restore Purchases as a top-level row, not buried under Account.
NavigationLink(destination: SettingsView()) {
Section {
Button("Restore Purchases") {
Task { await store.restore() }
}
}
}
Step 3: Wire the restore action to StoreKit
For StoreKit 2:
func restore() async {
isRestoring = true
do {
try await AppStore.sync() // surfaces any missed transactions
for await result in Transaction.currentEntitlements {
guard case .verified(let txn) = result else { continue }
await applyEntitlement(productID: txn.productID)
}
statusMessage = "Restored successfully."
} catch {
statusMessage = "Restore failed: \(error.localizedDescription)"
}
isRestoring = false
}
For StoreKit 1:
SKPaymentQueue.default().restoreCompletedTransactions()
// implement paymentQueueRestoreCompletedTransactionsFinished + paymentQueue:restoreCompletedTransactionsFailedWithError
Step 4: Surface progress and result clearly
On tap:
- Show a loading spinner or progress text immediately.
- On success: show “Your purchases were restored” with the product names found. Auto-dismiss after 2 seconds.
- On no purchases: show “No previous purchases found”.
- On error: show the error message and a retry button.
A reviewer must see something happen within 5 seconds of tapping.
Step 5: Document the path in App Review notes
Add to App Review Information:
RESTORE PURCHASES
- Available on the paywall (bottom, "Restore Purchases" link).
- Also available in Settings > Restore Purchases.
- Tap the button; on a fresh install, this triggers StoreKit sync.
- Test with sandbox tester apple-iap-sandbox@yourdomain.com (Pro is pre-purchased).
Step 6: Resubmit
In App Store Connect → App Store → tap the build → Submit for Review. Restore-button fixes that don’t change other UI go through quickly; expect 24-hour median.
How to confirm the fix
- A cold-install build shows Restore Purchases on the paywall without scrolling.
- Settings has a top-level Restore Purchases row.
- Tapping Restore shows immediate progress feedback.
- A sandbox tester with an existing purchase sees their entitlement applied after tapping Restore.
- A fresh tester with no purchases sees “No previous purchases found.”
If it still fails
- If the rejection persists, attach a 30-second QuickTime video in Resolution Center showing the new Restore flow from paywall to result.
- Verify the restore call actually contacts StoreKit (check Console.app logs for
StoreKit.Transaction); if not, your code is broken even though the button exists. - Try restoring with multiple sandbox testers to confirm the path works for different account states.
- Check that the Restore button is enabled (not greyed out) on a fresh launch; some implementations gate Restore behind a network call.
Prevention
- Treat Restore Purchases as a required UI primitive on every paywall and settings screen — bake it into your component library.
- Add a UI test that asserts the Restore button is visible on the paywall and in Settings on every PR.
- Include Restore wiring in your boilerplate / starter template so a new project never ships without it.
- Audit your paywall in every redesign — review the diff specifically for whether Restore survived the refactor.
- Track how often Restore is tapped; a sudden drop after a release signals a regression.