Release A Flutter-built iOS App To TestFlight In 47 Simple Steps
As of: 11/25/2023
These are the steps I took to release my aforementioned Flutter-built app to TestFlight in the Apple App Store. Where I thought it would be helpful for others, I've included notes identifying obstacles I had to get around in order to proceed. If you attempt to follow these steps and something has changed since I published this post, please do reach out (via the link at the bottom of the page) and let me know what you had to do differently, so I can update my instructions accordingly.
First, a couple of preliminary notes. Use Safari for all browser steps. I used Android Studio for my Flutter development but you can use Visual Studio Code, which also has Flutter/Dart extensions. And, most critically, you MUST have a Mac, capable of running XCode, to ship iOS apps.
OK, buckle up!
Enrolled in Apple Developer Program:
- Opened browser, navigated to: https://developer.apple.com/programs/enroll/
-
Pressed "Start Your Enrollment"
NOTE: If you are signed in with an Apple ID already, it will be assumed you want to use that for your Apple Developer Account. If you want to use/create a different account or if you're not signed in with an Apple ID, follow the relevant path Apple will take you through. - Clicked "Continue enrollment on the web"
- On "Confirm your personal information" form, entered my name, address, and phone number and pressed "Continue"
- On "Select your entity type" form, for "I develop apps as" selected "Company/Organization" and pressed "Continue"
-
On "Tell us about your organization" form, entered my company name and D-U-N-S Number, and the CAPTCHA letters, and pressed "Continue"
NOTE: I kept receiving an error stating "The information you entered did not match your D&B profile. Before submitting your information, look up your D&B profile." even after I used the "look up" link Apple provided in the error message to have them email what they thought my D-U-N-S Number should be (it matched what I'd been given prior by D&B themselves!). I submitted a help request, describing the problem in detail and then had an incremental back-and-forth, with various service reps at Apple, until the issue was resolved and I could proceed:- First Apple told me I needed to contact D&B to have them update their records, which could take a couple of days, so I did that.
-
Then I received an email, from Apple support, containing a deeplink which took me back into the account creation flow and prompted me for the following information, which I supplied:
- Company Email Address
- My Work Email Address
- Legal Entity Name
- Legal Entity Address
- Then I received an email, from Apple support, informing me I needed to supply one from a list of documents related to the formation of my company, and containing a deeplink which took me back into the account creation flow and prompted me to upload the requested documentation; I uploaded my LLC's Articles of Organization and latest Periodic Report
- I then received an email, from Apple support, informing me that, in order to proceed with my developer account creation process, I needed to speak with someone by phone. They supplied me with yet another deeplink to submit a request to be called back. When the guy from Apple called, he was very friendly and merely had to have me orally state that I was able to legally represent my company and sign legal documents on its behalf.
- I received a final email, from Apple support, stating that my enrollment request had been accepted and that I needed to review the Program License Agreement, with a deeplink to do so.
- I accepted the Program License Agreement
-
I paid $99 for an annual developer license
NOTE: Though it said I might have to wait up to 48 hours for them to process my credit card payment it went through in about 12. :)
Created Bundle ID:
- Opened browser, navigated to: https://developer.apple.com/account and signed in with Apple Developer Account credentials
- Under Certificates, IDs, & Profiles, clicked "Identifiers" (which routed me to: https://developer.apple.com/account/resources/identifiers/list)
- Pushed "Register an App ID" (which routed me to: https://developer.apple.com/account/resources/identifiers/add/bundleId)
- Selected App IDs, pushed "Continue"
- Under "Select a type", selected "App", pushed "Continue "(which routed me to: https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle)
-
On Register an App ID page:
- Entered Description
- For Bundle ID, selected "Explicit" and entered: [reverse domain].[app name]
- I didn't select any of the Capabilities or App Services as my app uses none of them
- Pressed "Continue"
- Confirmed the details and pressed "Register" to register the Bundle ID
Defined App in App Store Connect:
- With Safari browser, navigated to: https://appstoreconnect.apple.com and signed in with Apple Developer Account credentials
- Accepted TOS
- Clicked "+" in the top-left corner of the page, next to "Apps", and selected "New App"
- Under "Platforms", selected "iOS"
-
Entered app name in "Name" field
NOTE: If the name you want to use is taken (you won't be told this is the case until you click the "Create" button), save yourself some frustration by searching in the App Store to find a suitable alternative rather than guessing-and-checking repeatedly! - For "Primary Language", selected "English (U.S.)"
-
Selected "Bundle ID" created in step #14 (above), via dropdown
NOTE: If nothing is in dropdown you need to return to step #9 (above). - For "SKU", entered an identifier I'll recognize on an invoice
- For "User Access", selected "Full Access"
- Pressed "Create"
Reviewed Xcode Project Settings:
- Opened the default Xcode workspace in the project, in a terminal window, from the Flutter project directory:
$ open ios/Runner.xcworkspace
- Selected the Runner target in the Xcode navigator
-
Verified the most important settings:
- On the General tab:
- In the Supported Destinations section:
Confirmed iPhone, iPad and Mac (Designed for iPad) are present - In the Minimum Deployments section:
Left iOS set to: 11.0
NOTE: If you changed this, you need to open ios/Flutter/AppframeworkInfo.plist in your Flutter app and update the MinimumOSVersion value to match. - In the Identity section:
- Selected App Category
- Entered Display Name
- Set Bundle Identifier to match step #14 (above)
- Set Version: 1.0.0
- Set Build: 1
- In the Deployment Info section:
Confirmed these were set as-follows:- iPhone Orientation: Portrait, Landscape Left, Landscape Right
- Orientation: Portrait, Upside Down, Landscape Left, Landscape Right
- Status Bar Style: Default
- In the Supported Destinations section:
-
On the Signing & Capabilities tab:
- Confirmed "Automatically manage signing" checked
- For Team, selected "Add Account", was prompted to sign in with Apple Id associated with my registered Apple Developer account, which then populated the Team field and allowed me to enter Bundle Identifier matching step #14 (above)
- On the General tab:
Added An App Icon:
- Used https://imageresizer.com/ to resize the .png file, from vector set I purchased, to 1024x1024, per Apple standards
- In the Xcode project navigator, in the Runner folder, selected Assets
-
Double-clicked on default Flutter icon ("AppIcon", selected my custom .png icon (from step #28, above)
NOTE: App icons must be in .png format (per: https://developer.apple.com/design/human-interface-guidelines/app-icons#App-icon-attributes) - Renamed "AppIcon" in Xcode to literal filename of that in step #30 (above), minus the .png suffix
-
Verified the default Flutter icon had been replaced by my custom icon, by re-running the app in Android Studio:
$ flutter run
Added a Launch Screen:
-
In the Xcode project navigator, in the Runner folder, selected LaunchScreen, which displayed View Controller Scene
NOTE: Per https://docs.flutter.dev/platform-integration/ios/launch-screen, this should generally be defined as an empty screen with the background color of your app's first screen. -
Set background color:
- Expanded View Controller Scene >> View Controller and clicked on View
- From Xcode app menu, select View >> Inspectors >> Attributes to open right-side properties pane
-
In Background property, selected "Custom" and then used eye dropper color sampler to pull background color from app running in Flutter iOS Simulator
NOTE: I am CERTAIN there is a simpler way to do this. :)
Registered a (Suitable?) iOS Device For Cert Signing:
- Connected an iPhone to Mac/my development machine via cable, unlocked it, and told it to trust Mac
-
In Xcode, selected the Runner target in the Xcode navigator:
-
On the Signing & Capabilities tab:
In the iOS section, pressed "Register Device" - Confirmed the phone is visible under Certificates, Identifiers & Profiles >> Devices
-
On the Signing & Capabilities tab:
Created an Xcode Build Archive and App Store App Bundle:
-
Confirmed the app's (Flutter) build and version numbers are set as desired in Android Studio:
In pubspec.yaml, found "version" and confirmed it looked good (i.e., was already set to "1.0.0+1") -
Created an app bundle:
$ flutter build ipa --obfuscate --split-debug-info=v1.1.0+1
- When prompted with "codesign wants to access key "Apple Development: [my name] ([company name])" in your keychain.", I entered my MacMini user password and pressed "Always Allow"
-
After process completed with message stating "Xcode archive done.", I confirmed the existence of:
- An Xcode build archive (.xcarchive file) in the project's "build/ios/archive/" directory
- An App Store app bundle (.ipa file) in the project's "build/ios/ipa" directory
Uploaded App Bundle To App Store Connect:
- Installed and opened the Apple Transport macOS app (https://apps.apple.com/us/app/transporter/id1450874784)
- From the Mac Finder, dragged-and-dropped the app bundle (.ipa file), from the project's "build/ios/ipa" directory, into the Apple Transport app.
- Status updated to "Active"
- Pressed "Deliver"
- Watched detailed log which concluded with "UPLOAD SUCCEEDED" message
- In App Store Connect, navigated to the TestFlight tab, confirmed app is now visible under section heading "Version 1.0.0"
Released App On TestFlight:
-
Still on the TestFlight tab, of the app's application details page, on App Store Connect:
-
Under "General Information", clicked on Test Information (showing warning icon w/test explanation that I needed to complete test information to submit a build for external testing)
- Wrote up Beta App Description text
- Defined Feedback Email
- In the Beta App Review Information section, entered my contact information (name, phone number, and email address)
- Pressed "Save"
-
Under "Builds" >> iOS >> Version 1.0.0, Build 1 showed Missing Compliance:
- Clicked on "Manage"
- Prompted for Export Compliance Information
- Selected "None"
- Pressed "Save"
- Observed that Status changed to "Ready To Submit"
-
Clicked on "1" under Build column
On 1.0.0 (1) page, under Test Information:- Wrote up Test Details text
- Clicked "+" next to Individual Testers
- Selected "Add New Testers"
- Entered individual testers' email addresses and names into modal
- Pressed "Next"
- On Test Information modal, unchecked "Sign-in required" and pressed "Next"
- On What to Test modal, left "Automatically notify testers" checked and testing guidance text as-is, and pressed "Submit for Review"
- Observed that Status changed to "Waiting for Review"
-
Under "General Information", clicked on Test Information (showing warning icon w/test explanation that I needed to complete test information to submit a build for external testing)
Whew! Still with me?! Because my testers are not internal, as defined by Apple (meaning they do not have Apple Developer Accounts tied to my business [account]), I needed to set them up as external testers, which necessitates a (minimal) app review by Apple, before testing can commence. So, having completed the steps listed above, I sat back and waited. I had done this during the weekend, so it was not surprising that it took until Monday for approval to come through. I received an email notifying me that my app had been approved at the same time that my testers received individual emails inviting them to install the app via TestFlight.