iOS · App Review

10 builds, 6 App Review rejections: what Apple wants in 2026.

My first iOS app hit six straight App Review rejections in ten days before it shipped. Every reject taught me something that was not obvious from the guidelines alone. Here's the whole list with the exact fix.

Maksim Shin· April 18, 2026· 8 min read

VSkip turns voice messages from any messenger into a 3-line text summary. SwiftUI front end, Go backend, Groq (Whisper + Llama) for the AI. Two months of evenings to build, ten days and ten builds to push through App Review. This post is every rejection, in order, with what I actually changed to pass.

Reject 1/6 — Guideline 2.1(b) and 3.1.2(c)

What the reviewer saw. The paywall had no inline links to our Terms of Service and Privacy Policy, and the full auto-renewal disclosure sentence was missing from the sheet itself.

Why that's a rejection. Apple treats the purchase sheet as a standalone legal surface. Having the links in the app's Settings, or on the App Store listing, or in the privacy policy URL field, is not enough — they must be present where the user agrees to pay.

The fix. PaywallView now renders Terms and Privacy as Link() elements directly above the subscribe CTA, plus the full "Payment will be charged to your Apple ID account at the confirmation of purchase. Subscription automatically renews…" disclosure in a tertiary-color caption. Identical treatment on the onboarding paywall.

Reject 2/6 — Guideline 3.1.2(c), round two

What the reviewer saw. The paywall promised a 3-day free trial on the annual plan, but when they actually purchased, no trial was applied.

Why. The intro offer was configured in Configuration.storekit (the local StoreKit testing file) — not in App Store Connect itself. Sandbox purchases read from ASC, not from local config. So the code path "showed trial copy" but the server-side record had no offer attached.

The fix. POST to /v1/subscriptionIntroductoryOffers once per territory (the API requires territory as a relationship — no worldwide shortcut exists). For us that was 175 requests. Also made the trial copy conditional on subscription.isEligibleForIntroOffer at runtime so we never advertise a trial the current user can't receive.

Reject 3/6 — Guideline 2.1(b), iPad

What the reviewer saw. On iPad Air 11-inch (M3), the subscription page "loaded indefinitely". They quote the word indefinitely verbatim.

Why. Our Product.products(for:) call hit a StoreKit stall in the review sandbox. The UI showed a "Loading plans…" spinner with no timeout and no error state. The reviewer had no way to proceed.

The fix. Added a 15-second timeout via withThrowingTaskGroup, a ProductLoadState enum (loading, loaded, failed(String)), a retry button when loading fails, and disabled the CTA until products are actually present. Also warm up loadProducts() at app launch via .task on the root scene so the paywall is rarely cold.

Reject 4/6 — Guideline 3.1.2(c), round three

What the reviewer saw. "The auto-renewable subscription promotes the free trial more conspicuously than the billed amount."

Why. Our plan row was Annual label, then "3 days free, then $29.99/year" in the same caption style. Apple wants the billed amount to be the dominant pricing element, with any trial or intro copy in a visibly subordinate style.

The fix. Rewrote the plan row layout. Price is now title3.bold primary-colored — the first thing the eye lands on. "After 3-day free trial" sits below it in caption2.tertiary. Subscribe CTA changed from "Start Free Trial" to "Subscribe — $29.99/year" with a smaller "Includes a 3-day free trial" subcaption.

Reject 5/6 — Guidelines 5.1.1(i) and 5.1.2(i)

What the reviewer saw. "The app appears to share the user's personal data with a third-party AI service but the app does not clearly explain what data is sent, identify who the data is sent to, and ask the user's permission before sharing the data."

Why. This is the big 2025-era rule. Voice audio sent to Groq for transcription counts as personal data, and Apple now requires an in-app disclosure and consent step. Mentioning the AI provider in your privacy policy is necessary but no longer sufficient.

The fix. Built AIDisclosureView — a modal sheet that names the data (audio + transcript), the recipient (Groq, Inc. in California), how we use it, retention (immediate delete after processing), and the right to revoke. Every API call that touches Groq is gated on AIConsentStore.hasConsented. Settings added a toggle to revoke consent. Privacy policy now has an explicit "Data We Process and Send to Third-Party AI" section with everything named.

Reject 6/6 — Guideline 2.1(b), the final boss

What the reviewer saw. "Could not pass the subscription page even after a successful purchase."

Why. This was a design mistake I'd been carrying since day one. After StoreKit verified a purchase, we awaited our backend's /verify-receipt to return a session token before setting tier = .premium. On iPad, with fly.io occasionally slow, that call threw. The catch branch printed an error and returned. tier stayed .free, success evaluated to false, and the paywall's dismiss() never ran. The reviewer had paid and was stuck.

The fix. StoreKit's checkVerified is now the source of truth. The instant it passes, tier = .premium, we return true, the paywall dismisses. Server verification still runs — but in a detached Task { }, and its only job is to hand the Share Extension a session token for rate-limit bypass. A slow backend can never block the UI again. Also set URLRequest.timeoutInterval = 10 on /verify-receipt just to be safe.

The checklist I wish I'd had on day one

Most of these are not in the official guideline text verbatim. I learned them by getting rejected. I hope you skip that step.

Try VSkip

Every one of these fixes is shipping in the app today. 7-day free trial, no account.

Download on the App Store iOS 26+ · Built by an indie, no VC, no tracking

Related reading
Groq vs OpenAI Whisper for indies · VSkip press kit + fact sheet · Voice-to-text apps, honest comparison