- 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
292 lines
30 KiB
Markdown
292 lines
30 KiB
Markdown
# 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 1–3 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 1–2 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. 1–3 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.
|