leafittome/docs/firebase-einrichtung.md
cschlaefke 9aa49c641d iOS/TestFlight: App-Check-Release-Provider, App-Icon, Datenschutz-Hosting
- main.dart: App-Check-Provider per kReleaseMode umgeschaltet (Release =
  PlayIntegrity/AppAttest, Debug = Debug-Provider). Nötig, da die Functions
  App Check erzwingen — ein Release-Build mit Debug-Provider würde auf
  fremden Geräten bei jedem Function-Aufruf scheitern.
- App-Icon aus assets/icon/icon.png via flutter_launcher_icons generiert
  (iOS-Set + Android-Mipmaps).
- docs/legal/datenschutz.html: Platzhalter ausgefüllt, interner Hinweis raus.
- firebase.json: hosting-Block (public: docs/legal, Redirect / -> /datenschutz).
  URL: https://leaf-it-to-me-app.web.app/datenschutz
- Doku: firebase-einrichtung.md Schritt 9/10 + Hosting fortgeschrieben,
  handoff.md aktualisiert (iOS/TestFlight abgeschlossen, nur noch Android offen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMRhL16hB14xcfef1qez7N
2026-09-08 20:37:49 +02:00

30 KiB
Raw Permalink Blame History

Firebase-Einrichtung — Schritt für Schritt

Firebase ist Googles „Backend als Dienst": Es liefert uns Login (Auth), Datenbank mit Echtzeit-Sync (Firestore), Foto-Speicher (Storage), serverseitigen Code (Cloud Functions) und Push-Nachrichten (FCM) — ohne dass wir einen eigenen Server betreiben.

Stand: Schritt 13 erledigt, Blaze aktiv (18.07.2026). Der Code für Firestore (Pflanzen, Stellplätze, Haushalte) und Login (E-Mail+Passwort) ist eingebaut — offen ist Schritt 4 (Firestore-Datenbank anlegen + E-Mail-Login aktivieren, machst du in der Console) und danach das Rules-Deployment. Dieses Dokument wird mit jedem Baustein erweitert.

Was wofür? (Überblick)

Baustein Aufgabe in LeafItToMe
Authentication Login mit E-Mail+Passwort, Google und Apple
Cloud Firestore Datenbank: Haushalte, Pflanzen, Stellplätze, Bestätigungen — synchronisiert in Echtzeit auf alle Geräte des Haushalts
Storage Speichert die Pflanzen- und Stellplatz-Fotos
Cloud Functions Unser Server-Code: ruft PlantNet/Claude auf (damit API-Keys nie in der App stecken) und verschickt die täglichen Erinnerungen
Cloud Messaging (FCM) Stellt die Push-Nachrichten auf dem Sperrbildschirm zu

Schritt 1: Firebase-Projekt anlegen (machst du, ~10 Minuten)

  1. Öffne https://console.firebase.google.com und melde dich mit deinem Google-Konto an.
  2. „Projekt hinzufügen" → Name: leafittome → Google Analytics kannst du deaktivieren (brauchen wir nicht).
  3. Nach dem Anlegen bist du in der Projekt-Übersicht. Fertig für heute — Apps registrieren wir gemeinsam per CLI (Schritt 3).

Schritt 2: Auf den Blaze-Tarif umstellen (machst du, ~5 Minuten)

Cloud Functions (unser Server-Code) setzen den Blaze-Tarif („Pay as you go") voraus. Keine Sorge vor Kosten:

  • Die großzügigen Gratis-Kontingente bleiben auch im Blaze-Tarif bestehen (z. B. 2 Mio. Function-Aufrufe/Monat, 50.000 Firestore-Lesezugriffe/Tag). Bei ~5 Nutzern bleiben wir weit darunter → real ~0 €/Monat.
  • In der Console: Zahnrad → Nutzung und AbrechnungTarif ändern → Blaze → Kreditkarte hinterlegen.

Kostenschutz einrichten (wichtig, machen wir direkt mit):

  1. In der Google Cloud Console (https://console.cloud.google.com, gleiches Projekt) → Abrechnung → Budgets und Benachrichtigungen.
  2. Budget anlegen: z. B. 5 €/Monat, E-Mail-Alarm bei 50 %, 90 % und 100 %.
  3. So bekommst du eine Mail, lange bevor irgendetwas teuer wird.

Schritt 3: App mit Firebase verbinden (machen wir gemeinsam per CLI)

Wenn Schritt 12 erledigt sind, läuft das so (führe ich mit dir aus, hier zur Doku):

# Firebase-CLI installieren und einloggen (öffnet den Browser)
npm install -g firebase-tools
firebase login

# FlutterFire-CLI installieren
dart pub global activate flutterfire_cli

# Verbindet das Flutter-Projekt mit dem Firebase-Projekt:
# registriert die iOS- und Android-App und erzeugt lib/firebase_options.dart
flutterfire configure

flutterfire configure erzeugt lib/firebase_options.dart — darin stehen die Projekt-Kennungen (keine Geheimnisse, darf ins Git). Die eigentlichen API-Keys (Anthropic, PlantNet) leben nur in der Cloud-Functions-Umgebung, niemals in der App.

Was dabei konkret passiert ist (zum Nachvollziehen):

  • In der Firebase Console tauchen unter Projekteinstellungen → Meine Apps jetzt zwei Apps auf (iOS und Android, jeweils dev.leafittome.app).
  • android/app/google-services.json und ios/Runner/GoogleService-Info.plist sind die plattformspezifischen Verbindungsdateien; firebase.json merkt sich die Zuordnung fürs CLI.
  • In den Android-Gradle-Dateien wurde das google-services-Plugin eingetragen (verarbeitet die JSON-Datei beim Build).
  • lib/main.dart ruft beim Start Firebase.initializeApp(...) auf — ab jetzt können wir nach und nach Auth, Firestore, Storage und FCM andocken.

Schritt 4: Firestore und Login aktivieren (machst du, ~5 Minuten)

Der App-Code für Datenbank und Login ist fertig — zwei Schalter musst du in der Console einmalig umlegen:

  1. Firestore-Datenbank anlegen: Firebase Console → Build → Firestore Database„Datenbank erstellen" → Standort eur3 (Europa) wählen → Produktionsmodus (die strengen Startregeln werden gleich durch unsere eigenen ersetzt). Das aktiviert auch die Firestore-API des Projekts.
  2. E-Mail/Passwort-Login aktivieren: Firebase Console → Build → Authentication„Jetzt starten" → Tab Sign-in methodE-Mail/Passwort aktivieren. (Google- und Apple-Login rüsten wir in einem späteren Block nach — Apple braucht den Developer Account.)

Danach werden die Sicherheitsregeln aus dem Repo deployt (macht Claude per CLI):

firebase deploy --only firestore --project leaf-it-to-me-app

Das Datenmodell und die Sicherheitsregeln (zum Verständnis)

users/{uid}                      → E-Mail, householdId (= aktiver Haushalt), fcmTokens, reminderTime
                                   (nur der Nutzer selbst)
households/{id}                  → Name, ownerUid (Besitzer), memberUids,
                                   members {uid → Rolle+E-Mail}
households/{id}/plants/{id}      → Pflanze: Art, Intervalle, lastWatered(+By), ...
households/{id}/locations/{id}   → Stellplatz: Name
invites/{code}                   → Einladung: householdId, Rolle; nur Erstellen erlaubt,
                                   Einlösen ausschließlich über die Function joinHousehold

Rollen (V2): member = volle Rechte, sitter = Pflanzen-Sitter (sieht alles, darf aber nur Aufgaben bestätigen — die Rules erlauben Sittern an Pflanzen ausschließlich Änderungen der Bestätigungs-Felder). Der Beitritt per Einladungscode läuft über die Cloud Function joinHousehold, weil der Beitretende laut Rules noch kein Mitglied ist.

Mehrere Haushalte (V2.1): Ein Nutzer kann Mitglied beliebig vieler Haushalte sein — maßgeblich ist, in welchen memberUids er steht. users/{uid}.householdId zeigt nur, welcher Haushalt in der App gerade aktiv ist; gewechselt wird im Haushalts-Screen unter „Meine Haushalte“ oder über den Wechsler oben im Menü (die App fragt die eigenen Haushalte per where('memberUids', arrayContains: uid) ab — das erlauben die bestehenden Rules bereits). Beim Beitritt per Code wechselt die App in den neuen Haushalt, alle bisherigen Mitgliedschaften bleiben bestehen. Die tägliche Sammel-Push (sendDailyReminders) und die Tages-Checkliste sammeln fällige Aufgaben aus allen Haushalten des Nutzers (in der Checkliste mit Zwischenüberschrift je Haushalt).

Besitzer, Austreten, Entfernen (V2.2): Jeder Haushalt hat einen Besitzer (ownerUid = Ersteller; Alt-Haushalte ohne das Feld: erster Eintrag in memberUids, denn Beitretende werden hinten angehängt). Der Besitzer wird in der App ausgewiesen und kann als Einziger Mitglieder entfernen; er kann seinen eigenen Haushalt nicht verlassen. Umbenennen dürfen alle vollen Mitglieder — die Rules erlauben Clients am Haushalts-Dokument seitdem nur noch das Namensfeld; jede Mitglieder-Verwaltung läuft über Cloud Functions mit Admin-Rechten:

  • leaveHousehold — selbst austreten (auch als Sitter, der laut Rules das Dokument nicht ändern dürfte)
  • removeMember — ein Mitglied entfernen (nur der Besitzer)

Beide biegen, falls nötig, den aktiven Zeiger (users/{uid}.householdId) des Betroffenen auf einen seiner übrigen Haushalte um; bleibt keiner übrig, legen sie ihm einen frischen eigenen Haushalt an — so steht die App nie ohne Haushalt da.

E-Mail-Nachtrag für Alt-Haushalte: Haushalte aus der V1-Zeit haben für ihren Ersteller keinen Eintrag in der members-Map (die kam erst mit V2) — die Besitzer-Anzeige blieb dadurch leer. Die App trägt deshalb beim Start die eigene E-Mail in allen eigenen Haushalten nach (memberEntrySyncProvider). Die Rules erlauben dafür eine eng begrenzte Ausnahme: Jedes Mitglied (auch Sitter) darf genau seinen eigenen members-Eintrag ändern, die eigene Rolle aber nicht — sonst könnte sich ein Sitter selbst zum Mitglied befördern. Bis der Betroffene seine App einmal geöffnet hat, zeigen andere Geräte „?“ statt der E-Mail.

Die Regeln in firestore.rules setzen das Haushalts-Prinzip durch:

  • Dein Nutzerprofil (users/{uid}) kannst nur du selbst lesen/schreiben.
  • Einen Haushalt sieht und ändert nur, wer in dessen memberUids steht. Anlegen darf man einen Haushalt nur, wenn man sich dabei selbst als Mitglied einträgt (passiert automatisch bei der Registrierung).
  • Alle Unterdaten (Pflanzen, Stellplätze) erben diese Regel: voller Zugriff für Mitglieder, für niemanden sonst. V2 (Sitter einladen) wird damit nur „weitere UID in memberUids aufnehmen".

Wichtig zu verstehen: Die App spricht direkt mit Firestore — die Regeln laufen auf Googles Servern und sind die eigentliche Zugriffskontrolle. Selbst eine manipulierte App könnte fremde Haushalte nicht lesen.

Schritt 5: Foto-Erkennung — Storage, Functions und API-Keys

Der Code ist fertig: Beim Anlegen einer Pflanze kannst du fotografieren, die Cloud Function identifyPlant bestimmt die Art (PlantNet, bei Unsicherheit Claude als Zweitmeinung) und Claude erstellt das deutsche Pflegeprofil samt Intervall-Vorschlägen. Das Foto landet in Firebase Storage im Haushalts-Ordner.

Was du einmalig tun musst:

  1. Storage aktivieren: Firebase Console → Build → Storage → „Jetzt starten" → Standort eur3 (falls gefragt) → Produktionsmodus. (Die Zugriffsregeln kommen danach per Deploy aus storage.rules — gleiches Haushalts-Prinzip wie bei Firestore.)
  2. PlantNet-API-Key holen (kostenlos): Auf https://my.plantnet.org registrieren → unter Settings/API den Key kopieren. Free-Tier: 500 Erkennungen/Tag — mehr als genug.
  3. Beide API-Keys als Secrets hinterlegen (im Terminal, jeweils Key einfügen, Enter):
firebase functions:secrets:set PLANTNET_API_KEY --project leaf-it-to-me-app
firebase functions:secrets:set ANTHROPIC_API_KEY --project leaf-it-to-me-app

Die Keys liegen damit im Google Secret Manager — verschlüsselt, nur die Cloud Function kann sie lesen, sie tauchen nie im Code, im Git oder in der App auf.

Danach (macht Claude): firebase deploy --only functions,storage — deployt die Function nach europe-west3 (Frankfurt) und die Storage-Regeln.

Kosten: PlantNet kostenlos; Claude ca. 13 Cent pro Foto (Erkennungs-Fallback + Pflegeprofil); Functions/Storage bei eurer Nutzung im Gratis-Kontingent. Der Budget-Alarm aus Schritt 2 überwacht alles.

Schritt 6: Push-Erinnerungen (FCM + geplante Function)

So funktioniert es: Die App registriert nach dem Login das Gerät bei Firebase Cloud Messaging und speichert den Geräte-Token, die Zeitzone und die Erinnerungszeit im Nutzer-Dokument. Die geplante Function sendDailyReminders läuft alle 15 Minuten, prüft für jeden Nutzer „ist gerade seine Erinnerungszeit erreicht und gibt es offene Aufgaben?" und schickt dann eine Sammel-Push pro Tag („3 Pflanzen brauchen dich heute 🌱 — Gießen: Monstera, Orchidee · Düngen: Bogenhanf"). Ungültig gewordene Tokens (Gerät gewechselt, App gelöscht) werden automatisch aufgeräumt. Kosten: ~2.900 Läufe/Monat, tief im Gratis-Kontingent.

Android: funktioniert sofort — beim ersten App-Start nach diesem Update fragt die App die Benachrichtigungs-Berechtigung ab, fertig.

iOS braucht den Apple Developer Account (Push ist auf iOS ohne bezahlten Account nicht möglich). Erledigt am 2026-07-20 — iOS-Push ist eingerichtet und läuft auf dem echten Gerät. So wurde es gemacht:

  1. APNs-Schlüssel erzeugen: https://developer.apple.comCertificates, Identifiers & ProfilesKeys → „+" → Namen vergeben, Apple Push Notifications service (APNs) ankreuzen → Continue → Register → .p8-Datei herunterladen (nur einmal möglich!) und die Key ID notieren; die Team ID steht oben rechts im Account.
  2. In Firebase hinterlegen: Firebase Console → Projekteinstellungen (Zahnrad) → Cloud Messaging → Abschnitt „Apple-App-Konfiguration" → APNs-Authentifizierungsschlüssel hochladen (.p8 + Key ID + Team ID).
  3. Push-Capability in Xcode: ios/Runner.xcworkspace öffnen → Ziel RunnerSigning & Capabilities → „+ Capability" → Push Notifications hinzufügen (und einmal Background Modes → Remote notifications anhaken). Dafür muss als Team der Developer Account gewählt sein.

Danach einmal neu bauen (flutter run) — die Erinnerungen landen dann auch auf dem iPhone-Sperrbildschirm.

Stolperfalle (teuer erkauft): „Push Notifications" fehlte in Xcode unter „+ Capability", obwohl „Background Modes" da war. Ursache: Es war noch das alte kostenlose „Personal Team" als Signing-Team gewählt — Gratis-Teams dürfen kein Push, deshalb blendet Xcode den Eintrag komplett aus. Fix: In Xcode → Settings → Accounts die Apple-ID mit der bezahlten Mitgliedschaft hinzufügen und in Signing & Capabilities das Team ohne den Zusatz (Personal Team) wählen. Danach erscheint „Push Notifications" in der Liste.

Schritt 7: Anmelden mit Apple (iOS + Android)

So funktioniert es: Der Login-Screen hat neben E-Mail/Passwort einen Button „Mit Apple anmelden". Auf iOS öffnet sich der native Apple-Dialog, auf Android ein Browser-Fenster, das über den Firebase-Auth-Handler zu Apple und wieder zurück führt — beides über den in firebase_auth eingebauten AppleAuthProvider (kein Zusatz-Paket). Beim ersten Apple-Login legt die App automatisch Profil-Dokument und eigenen Haushalt an (wie bei der normalen Registrierung); bei „E-Mail verbergen“ wird die anonyme Apple-Relay-Adresse gespeichert. Der Code liegt in AuthRepository.signInWithApple().

Am Code ist nichts mehr zu tun — die folgende Konfiguration in Apple-Portal, Firebase-Konsole und Xcode macht den Button funktionsfähig:

  1. App-ID-Capability (iOS): In Xcode (ios/Runner.xcworkspace) → RunnerSigning & Capabilities → „+ Capability" → Sign in with Apple hinzufügen. Das Entitlement com.apple.developer.applesignin ist im Repo schon gesetzt; der Xcode-Schritt aktiviert die Capability zusätzlich an der App-ID dev.leafittome.app und frischt das Provisioning-Profil auf (mit „Automatically manage signing").

  2. Services-ID (für Android/Web): https://developer.apple.comIdentifiers → „+" → Services IDs → z. B. Beschreibung „LeafItToMe Sign-In", Identifier dev.leafittome.signin (frei wählbar, aber ≠ Bundle-ID). Nach dem Anlegen die Services-ID öffnen → Sign In with Apple aktivieren → Configure:

    • Primary App ID: dev.leafittome.app
    • Domains and Subdomains: leaf-it-to-me-app.firebaseapp.com
    • Return URLs: https://leaf-it-to-me-app.firebaseapp.com/__/auth/handler
  3. Sign-in-Schlüssel (.p8): Keys → „+" → Sign in with Apple ankreuzen → Configure → Primary App ID dev.leafittome.app → Register → .p8 herunterladen (nur einmal möglich!) und Key ID notieren. (Eigener Schlüssel, nicht der APNs-Schlüssel aus Schritt 6.)

  4. Firebase-Konsole: Authentication → Sign-in methodApple aktivieren. Für den Android/Web-Flow den Abschnitt „OAuth-Codeflow konfigurieren" ausfüllen:

    • Services ID: dev.leafittome.signin
    • Apple Team ID: (steht oben rechts im Developer-Account)
    • Key ID + Inhalt der .p8-Datei

Danach neu bauen (flutter run) und den Apple-Button auf iPhone und Android-Gerät testen. Erststart legt einen frischen Haushalt an — der wird in households sichtbar.

Hinweis: Apple liefert Name/E-Mail nur beim allerersten Login an eine App. Zum erneuten Testen des Erst-Login-Flows die App unter https://appleid.apple.comAnmelden mit Apple → für „LeafItToMe" die Freigabe entfernen; beim nächsten Login fragt Apple wieder nach den Daten.

Schritt 8: Anmelden mit Google (iOS + Android)

So funktioniert es: Der Login-Screen hat unter dem Apple-Button einen Button „Mit Google anmelden". Auf beiden Plattformen öffnet sich ein Browser-Fenster, das über den Firebase-Auth-Handler zu Google und wieder zurück führt — über den in firebase_auth eingebauten GoogleAuthProvider, genau wie beim Apple-Login auf Android (kein Zusatz-Paket, kein google_sign_in). Beim ersten Google-Login legt der reaktive Haushalts-Bootstrap automatisch Profil + Haushalt an. Der Code liegt in AuthRepository.signInWithGoogle().

Am Code ist nichts mehr zu tun — im Repo sind Button, Fehlerbehandlung und das iOS-URL-Scheme (Encoded App ID in ios/Runner/Info.plist, der Rückweg aus dem Browser in die App) schon eingerichtet. Es fehlt nur ein Konsolen-Schritt:

  1. Firebase-Konsole: Authentication → Sign-in methodGoogle aktivieren. Dabei den öffentlichen App-Namen prüfen und als Support-E-Mail deine Adresse auswählen → Speichern. (Firebase legt die nötigen OAuth-Clients selbst an.)

Danach neu bauen (flutter run) und den Google-Button auf iPhone und Android testen — der erste Login mit einem Google-Konto muss kurz „Dein Haushalt wird eingerichtet …" zeigen und danach normal funktionieren.

Hinweise:

  • Anders als bei Apple ist kein Entwickler-Portal, kein Schlüssel und keine Services-ID nötig — der eine Konsolen-Schalter genügt.
  • Beim Aktivieren zeigt die Konsole den Hinweis „SHA-1-Releasefingerabdruck angeben". Der gilt für den nativen Google-Flow (google_sign_in-Paket) — unser Browser-Flow braucht ihn nicht. Die Debug-Fingerabdrücke (SHA-1 + SHA-256 aus ~/.android/debug.keystore) sind trotzdem per CLI im Projekt hinterlegt (firebase apps:android:sha:create …), damit später nichts fehlt (z. B. App Check). Vor einem App-Store-/Play-Store-Release muss zusätzlich der Fingerabdruck des Release-Schlüssels hinterlegt werden — den gibt es aber noch nicht.
  • Die GoogleService-Info.plist/google-services.json müssen nicht neu heruntergeladen werden — der Flow läuft über den Auth-Handler im Web, nicht über das native Google-SDK.

Schritt 9: App Check aktivieren

So funktioniert es: App Check bestätigt der Firebase-Console, dass ein Function-Aufruf wirklich aus der echten App kommt (Play Integrity auf Android, App Attest auf iOS) — Schutz gegen missbrauchte API-Keys/Function-URLs von außerhalb der App. Der Client-Code ist bereits vorbereitet (FirebaseAppCheck.instance.activate(...) in lib/main.dart), läuft aber noch mit dem Debug-Provider und die Functions erzwingen App Check noch nicht — die Log-Warnung „No AppCheckProvider installed" ist bis dahin harmlos, Login + Security Rules schützen weiterhin.

Zwei getrennte Schritte, nacheinander, nicht gleichzeitig (sonst sperrt man sich versehentlich selbst aus den Functions aus):

  1. Provider registrieren (Firebase-Konsole → App Check):
    • Android-App auswählen → Play Integrity als Provider aktivieren (kein zusätzliches Setup in der Play Console nötig, die API ist automatisch verfügbar).
    • iOS-App auswählen → App Attest als Provider aktivieren.
    • Für Testgeräte im Debug-Modus: beim ersten Start gibt die App in der Konsole (Logcat bzw. Xcode-Log) ein Debug-Token aus — das unter App Check → Apps → [App] → Debug-Token verwalten eintragen, sonst schlagen Debug-Builds nach dem Enforcement fehl.
    • Erwartete, harmlose Fehlermeldung beim iOS-Start: App not registered: ...exchangeDeviceCheckToken ... FAILED_PRECONDITION. Grund: Das native Firebase-SDK registriert beim Plugin-Start zunächst intern den Standard-Provider „DeviceCheck" (bevor unser Dart-Code in main.dart per activate() auf den Debug-Provider umstellt) und schickt in diesem kurzen Fenster automatisch einen ersten Token-Refresh-Versuch los — der schlägt fehl, weil wir DeviceCheck nie in der Console aktiviert haben (wir nutzen App Attest + Debug). Einmalig beim Kaltstart, ohne Funktionsauswirkung, tritt unabhängig vom hinterlegten Debug-Token auf. Zum Prüfen, ob App Check tatsächlich funktioniert: irgendeine Funktion aufrufen (z. B. Pflanze anlegen) und in der Firebase-Console unter App Check → APIs nachsehen, ob „Verified requests" für die iOS-App hochzählt.
    • Metriken abwarten: In der Console unter App Check → APIs prüfen, dass für die genutzten Functions „Verified requests" ankommen (kann nach einem frischen Build ein paar Minuten dauern).
  2. Erst wenn Schritt 1 bestätigt ist: In functions/src/index.ts und functions/src/household.ts bei den onCall(...)-Funktionen (identifyPlant, diagnosePlant, analyzeLocation, assessPlantFit, joinHousehold, leaveHousehold, removeMember) die Option enforceAppCheck: true ergänzen und neu deployen. Danach lehnen die Functions Aufrufe ohne gültiges App-Check-Token ab.

Erledigt (2026-09-08): main.dart schaltet die App-Check-Provider jetzt per kReleaseMode um — Release-Builds (TestFlight/Play) nutzen AndroidPlayIntegrityProvider / AppleAppAttestProvider, Debug-Builds weiter die Debug-Provider mit den hinterlegten Debug-Tokens. Wichtig, weil die Functions App Check erzwingen: Ein Release-Build mit Debug-Provider würde auf fremden Geräten bei jedem Function-Aufruf (Pflanze anlegen, Diagnose, Haushalt beitreten) scheitern. Auf dem ersten TestFlight-Build bestätigt.

Schritt 10: TestFlight einrichten (iOS) — abgeschlossen 2026-09-08

Ziel: die App auf eigenen/Familien-Geräten installierbar machen, ohne öffentlichen Store-Eintrag oder Apple-Review (interne Tester werden nicht geprüft). Erster Build (1.0.0 (1)) am 2026-09-08 hochgeladen, in TestFlight „Bereit zum Testen", auf dem iPhone installiert und nutzbar.

  1. App Store Connect (https://appstoreconnect.apple.com, mit dem aktiven Apple-Developer-Konto) → Apps → „+" → Neue App:
    • Plattform iOS, Name „LeafItToMe" (falls vergeben: „LeafItToMe Pflanzenpflege"), primäre Sprache Deutsch, Bundle-ID dev.leafittome.app aus der Liste wählen, SKU frei (z. B. leafittome-001), voller Zugriff.
    • Screenshots, Beschreibung, Preis, Altersfreigabe sind für interne TestFlight-Tests nicht nötig — nur die Datenschutz-Angaben unten.
  2. App-Informationen → Datenschutzrichtlinie-URL: https://leaf-it-to-me-app.web.app/datenschutz (Hosting via Firebase Hosting, s. u.).
  3. App-Datenschutz (Privacy-Fragebogen): Apple fragt Datentyp für Datentyp ab. Antworten passend zu docs/legal/datenschutz.html — jeweils erhoben: ja, mit Nutzer verknüpft: ja, Tracking: nein, Zweck App-Funktionalität:
    • Kontaktdaten → E-Mail-Adresse (Konto).
    • Benutzerinhalte → Fotos oder Videos (Pflanzen-/Standort-Erkennung).
    • Benutzerinhalte → Andere Benutzerinhalte (Pflanzen-/Haushaltsdaten).
    • Kennungen → Nutzer-ID (Apple/Google-Login-Konto-ID).
    • Alles andere: nicht erhoben. Kein Tracking, keine Werbedaten, keine Analyse-/Diagnosedaten (kein Analytics/Crashlytics im Projekt).
  4. Distribution-Zertifikat anlegen (einmalig, sonst scheitert der Upload!): Xcode → Settings → Accounts → Apple-ID + Team markieren → „Manage Certificates…" → „+" → „Apple Distribution". Ohne dieses Zertifikat gibt es im Schlüsselbund nur „Apple Development", und der Upload wird mit „Invalid Signature … not signed with a distribution certificate" (Code 90035) für jede Framework-Datei abgelehnt. Kontrolle: security find-identity -v -p codesigning muss eine Zeile „Apple Distribution: …" zeigen.
  5. Build als Archiv aus Xcode bauen (nicht flutter build ipa über die CLI — die konnte das Distribution-Zertifikat headless nicht ziehen und fiel auf Development-Signing zurück):
    • flutter build ios --config-only --release
    • ios/Runner.xcworkspace in Xcode öffnen → Geräte-Dropdown „Any iOS Device (arm64)" → Menü Product → Archive.
    • Der Organizer öffnet sich → „Distribute App" → App Store Connect → Upload → Optionen auf Standard.
    • Harmlose Meldungen beim Upload: „Upload Symbols Failed … did not include a dSYM for FirebaseFirestoreInternal / grpc / absl / RecaptchaEnterpriseSDK …". Firebase/gRPC liefern ihre Frameworks vorkompiliert ohne Debug-Symbole; das betrifft nur die Crash-Symbolisierung (ohne Crashlytics irrelevant). Der Build gilt trotzdem als angenommen.
  6. TestFlight → Interne Tests: neue Testgruppe (z. B. „Haushalt"), Tester per Apple-ID-E-Mail — die Personen müssen unter Benutzer und Zugriff im Team stehen (Rolle „Entwickler"/„Kundenservice" reicht). Bis zu 100 interne Tester, kein Review, Build nach wenigen Minuten Verarbeitung verfügbar.
  7. Export-Compliance: taucht dank ITSAppUsesNonExemptEncryption = false in Info.plist nicht als Nachfrage auf.

Schritt 11: Play Internal Testing einrichten (Android)

Ziel: dieselbe „ohne öffentlichen Store-Eintrag"-Stufe für Android.

  1. Voraussetzung — Release-Signing: android/key.properties lokal anlegen (Vorlage: android/key.properties.example) und den Upload-Key erzeugen:
    keytool -genkey -v -keystore android/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
    
    Wichtig: Diesen Befehl bewusst selbst ausführen (per ! keytool ... in der Eingabezeile) und die erzeugte .jks-Datei sofort sichern (Passwort-Manager + Backup außerhalb des Repos) — ein verlorener Upload-Key bedeutet, dass die App auf Play nie wieder aktualisiert werden kann. Datei und key.properties sind absichtlich in .gitignore, landen also nicht im Git-Repo.
  2. Release-APK/AAB bauen: flutter build appbundle --release (nutzt automatisch den Release-Key, sobald key.properties existiert).
  3. Play Console (https://play.google.com/console) → App erstellen: Name „LeafItToMe", Paketname dev.leafittome.app (wird beim ersten AAB-Upload festgelegt).
  4. App-Inhalte (Pflichtabschnitte, auch für den internen Test):
    • Datenschutzerklärung: URL zu docs/legal/datenschutz.html eintragen.
    • Data-Safety-Formular: analog zum Apple-Fragebogen aus Schritt 10 — Fotos, E-Mail/Kontodaten, Nutzerinhalte werden erhoben, sind mit dem Nutzerkonto verknüpft, dienen ausschließlich der App-Funktion, keine Weitergabe zu Werbezwecken, Datenverschlüsselung bei Übertragung: ja, Löschung auf Anfrage: ja.
    • Content-Rating-Fragebogen: Kategorie „Utility/Produktivität", keine der abgefragten Inhalte (Gewalt, Nacktheit, Glücksspiel etc.) treffen zu → niedrigste Alterseinstufung.
    • Zielgruppe: Erwachsene/allgemein, nicht speziell an Kinder gerichtet.
    • Anzeigen: keine Werbung in der App.
  5. Interner Test-Track: Testen → Interner Test → neue Version → AAB hochladen → Tester per Google-Konto-E-Mail oder Google-Gruppe hinzufügen → Link zur Teilnahme an die Tester schicken (kein Review, Verfügbarkeit meist innerhalb weniger Minuten).

Hosting: Datenschutzerklärung öffentlich erreichbar machen

docs/legal/datenschutz.html ist bewusst eine einzelne, eigenständige HTML-Datei ohne Build-Schritt — sie kann 1:1 ausgeliefert werden.

Genutzte Lösung: Firebase Hosting (2026-09-08)

Kein Zusatz-Setup, kostenlos (die ~4-KB-Seite bleibt weit im Gratis-Kontingent: 10 GB Speicher + 10 GB Transfer/Monat). In firebase.json steht ein hosting-Block:

"hosting": {
  "public": "docs/legal",
  "cleanUrls": true,
  "ignore": ["firebase.json", "**/.*"],
  "redirects": [{ "source": "/", "destination": "/datenschutz", "type": 302 }]
}
  • public: "docs/legal" deployt die Seite direkt aus dem Repo — keine Kopie zum Synchronhalten. (Vorsicht: alles, was künftig in docs/legal/ landet, wäre damit öffentlich.)
  • Deploy: firebase deploy --only hosting --project leaf-it-to-me-app
  • URL für App Store Connect / Play Console: https://leaf-it-to-me-app.web.app/datenschutz (die nackte Domain leitet per Redirect dorthin).
  • Nach jeder Änderung an docs/legal/datenschutz.html erneut firebase deploy --only hosting.

Alternative: eigene Domain über Cloudflare + Hetzner-VPS

Falls die Datenschutzerklärung später unter eigener Domain laufen soll — Chris nutzt bereits Cloudflare vor einer eigenen Domain:

  1. Cloudflare-DNS: Subdomain anlegen, z. B. datenschutz.<domain>.tld → A-Record auf die IP des Hetzner-VPS. Proxy (orange Wolke) aktiviert lassen.
  2. SSL/TLS-Modus auf „Full (strict)" stellen (Cloudflare → SSL/TLS → Übersicht) — nicht „Flexible", sonst ist die Strecke Cloudflare↔Server unverschlüsselt. Dafür ein Origin Certificate erzeugen (Cloudflare → SSL/TLS → Origin Server → Zertifikat erstellen; 15 Jahre gültig, kein Renewal wie bei Let's Encrypt nötig) und auf dem Server ablegen.
  3. Auf dem Hetzner-VPS: Datei nach z. B. /var/www/leafittome-legal/datenschutz.html kopieren, neuer nginx-server{}-Block neben dem bestehenden Forgejo-Setup:
    server {
        listen 443 ssl;
        server_name datenschutz.<domain>.tld;
    
        ssl_certificate     /etc/nginx/cloudflare-origin.pem;
        ssl_certificate_key /etc/nginx/cloudflare-origin.key;
    
        root /var/www/leafittome-legal;
        index datenschutz.html;
    }
    
  4. Vor dem Hochladen die Platzhalter ([Name]/[E-Mail-Adresse]/[Datum]) in docs/legal/datenschutz.html ausfüllen.
  5. Die resultierende URL in App Store Connect (Schritt 10) und Play Console (Schritt 11) eintragen.

Begriffe kurz erklärt

  • Projekt: Der Container für alles — eine App = ein Firebase-Projekt.
  • Firestore-Dokument/Collection: Firestore ist keine Tabellen-Datenbank, sondern speichert JSON-artige Dokumente in Collections (Ordnern), z. B. households/abc123/plants/xyz789.
  • Security Rules: Regeln auf dem Server, die festlegen, wer welche Dokumente lesen/schreiben darf — unser Schutz, obwohl die App direkt mit der Datenbank spricht.
  • Cloud Function: Eine Funktion (bei uns TypeScript), die bei Ereignissen oder nach Zeitplan auf Google-Servern läuft.