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

292 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 Abrechnung****Tarif ä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):
```bash
# 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 method***E-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):
```bash
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):
```bash
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.com>*Certificates, Identifiers & Profiles***Keys** → „+" → 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 *Runner**Signing & 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`) → *Runner**Signing & 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.com>*Identifiers* → „+" → **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 method***Apple** 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.com> → *Anmelden 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 method***Google** 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:
```json
"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:
```nginx
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.