I shipped ContentForge's iOS app on the fourth App Store submission. Here is what each rejection was and how I fixed it, because I could not find this information clearly written anywhere when I needed it.
Rejection 1: Missing Info.plist purpose strings (ITMS-90683)
If your Capacitor app uses any plugin that touches a privacy-sensitive iOS API, you need a corresponding NSUsageDescription string in your Info.plist. Apple's CI scanner catches this automatically and rejects the build before it ever reaches a human reviewer.
My app used Capacitor's StatusBar plugin. The purpose string was missing.
The fix: add the required string to ios/App/App/Info.plist. After fixing this the first time, I built a Node script that runs in CI and checks for missing strings automatically. It reads the Capacitor plugins from package.json, maps each one to the privacy APIs it touches, and verifies the corresponding purpose strings exist. That script runs on every push to main.
Rejection 2: Incomplete AppIcon.appiconset
Apple requires specific icon sizes for every device type your app supports. My asset catalog was missing several of the required dimensions.
The fix: generate the full set programmatically from a 1024x1024 source PNG. Make sure the source PNG is flat RGB with no alpha channel and is not upscaled from a smaller source.
Rejection 3: Magic link auth fails in Apple's review sandbox
Apple's review team cannot use magic link authentication. They work in a sandboxed environment where email links do not resolve. If your app's only auth path is magic link, reviewers cannot create an account.
The fix: detect whether you are running as a Capacitor native app using Capacitor.isNativePlatform(), and conditionally hide the magic link UI. On iOS, show only email and password. On web, show magic links as normal.
What passed on submission 4
The fourth submission included all of the above plus: server.startPage set to app.html in capacitor.config.ts, comprehensive touch target fixes at 44x44px minimum, 16px minimum font size on all inputs to prevent iOS zoom-on-focus, and env(safe-area-inset-*) padding on sticky navs for notch and Dynamic Island support.
The CI workflow I wish I had before submission 1
After all of this, the CI pipeline on main now runs npm install and build, the custom Info.plist lint script, TypeScript type check, and Vercel deploy preview. If the Info.plist lint fails, the build fails. The ITMS-90683 rejection path is now closed.
If you are building a Capacitor app and heading toward App Store submission: read the full App Store Review Guidelines before you build the auth flow, test in airplane mode if you have any concern about network-dependent auth flows, and build the CI lint before you need it.
Top comments (0)