The Flutter App Store submission checklist nobody writes down
A working list of everything that has rejected a Flutter build, in the order you should deal with it — with the exact guideline numbers reviewers cite.
flutter build ipa succeeding is not the milestone it feels like. The gap between a build that
compiles and a build that passes review is a few hours of tedious, entirely predictable work, and
almost none of it is Flutter-specific — which is exactly why Flutter developers keep getting caught
by it.
This is the list, in the order that costs the least time.
Before you build
Bundle identifier and display name are yours. com.example.myapp will be rejected, and the
default from flutter create is still in ios/Runner.xcodeproj more often than you would think.
Check CFBundleDisplayName too — it is what appears under the icon, and it is not automatically
your app's name.
Every permission has a purpose string. Every NS*UsageDescription in Info.plist needs a
sentence explaining why your app specifically needs it. "This app uses the camera" is rejected
under Guideline 5.1.1. "Take a photo to attach to a report" is not.
The subtle version of this: plugins add permissions you did not ask for. image_picker pulls in
photo library access whether you use that path or not. Build the archive, open the generated
Info.plist, and read what is actually in it rather than what you put there.
App Tracking Transparency, if you have any ad SDK at all. If the binary contains an SDK that
touches the IDFA, you need NSUserTrackingUsageDescription and an ATT prompt. This includes
analytics SDKs you configured not to track. Reviewers check the binary, not your configuration.
Encryption declaration. Add ITSAppUsesNonExemptEncryption to Info.plist — almost always
false unless you implement your own crypto. Without it, every single upload stops and waits for
you to answer the question in App Store Connect by hand.
Icons and assets
No transparency, no rounded corners, no alpha channel. iOS applies the mask itself. A 1024×1024 icon with an alpha channel is rejected automatically, before a human sees it.
Every size present. flutter_launcher_icons handles this; verify the output anyway. A missing
size in the asset catalog produces a validation failure at upload with a message that does not name
the missing size.
Launch screen is not a splash screen. Apple expects the launch storyboard to resemble the first screen of the app, not a branded logo card. This is Guideline 4.0 territory and enforcement is inconsistent — but a launch screen that matches your first screen also removes the flash of mismatched colour that Flutter apps are known for, so do it regardless.
The rejections that are actually about your product
The plist work above is mechanical. These are the ones that cost days.
Sign in with Apple — Guideline 4.8. If you offer any third-party login (Google, Facebook, anything), you must also offer Sign in with Apple. Not optional, not negotiable, and it is a real implementation with a real backend component. Plan for it before you build auth, not after.
Account deletion — Guideline 5.1.1(v). If users can create an account in your app, they must be able to delete it in your app. Not by emailing you. Not through a web form. An in-app path that actually deletes the data.
Restore purchases — Guideline 3.1.1. Covered in the subscriptions post, but it belongs on this list too, because it is the single most common monetisation rejection.
A demo account that works. If anything is behind a login, App Review needs credentials in the review notes. They will not sign up. The account must have data in it and must not expire — a demo account that hits a rate limit or a trial expiry is a rejection with a confusing explanation.
Privacy nutrition labels that match reality. You declare what you collect in App Store Connect. If your app sends analytics events with a user id and your label says you collect nothing, that is a rejection — and increasingly an automated one.
Build and upload
flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
Check the build number, not just the version. Every upload needs a unique CFBundleVersion,
even for the same version string. Rejected builds burn build numbers too.
Strip debug code. print statements go to the device console in release builds. So do any debug
menus you added — including the entitlement toggle from the subscriptions post. Gate them behind
kDebugMode and confirm the gate works in a release build, not just in your head.
Test on a real device in release mode. flutter run --release on a physical phone. Debug-mode
Flutter is a different runtime with different performance and different behaviour around
animations and jank. Things that only appear in release: missing tree-shaken icons, assets you
forgot to declare in pubspec.yaml, and anything depending on kDebugMode.
After you submit
Answer the reviewer in the Resolution Center rather than resubmitting silently. A build resubmitted without explanation goes back to the end of the queue with the same reviewer and the same objection. A reply that explains what changed usually gets looked at the same day.
And expect the first submission to be rejected. Budget for it, submit early in your release week, and treat it as normal — because it is. The list above is what turns three rounds into one.