Flutter-app naar TestFlight zonder Mac (stap voor stap)

  • flutter
  • ios
  • testflight
  • codemagic
  • app store connect
  • handleiding

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

  1. Overzicht en benodigdheden
  2. Beslissingen vooraf
  3. Apple Developer-account
  4. Flutter-project voorbereiden
  5. AdMob voor iOS (alleen als de app advertenties toont)
  6. Codemagic: eerste build zonder signing
  7. App Store Connect: API-key, bundle ID en app-record
  8. Code signing: certificaat en provisioning profile
  9. Codemagic: gesigneerde build naar TestFlight
  10. Automatisch bouwen bij een release-tag
  11. TestFlight: testers toevoegen en installeren
  12. Problemen en oplossingen
  13. Korte checklist

0. Overzicht en benodigdheden

Overzicht van de stappen

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

  1. Op de iPhone: Instellingen → [je naam] → Inloggen en beveiliging → Tweestapsverificatie aan.

2.2 Inschrijven 💶 ⏳

  1. 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/.
  2. Kies Individual, betaal €99.
  3. ⏳ 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

  1. Ga naar App Store Connect → Business.
  2. Free Apps Agreement: wordt actief na het accepteren van de voorwaarden. Status moet Active zijn.
  3. 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).

  1. Business → Compliance → Digital Services Act.
  2. Kies Trader als je inkomsten hebt (ook advertenties). ⚠️ Je naam, adres, telefoon en e-mail worden dan publiek getoond in de App Store.
  3. 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.

  1. Voeg flutter_launcher_icons toe:

    flutter pub add --dev flutter_launcher_icons
  2. Maak flutter_launcher_icons.yaml naast pubspec.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
  3. 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

  1. AdMob → Apps → App toevoegen.
  2. Platform iOS.
  3. ⚠️ 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.)
  4. Naam invullen → App toevoegen → noteer het app-ID ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX.
  5. Advertentieblokken → Advertentieblok toevoegen → Banner → noteer het ID ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX.
  6. 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 GADApplicationIdentifier crasht de app bij het opstarten. Heb je nog geen eigen app-ID, gebruik tijdelijk Google's test-app-ID ca-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.

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

  1. Ga naar https://codemagic.io → Sign up with GitHub.
  2. 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 Podfile in de repo: Flutter maakt die aan tijdens de build op macOS.
  • Paden onder artifacts zijn relatief ten opzichte van working_directory.

5.3 Starten

  1. Commit en push codemagic.yaml.
  2. Codemagic → app → Start new build → workflow iOS unsigned build → branch → Start.
  3. 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)

  1. App Store Connect → Users and Access → Integrations → App Store Connect API → Team Keys. (Eerste keer: Request Access en voorwaarden accepteren.)
  2. Klik + → Name Codemagic → Access App Manager → Generate.
  3. Noteer de Issuer ID (boven de lijst) en de Key ID.
  4. Download API Key → .p8-bestand. ⚠️ Kan maar één keer; meteen in je wachtwoordbeheerder.

6.2 API-key in Codemagic

  1. Codemagic → Settings (Personal Account) of Team settings → Integrations → Developer Portal → Connect / Manage keys.
  2. App Store Connect API key name: bv. Codemagic ← deze naam komt in codemagic.yaml.
  3. 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

  1. Certificates, Identifiers & Profiles → Identifiers → +.
  2. App IDs → App → Continue.
  3. Description: app-naam (geen speciale tekens). Bundle ID: Explicit → je bundle ID.
  4. 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.
  5. Continue → Register.

6.4 App-record aanmaken

  1. App Store Connect → Apps → + → New App.
  2. 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
  3. Create. Noteer het Apple ID van de app (getal, staat bij App Information en in de URL).
  4. ⚠️ 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)

  1. Codemagic → Settings / Team settings → codemagic.yaml settings → Code signing identities → iOS certificates.
  2. Generate certificate → API-key Codemagic → type Apple Distribution.
  3. ⚠️ Bewaar het getoonde wachtwoord van de .p12 in 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)

  1. Certificates, Identifiers & Profiles → Profiles → +.
  2. Onder Distribution: App Store Connect (niet iOS App Development, niet Ad Hoc) → Continue.
  3. App ID: het bundle ID van de app → Continue.
  4. Certificate: het Apple Distribution-certificaat uit 7.1 → Continue.
  5. Name: bv. Mijn App App Store → Generate. Downloaden is niet nodig.

7.3 Profiel ophalen in Codemagic

  1. Codemagic → Code signing identities → iOS provisioning profiles → Fetch profiles.
  2. Vink het profiel aan → Download selected.
  3. 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

  1. Start new build → workflow iOS release to TestFlight → kies een tag (bv. v1.2.0) of branch → Start. Duur: ±5–10 minuten.
  2. 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.

  1. Codemagic → app → Webhooks → Update webhook (of Create webhook). Geef Codemagic toegang tot de repo als GitHub daarom vraagt.
  2. Controleer in GitHub: Settings → Webhooks → URL https://api.codemagic.io/hooks/… met een groen vinkje bij de laatste levering.
  3. 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).
  4. 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_TOKEN niet 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.

  1. Nodig iemand uit: Users and Access → People → + → naam, e-mail, rol Marketing of Customer Support (geen Admin), Access to Apps → alleen deze app.
  2. Na aanvaarding: app → TestFlight → Internal Testing → + (groep, bv. Familie) → testers toevoegen.
  3. Laat Automatic Distribution aan: elke nieuwe build gaat dan meteen naar de groep.
  4. 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.

  1. Vul eerst TestFlight → Test Information in: beschrijving, feedback-e-mail, contactgegevens, privacybeleid-URL.
  2. Het onderdeel External Testing verschijnt pas na de eerste build. Maak dan een groep.
  3. Testers toevoegen per e-mail of via een Public Link (met optioneel maximum).
  4. Build aan de groep toevoegen → ⏳ Beta App Review (eerste build per versie, meestal < 1 dag).
  5. Automatisch vanuit Codemagic: submit_to_testflight: true en beta_groups: [Naam van de groep] onder publishing.app_store_connect.

10.3 Installeren op het toestel

  1. Installeer TestFlight uit de App Store.
  2. Meld aan met het Apple ID dat als tester is uitgenodigd.
  3. Open de uitnodigingsmail of de publieke link → Install.
  4. 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 + .p8 bewaard
  • [ ] 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.