Skip to content
    Field notes

    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.

    4 min read

    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.