Flutter-app naar TestFlight zonder Mac (stap voor stap)
Je Flutter-app draait op Android en je wilt hem ook op een iPhone of iPad testen, maar je werkt op Windows en hebt geen Mac. Dat hoeft geen probleem te zijn. In deze handleiding bouw en signeer je de iOS-versie in de cloud met Codemagic, zet je hem via App Store Connect op TestFlight en installeer je hem op je eigen toestel. Allemaal zonder eigen Mac.
Ik schreef deze stappen op tijdens de publicatie van een echte app (oktober 2026), inclusief de valkuilen waar ik zelf in trapte. Vervang de voorbeeldwaarden (Mijn App, com.voorbeeld.mijnApp, src/mijn_app, …) door die van je eigen project.
Read this article in English: How to publish a Flutter app to TestFlight without a Mac.
Ook de Android-versie publiceren? Lees dan Flutter-app publiceren in Google Play met GitHub Actions.
Legenda: 💶 = kost geld · ⏳ = wachttijd · ⚠️ = valkuil die we echt tegenkwamen
Inhoud
- Overzicht en benodigdheden
- Beslissingen vooraf
- Apple Developer-account
- Flutter-project voorbereiden
- AdMob voor iOS (alleen als de app advertenties toont)
- Codemagic: eerste build zonder signing
- App Store Connect: API-key, bundle ID en app-record
- Code signing: certificaat en provisioning profile
- Codemagic: gesigneerde build naar TestFlight
- Automatisch bouwen bij een release-tag
- TestFlight: testers toevoegen en installeren
- Problemen en oplossingen
- Korte checklist
0. Overzicht en benodigdheden
| Wat | Waarom | Kost |
|---|---|---|
| Apple ID met tweestapsverificatie | Nodig voor het Developer-account | gratis |
| Apple Developer Program | Signeren, TestFlight, App Store | 💶 €99/jaar |
| Codemagic-account | macOS-buildmachine in de cloud | gratis tier (500 min/maand) |
| GitHub-repo met het Flutter-project | Bron voor Codemagic | gratis |
| iPhone of iPad | Testen via de TestFlight-app | — |
| Publieke privacybeleid-URL | Verplicht in App Store Connect en in de app | — |
Totale doorlooptijd de eerste keer: ±2–4 dagen, vooral ⏳ wachten op Apple (activatie account, DSA-verificatie). Het eigenlijke werk is een halve dag.
1. Beslissingen vooraf
Leg deze vast vóór je begint; sommige zijn later niet meer te wijzigen.
| Beslissing | Voorbeeld | Opmerking |
|---|---|---|
| Bundle ID | com.voorbeeld.mijnApp | ⚠️ Geen underscores op iOS (Android mag wel). Na de eerste upload niet meer te wijzigen. Gebruik je domein omgekeerd. |
| App-naam (App Store) | Mijn App | Max. 30 tekens, moet uniek zijn in de App Store. |
| Account-type | Individual | Organization vraagt een D-U-N-S-nummer en duurt langer. |
| Toestellen | iPhone + iPad | iPad betekent extra screenshots en layoutcontrole. |
| Oriëntatie | iPhone alleen portrait, iPad alles | ⚠️ iPad vereist alle 4 oriëntaties óf UIRequiresFullScreen. |
| SKU | mijnapp-ios | Interne code, alleen voor jou zichtbaar. |
| Privacybeleid-URL | https://…/privacy/ | Ook als link in de app zetten (Apple-richtlijn 5.1.1). |
2. Apple Developer-account
2.1 Apple ID klaarmaken
- Op de iPhone: Instellingen → [je naam] → Inloggen en beveiliging → Tweestapsverificatie aan.
2.2 Inschrijven 💶 ⏳
- Installeer de Apple Developer-app op de iPhone (makkelijkste route: betaling via de App Store, identiteitscontrole met je ID-kaart). Alternatief: https://developer.apple.com/programs/enroll/.
- Kies Individual, betaal €99.
- ⏳ Wacht op de bevestigingsmail ("Thank you for joining the Apple Developer Program"), meestal 24–48 uur.
Tip: stappen 3 en 5 (project voorbereiden, unsigned build) kunnen al tijdens het wachten.
2.3 Overeenkomsten in App Store Connect
- Ga naar App Store Connect → Business.
- Free Apps Agreement: wordt actief na het accepteren van de voorwaarden. Status moet Active zijn.
- Paid Apps Agreement: alleen nodig voor betaalde apps of in-app-aankopen. Advertentie-inkomsten lopen via AdMob, dus voor een gratis app met ads mag dit op New blijven staan.
2.4 EU Digital Services Act (DSA) ⏳
Verplicht om in de EU te publiceren (TestFlight voor interne testers werkt al zonder).
- Business → Compliance → Digital Services Act.
- Kies Trader als je inkomsten hebt (ook advertenties). ⚠️ Je naam, adres, telefoon en e-mail worden dan publiek getoond in de App Store.
- Apple vraagt twee documenten (PDF/JPEG/PNG, max. 10 MB):
| Document | Wat werkt (België) | Waar |
|---|---|---|
| Name Identification Document (bedrijfs- of gerechtelijk document met je naam) | Uittreksel uit de KBO / attest van inschrijving | KBO Public Search of je ondernemingsloket |
| Address Identification Document | KBO-uittreksel (als het adres klopt), attest van woonst, recente energie-/internetfactuur, bankafschrift | Attest van woonst: Mijn Burgerprofiel of Mijn Dossier |
- ⚠️ Naam en adres moeten letterlijk overeenkomen met wat je bij Apple invulde.
- Bij een factuur of bankafschrift mag je bedragen onleesbaar maken; naam, adres en datum niet.
- Status wordt In Review; ⏳ enkele dagen. Je kunt intussen verder.
3. Flutter-project voorbereiden
Alles in de map ios/ van het Flutter-project (in een monorepo bv. src/<app>/ios/). Dit kan volledig op Windows.
3.1 Bundle ID en minimale iOS-versie
In ios/Runner.xcodeproj/project.pbxproj (zoek en vervang alles):
PRODUCT_BUNDLE_IDENTIFIER = com.example.mijnApp; → com.voorbeeld.mijnApp;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mijnApp.RunnerTests; → com.voorbeeld.mijnApp.RunnerTests;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; → 13.0 Staat er in ios/Flutter/AppFrameworkInfo.plist een MinimumOSVersion, zet die ook op 13.0.
3.2 ios/Runner/Info.plist
Pas aan of voeg toe binnen <dict>:
<!-- Naam onder het icoon op het beginscherm -->
<key>CFBundleDisplayName</key>
<string>Mijn App</string>
<!-- Korte interne naam, max. 15 tekens -->
<key>CFBundleName</key>
<string>Mijn App</string>
<!-- Geen eigen encryptie: Apple vraagt dan niets bij elke upload -->
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<!-- iPhone: alleen portrait -->
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<!-- iPad: alle vier (vereist voor split view), laat de standaardlijst staan --> Toont de app advertenties, zie ook stap 4 voor GADApplicationIdentifier, SKAdNetworkItems en NSUserTrackingUsageDescription.
3.3 App-iconen
⚠️ De App Store weigert iconen met transparantie. Gebruik een 1024×1024 PNG zonder alfakanaal.
-
Voeg flutter_launcher_icons toe:
flutter pub add --dev flutter_launcher_icons -
Maak
flutter_launcher_icons.yamlnaastpubspec.yaml:flutter_launcher_icons: android: false # Android-iconen bestaan al ios: true image_path: "assets/icon/app_icon.png" # of het bestaande Android-icoon remove_alpha_ios: true -
Genereer:
dart run flutter_launcher_icons
3.4 Splashscreen
Gebruik je flutter_native_splash:
dart run flutter_native_splash:create ⚠️ Dit genereert ook Android- en web-bestanden opnieuw. Wil je alleen iOS bijwerken, zet die wijzigingen terug (git checkout -- android web) of zet android: false / web: false in de config.
3.5 In de app zelf
-
Link naar het privacybeleid (bv. in het scherm "Over" of de instellingen). Apple vereist dit.
-
Brede schermen (iPad): beperk de inhoud tot een leesbare breedte, bv.:
body: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 700), child: page, ), ),
3.6 Controleren
dart format --set-exit-if-changed lib test
flutter analyze
flutter test 4. AdMob voor iOS (alleen als de app advertenties toont)
4.1 App aanmaken in AdMob
- AdMob → Apps → App toevoegen.
- Platform iOS.
- ⚠️ Vraag "Staat de app in een ondersteunde app store?" → Nee. (Kies je Ja, dan moet je de app in de App Store zoeken, en die bestaat nog niet.)
- Naam invullen → App toevoegen → noteer het app-ID
ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX. - Advertentieblokken → Advertentieblok toevoegen → Banner → noteer het ID
ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX. - Na publicatie in de App Store: App-instellingen → App-store-details toevoegen om te koppelen. Tot dan toont AdMob weinig of geen echte advertenties (normaal).
4.2 Privacy & messaging
In AdMob → Privacy & messaging, voor de iOS-app:
- GDPR-bericht (EER/VK/CH) aanmaken en publiceren.
- IDFA-uitlegbericht aanmaken (verschijnt vóór de ATT-popup van Apple).
4.3 Info.plist
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX</string>
<!-- Tekst in de ATT-popup ("App Tracking Transparency") -->
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>
<!-- Minimaal Google's eigen netwerk-ID -->
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string>
</dict>
</array> - ⚠️ Zonder
GADApplicationIdentifiercrasht de app bij het opstarten. Heb je nog geen eigen app-ID, gebruik tijdelijk Google's test-app-IDca-app-pub-3940256099942544~1458002511. - De volledige SKAdNetwork-lijst (externe kopers) staat in de Google Mobile Ads iOS-documentatie.
4.4 Test-ID's in debug
Gebruik in debug- en profile-builds altijd Google's test-banner-ID's; echte ID's alleen in release:
static String get bannerAdUnitId {
if (kReleaseMode) {
return Platform.isAndroid ? _androidBannerAdUnitId : _iosBannerAdUnitId;
}
return Platform.isAndroid
? 'ca-app-pub-3940256099942544/6300978111' // Android test-banner
: 'ca-app-pub-3940256099942544/2934735716'; // iOS test-banner
} ⚠️ TestFlight installeert een release-build → echte advertenties. Nooit op je eigen ads klikken.
4.5 Consent (GDPR) en ATT
Gebruik de UMP SDK (zit in google_mobile_ads): vraag consent na de eerste frame en start Mobile Ads pas als ConsentInformation.instance.canRequestAds() true is. Met een IDFA-bericht in AdMob toont UMP daarna zelf de ATT-popup; extra code is niet nodig.
5. Codemagic: eerste build zonder signing
Bewijst dat de app compileert voor iOS. Kan al vóór het Developer-account actief is.
5.1 Account en repo
- Ga naar https://codemagic.io → Sign up with GitHub.
- Add application → kies de GitHub-repo → projecttype Flutter App (via codemagic.yaml).
5.2 codemagic.yaml in de root van de repo
workflows:
ios-unsigned:
name: iOS unsigned build
instance_type: mac_mini_m2
max_build_duration: 60
working_directory: src/mijn_app # weglaten als het Flutter-project in de root staat
environment:
flutter: 3.47.3 # zelfde versie als lokaal/CI
xcode: latest
cocoapods: default
cache:
cache_paths:
- $HOME/.pub-cache
- $HOME/Library/Caches/CocoaPods
scripts:
- name: Get packages
script: flutter pub get
- name: Run tests
script: flutter test
- name: Build iOS (no code signing)
script: flutter build ios --release --no-codesign
artifacts:
- build/ios/iphoneos/*.app # relatief t.o.v. working_directory
- /tmp/xcodebuild_logs/*.log - Er hoeft geen
Podfilein de repo: Flutter maakt die aan tijdens de build op macOS. - Paden onder
artifactszijn relatief ten opzichte vanworking_directory.
5.3 Starten
- Commit en push
codemagic.yaml. - Codemagic → app → Start new build → workflow iOS unsigned build → branch → Start.
- Resultaat: alle stappen groen en een artifact
Runner.app.zip. Dit bestand kun je niet installeren (niet gesigneerd); het bewijst alleen dat de build lukt.
6. App Store Connect: API-key, bundle ID en app-record
Vanaf hier is een actief Developer-account nodig.
6.1 App Store Connect API-key (één keer per account, herbruikbaar voor alle apps)
- App Store Connect → Users and Access → Integrations → App Store Connect API → Team Keys. (Eerste keer: Request Access en voorwaarden accepteren.)
- Klik + → Name
Codemagic→ Access App Manager → Generate. - Noteer de Issuer ID (boven de lijst) en de Key ID.
- Download API Key →
.p8-bestand. ⚠️ Kan maar één keer; meteen in je wachtwoordbeheerder.
6.2 API-key in Codemagic
- Codemagic → Settings (Personal Account) of Team settings → Integrations → Developer Portal → Connect / Manage keys.
- App Store Connect API key name: bv.
Codemagic← deze naam komt incodemagic.yaml. - Issuer ID, Key ID en het
.p8-bestand invullen → Save. Groen bolletje = werkt.
De Issuer ID en Key ID zijn geen geheimen. Het
.p8-bestand wel: nooit in git of een chat.
6.3 Bundle ID registreren
- Certificates, Identifiers & Profiles → Identifiers → +.
- App IDs → App → Continue.
- Description: app-naam (geen speciale tekens). Bundle ID: Explicit → je bundle ID.
- Capabilities: niets aanvinken, tenzij de app ze echt gebruikt (push, Sign in with Apple, …). AdMob en ATT hebben geen capability nodig. Later toevoegen kan altijd.
- Continue → Register.
6.4 App-record aanmaken
- App Store Connect → Apps → + → New App.
- Invullen:
- Platforms: iOS
- Name: de app-naam (⚠️ moet uniek zijn in de App Store)
- Primary Language: bv. English (U.S.)
- Bundle ID: kies het geregistreerde ID uit de lijst
- SKU: bv.
mijnapp-ios - User Access: Full Access
- Create. Noteer het Apple ID van de app (getal, staat bij App Information en in de URL).
- ⚠️ Klik niet op Add for Review; dat is voor de App Store-indiening, niet voor TestFlight.
7. Code signing: certificaat en provisioning profile
Hier liepen we het langst vast. Codemagic maakt wel een certificaat aan, maar geen provisioning profile. Volg exact deze volgorde.
7.1 Distributiecertificaat (één keer per account, herbruikbaar)
- Codemagic → Settings / Team settings → codemagic.yaml settings → Code signing identities → iOS certificates.
- Generate certificate → API-key
Codemagic→ type Apple Distribution. - ⚠️ Bewaar het getoonde wachtwoord van de
.p12in je wachtwoordbeheerder.
Apple staat maar een paar distributiecertificaten per account toe. Hergebruik dit certificaat voor al je apps; maak niet per app een nieuw.
7.2 Provisioning profile (één per app)
- Certificates, Identifiers & Profiles → Profiles → +.
- Onder Distribution: App Store Connect (niet iOS App Development, niet Ad Hoc) → Continue.
- App ID: het bundle ID van de app → Continue.
- Certificate: het Apple Distribution-certificaat uit 7.1 → Continue.
- Name: bv.
Mijn App App Store→ Generate. Downloaden is niet nodig.
7.3 Profiel ophalen in Codemagic
- Codemagic → Code signing identities → iOS provisioning profiles → Fetch profiles.
- Vink het profiel aan → Download selected.
- Controleer in de lijst: type App Store, juiste bundle ID, en een vinkje bij het certificaat.
⚠️ "No profiles to add" bij Fetch betekent: het profiel bestaat nog niet bij Apple → doe 7.2.
8. Codemagic: gesigneerde build naar TestFlight
8.1 Workflow toevoegen aan codemagic.yaml
ios-release:
name: iOS release to TestFlight
instance_type: mac_mini_m2
max_build_duration: 60
working_directory: src/mijn_app
integrations:
app_store_connect: Codemagic # naam uit stap 6.2
environment:
flutter: 3.47.3
xcode: latest
cocoapods: default
ios_signing:
distribution_type: app_store
bundle_identifier: com.voorbeeld.mijnApp
triggering: # zie stap 9
events:
- tag
tag_patterns:
- pattern: "v*"
include: true
cache:
cache_paths:
- $HOME/.pub-cache
- $HOME/Library/Caches/CocoaPods
scripts:
- name: Apply code signing settings to the Xcode project
script: xcode-project use-profiles
- name: Get packages
script: flutter pub get
- name: Run tests
script: flutter test
- name: Build signed IPA
script: flutter build ipa --release --export-options-plist=/Users/builder/export_options.plist
artifacts:
- build/ios/ipa/*.ipa
- /tmp/xcodebuild_logs/*.log
publishing:
app_store_connect:
auth: integration
submit_to_testflight: false # true = ook naar externe testers (met Beta App Review) 8.2 Versie en buildnummer
- Komen uit
pubspec.yaml:version: 1.2.0+12→ versie 1.2.0, buildnummer 12. - ⚠️ Apple weigert een upload met een buildnummer dat al bestaat voor die versie. Verhoog het buildnummer voor elke nieuwe upload, met de hand of automatisch met een release-tool zoals release-please.
- Een mislukte build die niets uploadde, kun je opnieuw starten met hetzelfde buildnummer.
8.3 Starten en volgen
- Start new build → workflow iOS release to TestFlight → kies een tag (bv.
v1.2.0) of branch → Start. Duur: ±5–10 minuten. - Na Publishing: App Store Connect → TestFlight → Builds. Status Processing → ⏳ 15–30 minuten → Ready to Test. Je krijgt een mail van Apple.
9. Automatisch bouwen bij een release-tag
De triggering-sectie werkt alleen als Codemagic een webhook in de GitHub-repo heeft.
- Codemagic → app → Webhooks → Update webhook (of Create webhook). Geef Codemagic toegang tot de repo als GitHub daarom vraagt.
- Controleer in GitHub: Settings → Webhooks → URL
https://api.codemagic.io/hooks/…met een groen vinkje bij de laatste levering. - Bij de volgende tag
v*start de build vanzelf. ⚠️ Een tag die al bestond vóór de webhook start niets; bouw die met de hand (stap 8.3). - Gebruik je release-please: de tag wordt via de GitHub-API gemaakt. Webhooks naar Codemagic gaan dan wel door (alleen GitHub Actions-workflows worden door
GITHUB_TOKENniet getriggerd). Controleer het bij de eerste release.
10. TestFlight: testers toevoegen en installeren
10.1 Interne testers (max. 100, geen review)
Moeten gebruiker in je App Store Connect-team zijn.
- Nodig iemand uit: Users and Access → People → + → naam, e-mail, rol Marketing of Customer Support (geen Admin), Access to Apps → alleen deze app.
- Na aanvaarding: app → TestFlight → Internal Testing → + (groep, bv.
Familie) → testers toevoegen. - Laat Automatic Distribution aan: elke nieuwe build gaat dan meteen naar de groep.
- Dit kan al vóór de eerste build.
10.2 Externe testers (max. 10.000, met Beta App Review)
Geen toegang tot je account nodig; ideaal voor vrienden en familie.
- Vul eerst TestFlight → Test Information in: beschrijving, feedback-e-mail, contactgegevens, privacybeleid-URL.
- Het onderdeel External Testing verschijnt pas na de eerste build. Maak dan een groep.
- Testers toevoegen per e-mail of via een Public Link (met optioneel maximum).
- Build aan de groep toevoegen → ⏳ Beta App Review (eerste build per versie, meestal < 1 dag).
- Automatisch vanuit Codemagic:
submit_to_testflight: trueenbeta_groups: [Naam van de groep]onderpublishing.app_store_connect.
10.3 Installeren op het toestel
- Installeer TestFlight uit de App Store.
- Meld aan met het Apple ID dat als tester is uitgenodigd.
- Open de uitnodigingsmail of de publieke link → Install.
- Een TestFlight-build blijft 90 dagen bruikbaar.
10.4 Testchecklist
- [ ] Icoon en splashscreen
- [ ] Eerste start (onboarding), daarna niet meer
- [ ] Kernfunctionaliteit (zelfde resultaten als Android)
- [ ] Instellingen blijven bewaard na herstart
- [ ] GDPR-consent, daarna de ATT-popup
- [ ] Advertenties (of geen, zolang AdMob niet gekoppeld is)
- [ ] Notch / home-indicator / safe area
- [ ] Licht en donker thema
- [ ] Alle links (mail, website, privacybeleid)
- [ ] iPad: layout op breed scherm, draaien, split view
11. Problemen en oplossingen
| Melding / symptoom | Oorzaak | Oplossing |
|---|---|---|
| AdMob: je kunt de iOS-app alleen in de App Store zoeken | Verkeerde keuze bij "staat de app in een store?" | Terug → kies Nee |
| App crasht meteen bij opstarten | GADApplicationIdentifier ontbreekt in Info.plist | Toevoegen (test-ID mag tijdelijk) |
No matching profiles found for bundle identifier "…" and distribution type "app_store" | Geen App Store-profiel in Codemagic | Stap 7.2 en 7.3 |
| Fetch profiles: No profiles to add | Profiel bestaat nog niet bij Apple | Stap 7.2 |
| Profiel gevonden, maar certificaat ontbreekt | Profiel gekoppeld aan een ander certificaat | Profiel bij Apple bewerken → certificaat uit 7.1 kiezen → opnieuw fetchen |
| Tag gemaakt, maar geen build in Codemagic | Geen webhook | Stap 9; bestaande tag met de hand bouwen |
| Upload geweigerd: build already exists | Buildnummer al gebruikt | Buildnummer in pubspec.yaml verhogen |
| App Store Connect: icoon geweigerd | Transparantie in het icoon | remove_alpha_ios: true en opnieuw genereren |
| DSA vraagt een "Name Identification Document" | Verificatie als trader | KBO-uittreksel (stap 2.4) |
| Geen advertenties in TestFlight | AdMob-app nog niet gekoppeld aan de App Store | Normaal; koppelen na publicatie (stap 4.1) |
12. Korte checklist
Eenmalig per Apple-account (bij een volgende app overslaan):
- [ ] Apple ID met tweestapsverificatie
- [ ] Apple Developer Program 💶 ⏳
- [ ] Free Apps Agreement actief
- [ ] DSA-handelaarsstatus ⏳
- [ ] App Store Connect API-key +
.p8bewaard - [ ] API-key in Codemagic
- [ ] Apple Distribution-certificaat in Codemagic
Per app:
- [ ] Bundle ID, naam, SKU, privacybeleid-URL gekozen
- [ ]
project.pbxproj: bundle ID + iOS 13.0 - [ ]
Info.plist: namen, encryptie, oriëntatie (+ AdMob-sleutels) - [ ] Iconen zonder transparantie, splash
- [ ] Privacylink in de app
- [ ] (AdMob) iOS-app, banner-ID, GDPR- en IDFA-bericht
- [ ]
codemagic.yaml:ios-unsigned→ groene build - [ ] Bundle ID geregistreerd bij Apple
- [ ] App-record in App Store Connect
- [ ] App Store-provisioning profile bij Apple + opgehaald in Codemagic
- [ ]
codemagic.yaml:ios-release→ groene build - [ ] Webhook in Codemagic
- [ ] TestFlight-groep + testers → installeren en testen
Vragen of liep je ergens anders vast? Laat het me gerust weten, dan vul ik deze handleiding aan.