Build for iOS and TestFlight
Install the pods, set your signing team and bundle identifier from app_config.json, check the new Apply app_config.json build phase, add the push capability the project does not ship, then archive and upload to TestFlight.
An iOS build needs a Mac, Xcode and CocoaPods, and it needs three things only
you can supply: your Apple team, your bundle identifier, and, if you
use push, the Push Notifications capability. The name under the icon
takes care of itself: a build phase writes it from app_config.json on every
build.
This page assumes the app already runs against your server. If it does not, start with Install and run the app. Every command below runs from the project folder unless it says otherwise.
Before you start
- A Mac with a current Xcode, and CocoaPods
- Membership of the Apple Developer Program as an organisation (see Developer accounts)
- Your Team ID, from the Membership details page of your Apple Developer account
- Your bundle identifier chosen (see Branding), and an App ID registered for it
-
flutter pub gethas run in the project folder
Install the pods
The package ships without ios/Pods/ and without ios/Podfile.lock, so the
pods are resolved on your machine the first time.
flutter pub get
cd ios
pod install
cd ..Order matters. The Podfile reads ios/Flutter/Generated.xcconfig, which
flutter pub get writes. Run pod install first and it stops with:
.../Flutter/Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed firstFrom now on, open ios/Runner.xcworkspace in Xcode, not
Runner.xcodeproj. Only the workspace includes the pods.
"The sandbox is not in sync with the Podfile.lock"
Xcode checks, at the start of every build, that the installed pods match
Podfile.lock. When they do not, the [CP] Check Pods Manifest.lock phase
stops the build with:
error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.It happens when the lock and the installed pods have drifted apart: after you
take a new release of the app, after a plugin was added or updated, or when a
stale Podfile.lock was carried over from an older copy of the project. The
package ships no Podfile.lock, so a lock in your tree is one your machine
wrote, or one you copied in. The fix is always the same: flutter pub get,
then pod install in ios/, then build again.
flutter run, flutter build ios and flutter build ipa run pod install
themselves when the plugins have changed. Building or archiving from Xcode
does not, which is why this message usually appears there.
The project's deployment target is iOS 13.0. The Podfile raises any pod still set below iOS 12.0 up to 12.0, because Xcode 15 and later no longer support older targets, and turns off "treat warnings as errors" for the pods.
Set your signing team
Xcode signs the app with a team, and it cannot sign or archive with a team you are not a member of.
The project in the package carries no team, so you set one before your
first archive. Put your Apple Team ID in app_config.json as iosTeamId
(exactly 10 characters, upper-case letters and digits), then write it into the
Xcode project:
{
"iosTeamId": "AB12CD34EF"
}dart run tool/apply_app_config.dartThe tool writes it into every Runner build configuration (Debug, Release and
Profile) as DEVELOPMENT_TEAM, and says so:
iOS signing team AB12CD34EF (iosTeamId; written into ios/Runner.xcodeproj/project.pbxproj: 3 Runner configurations, was (none))A value that is not a Team ID is refused, and nothing is written. If you would
rather not keep the team in the config, leave iosTeamId out or empty (the
example ships it as ""): nothing is written, and you choose your team in
Xcode. Select the Runner target, open Signing & Capabilities, and pick
it under Team.
Until one of the two is done, the release preflight reports
[ios-team] the Xcode project names no Apple Development Team for the Runner Release configuration.
If you set iosTeamId and later pick a different team in Xcode,
dart run tool/apply_app_config.dart --check exits with an error and the
preflight reports [ios-team-mismatch].
Copies of the project from releases up to 5.3.9 carry the vendor's own team,
52K7QMD7KR, in the project file. If Xcode reports a team you do not
recognise, that is it: replace it with yours by either route above. The
preflight reports it as [ios-team] until you do.
Set your bundle identifier
Apple knows your app by its bundle identifier. It is a build setting that signing and provisioning read before any script runs, so the build cannot change it on the fly. It is written into the Xcode project, once, by a tool.
-
Set
iosBundleIdinassets/config/app_config.json. Letters, digits and hyphens, in two or more dot-separated parts. No underscores.{ "iosBundleId": "com.myexchange.app" } -
Write it into the Xcode project. Run this again every time you change the ID:
dart run tool/apply_app_config.dart -
Register an App ID with exactly the same ID in your Apple Developer account, under Certificates, Identifiers & Profiles.
-
Check Xcode agrees. Open
ios/Runner.xcworkspace, select the Runner target, open Signing & Capabilities, and confirm the Bundle Identifier shown there is yours.
A few rules the tool enforces, so that a mistake stops here rather than in App Store Connect:
- The example file ships
"iosBundleId": "". Empty or absent means "keep whatever the Xcode project has, and do not check it", so a new release never moves an iOS app you already publish. If you prefer to set the ID in Xcode, leave it empty. - The template's own ID is
com.bicryptto.mobile(spelt with two t's). The tool will not write it over an ID of yours that the project already has;--allow-template-idoverrides that, if you truly want it back. - If your project uses different IDs per build configuration (a
.devsuffix on Debug, say), the tool refuses to merge them into one. LeaveiosBundleIdempty and manage those IDs in Xcode. - It does not change an app extension's ID, such as a Notification Service extension you added. It tells you when one still starts with the old ID; change it in Xcode.
To check without writing anything, run
dart run tool/apply_app_config.dart --check. It exits with an error when the
Xcode project disagrees with iosBundleId or iosTeamId.
Changing the bundle identifier after release makes a different App Store app.
It must match the App ID you registered and the provisioning profile you sign
with, and a Firebase GoogleService-Info.plist must be issued for the same ID.
The "Apply app_config.json" build phase
The Runner target has a build phase called Apply app_config.json, listed
after Thin Binary. On every iOS build (flutter run, flutter build ios,
flutter build ipa or an Archive in Xcode) it runs:
"$FLUTTER_ROOT/bin/dart" "$SRCROOT/../tool/ios_app_identity.dart"It does two things:
- Writes
appNameinto the built app asCFBundleDisplayName(the name under the icon) andCFBundleName. It edits the copy ofInfo.plistinside the built app, before Xcode signs it. The sourceios/Runner/Info.pliststill saysBiCrypto, and should: that is the default, and the phase overwrites it in the built copy only. Leave the source file alone. - Checks the bundle identifier. If
iosBundleIdis set and the build is signing a different one, the build stops rather than producing an app under an ID you did not configure.
Because the phase runs the file in tool/, every iOS build needs that folder
in the project. Keep it when you copy or update the project.
What to look for in the build log
The phase is new: releases up to 5.3.9 do not have it, and they put
BiCrypto under the icon whatever appName said. Until you have seen it work
on your own Mac, check its output after your first build. In Xcode, open the
Report navigator, select the build, and find the step for Apply
app_config.json. From the command line, add --verbose to flutter build ios
or flutter build ipa to see the same output. A good run prints one line:
app_config.json: iOS home-screen name "My Exchange" applied; bundle id com.myexchange.app matchesWith iosBundleId left empty, the second half reads
bundle id not checked (iosBundleId not set). Then check the name under the
icon on a device or simulator.
If there is no such step in the log and the icon still says BiCrypto, the
phase is missing from your Xcode project, for example because the project came
from an older release. dart run tool/apply_app_config.dart --check says so in
a warning (the Runner target ... has no "Apply app_config.json" build phase).
Take ios/Runner.xcodeproj from the current package, then set your team and
bundle identifier again as above, and add the Push Notifications capability
again if you use push.
A bundle identifier that disagrees stops the build with a message that offers both ways out, because the phase cannot know which side is right:
error: iosBundleId in assets/config/app_config.json is "com.myexchange.app", but this build (Release) signs Runner as "com.bicryptto.mobile" (PRODUCT_BUNDLE_IDENTIFIER). ...If the config is right, run dart run tool/apply_app_config.dart. If the
project is right (you changed the ID in Xcode), put that ID in iosBundleId,
or empty it.
If app_config.json is missing or empty, the phase does not stop. It prints a
warning, which Xcode lists with the build's issues, and the build carries the
template's values:
warning: assets/config/app_config.json is missing or empty, so this build uses the template assets/config/app_config.example.json. ...You can preview what the phase will do on any machine, Windows and Linux included, without Xcode and without changing anything:
dart tool/ios_app_identity.dart --dry-runPush notifications need a capability the project does not ship
The project has no entitlements file, so it has no push entitlement. Without one, the app still asks for notification permission, the customer still agrees, the build still archives, and no notification is ever delivered. The app does not report it.
If you use push on iOS:
-
Add the capability. In Xcode, select the Runner target, open Signing & Capabilities, press + Capability and add Push Notifications. Xcode creates
Runner.entitlementsand points the build at it. -
Add your Firebase file. Download
GoogleService-Info.plistfrom a Firebase iOS app registered with your bundle identifier, put it inios/Runner/, and add it to the Runner target in Xcode. -
Upload an APNs key to your Firebase project, in its Cloud Messaging settings. iOS delivers every push through Apple's service, whatever Firebase is doing.
-
Configure the server half. See Push notifications.
Once GoogleService-Info.plist exists, the preflight refuses a project without
the entitlement: iOS push cannot work: Firebase is configured but the app has
no aps-environment entitlement. The capability lives in the Xcode project, so
taking a new release's ios/Runner.xcodeproj removes it. Add it again after
every update.
Google sign-in on iOS also needs entries in Info.plist that the project does
not ship. See Sign-in methods.
What the project already declares
| Declaration | Value | What it means for you |
|---|---|---|
ITSAppUsesNonExemptEncryption in Info.plist |
false |
App Store Connect does not ask the export-compliance question on every upload. The value declares that the app uses no non-exempt encryption; its network traffic uses standard HTTPS and TLS. If you add encryption of your own, revisit the answer. |
NSCameraUsageDescription |
Take a photo of your ID document to complete identity verification, or capture evidence for a trade dispute. | The reason iOS shows when the app asks for the camera. |
NSPhotoLibraryUsageDescription |
Attach an existing photo of your ID document for identity verification, or evidence for a trade dispute. | The reason iOS shows when the app asks for the photo library. |
ios/Runner/PrivacyInfo.xcprivacy |
The privacy manifest | Must stay in the Runner target's resources. See Data safety and privacy. |
| Device family | iPhone and iPad | App Store Connect treats it as an iPad app too, and asks for iPad screenshots. Only the futures trading screen has a wide layout; every other screen is the phone layout at tablet width. |
Version and build number
iOS takes both numbers from the same version: line in pubspec.yaml as
Android. 5.3.9+10 becomes version 5.3.9 (CFBundleShortVersionString) and
build 10 (CFBundleVersion). You can override them with
--build-name and --build-number on flutter build ipa.
App Store Connect refuses a build number it has already received for that
version. Raise the build number for every upload. A new source release from us
arrives with our version: line, so check it against your last upload before
you build; see Updating your app.
Archive and upload
Run the preflight first and fix what it prints:
dart run tool/release_preflight.dart --explainThen archive by either route.
flutter build ipa --release
# Archive: build/ios/archive/Runner.xcarchive
# Upload-ready file: build/ios/ipa/*.ipa1. flutter build ios --release (compiles the Dart code and applies the pubspec version)
2. Open ios/Runner.xcworkspace
3. Choose the Runner scheme and "Any iOS Device (arm64)" as the destination
4. Product > Archive
5. In the Organizer, select the archive, press Distribute App,
and choose the App Store Connect uploadflutter build ipa ends with a summary read from the archived app. Its
Display Name should be your appName and its Bundle Identifier your
bundle identifier. Upload its .ipa with Apple's Transporter app,
or open the .xcarchive in Xcode's Organizer and distribute from there.
The project ships no ExportOptions.plist, no fastlane setup and no CI
configuration. The two routes above are the whole process.
The Merchant, AI Investment and Affiliate modules are left out of a normal
build. To include them, add --dart-define=STORE_RESTRICTED_MODULES=true to
flutter build ipa, and read
Restricted modules first: it is a store
decision as much as a technical one.
TestFlight
After an upload, App Store Connect processes the build before it can be tested; it then appears under TestFlight for that app. Internal testers (people on your App Store Connect team) can install it from there. External testers need the build to pass Beta App Review first.
Test on a real iPhone before you submit:
- The name under the icon is your
appName - Sign-in works with a real account on your server
- If you use push: after the first sign-in the app's Stay informed sheet appears, Continue leads to the iPhone's permission prompt, and a test notification arrives
- Profile (the gear icon at the top right of Home) → Open-source Licenses shows the version and build you uploaded
The reviewer will need a working login, because every screen is behind sign-in. See App review access.
More build and run-time failures are on Troubleshooting.