From 5ed3a68e45ee6afe2a814a8ce27c0c5bcfff5417 Mon Sep 17 00:00:00 2001 From: cschlaefke Date: Thu, 23 Jul 2026 17:49:49 +0200 Subject: [PATCH] Fix: Reaktiver Haushalts-Bootstrap + BootstrapGate (Apple-Login legte keinen Haushalt an) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Bootstrap hing an signInWithApple und lief erst, nachdem der Router den Nutzer schon in die App geleitet hatte — Fehler blieben unsichtbar, der Account blieb ohne users-Doc/Haushalt und alle Writes scheiterten an den Rules. Jetzt beobachtet householdBootstrapProvider das users-Dokument und legt Profil + Haushalt reaktiv an (alle Login-Wege); das BootstrapGate hält die App so lange zurück und macht Fehler mit „Erneut versuchen" sichtbar. Bestehende kaputte Accounts heilen beim nächsten Start selbst. Co-Authored-By: Claude Fable 5 --- docs/architektur.md | 1 + docs/handoff.md | 26 +++---- lib/app.dart | 5 +- lib/features/auth/data/auth_repository.dart | 30 +++++--- .../data/household_bootstrap_provider.dart | 71 +++++++++++++++++++ .../auth/presentation/bootstrap_gate.dart | 69 ++++++++++++++++++ lib/l10n/app_de.arb | 3 + lib/l10n/generated/app_localizations.dart | 18 +++++ lib/l10n/generated/app_localizations_de.dart | 10 +++ test/widget_test.dart | 44 ++++++++++++ 10 files changed, 255 insertions(+), 22 deletions(-) create mode 100644 lib/features/auth/data/household_bootstrap_provider.dart create mode 100644 lib/features/auth/presentation/bootstrap_gate.dart diff --git a/docs/architektur.md b/docs/architektur.md index 95c9f50..f6eea04 100644 --- a/docs/architektur.md +++ b/docs/architektur.md @@ -48,6 +48,7 @@ Jedes Feature folgt demselben Muster (nicht jedes braucht alle Schichten): - **Haushaltszentriert gedacht:** Pflanzen, Stellplätze und Aufgaben gehören dem *Haushalt*, nicht einem Nutzer. In Firestore wird das `households/{id}/plants/{id}` usw. — so ist V2 (Sharing) nur „weiteres Mitglied hinzufügen", keine Datenmigration. - **Mehrere Haushalte pro Nutzer:** Quelle der Wahrheit für die Mitgliedschaft ist das Feld `memberUids` der Haushalte (`myHouseholdsProvider` fragt sie per `arrayContains` ab). `users/{uid}.householdId` ist nur noch der Zeiger auf den gerade **aktiven** Haushalt — Wechseln heißt: diesen Zeiger umstellen (Haushalts-Screen „Meine Haushalte“ oder Wechsler im Menü). Der aktive Haushalt bestimmt Pflanzen-/Stellplatz-Listen und wohin neue Pflanzen wandern; die **Tages-Checkliste und die Sammel-Push aggregieren dagegen über alle Haushalte** (Checkliste mit Zwischenüberschriften, `DueTask` trägt dafür seine Haushalts-Herkunft). Jeder Haushalt hat einen **Besitzer** (`ownerUid`): nur er entfernt Mitglieder (`removeMember`-Function), er kann nicht austreten; alle anderen können per `leaveHousehold`-Function austreten. Umbenennen dürfen volle Mitglieder direkt (Rules lassen am Haushalts-Dokument clientseitig nur noch `name` zu). - **`null` bei `lastWatered` heißt „noch nie"** → Aufgabe ist sofort fällig. So braucht eine neu angelegte Pflanze keine Sonderbehandlung. +- **Haushalts-Bootstrap läuft reaktiv, nicht am Login-Aufruf:** Beim ersten Login (egal ob E-Mail-Registrierung, Apple oder künftig Google) müssen Profil-Dokument (`users/{uid}`) und ein eigener Haushalt angelegt werden — ohne sie lehnen die Rules jeden Schreibzugriff ab. Statt den Bootstrap an die einzelnen Login-Methoden zu hängen (fehleranfällig: der Router leitet schon bei „angemeldet“ weiter, ein Bootstrap-Fehler danach wäre unsichtbar), beobachtet `householdBootstrapProvider` (auth/data) das `users`-Dokument und legt beides an, sobald „angemeldet, aber kein Profil“ eintritt. Das `BootstrapGate` in `app.dart` zeigt so lange einen Warte-Bildschirm und macht Fehler mit „Erneut versuchen“ sichtbar. ## Übersetzungen (i18n) diff --git a/docs/handoff.md b/docs/handoff.md index a1cc251..a1a8f5b 100644 --- a/docs/handoff.md +++ b/docs/handoff.md @@ -27,24 +27,26 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si ## Offen / Nächstes -### 🔴 AKTIVER BUG (hier weitermachen): Apple-Login legt keinen Haushalt an +### 🟡 Apple-Login-Bug: Fix implementiert, wartet auf Gerätetest durch den Nutzer -**Symptom:** Nutzer hat Apple-Login vollständig konfiguriert (Apple-Portal Services-ID + Sign-in-Key, Firebase-Console Apple-Provider, Xcode „Sign in with Apple"-Capability) — **Anmeldung klappt**. Aber: Mit dem **neuen Apple-Account** schlägt das **Anlegen einer Pflanze** mit generischer Meldung „kann nicht gespeichert werden" fehl (`saveError`, aus dem `catch (_)` in `plant_form_screen.dart:154`). +**Der Bug:** Mit einem neuen Apple-Account schlug das Anlegen einer Pflanze fehl („Speichern fehlgeschlagen"). Ursache (per Code-Analyse bestätigt, Firestore-Sichtung stand aus): Der Haushalts-Bootstrap hing an `signInWithApple` **nach** `signInWithProvider` — der Router leitet aber schon bei „angemeldet" weiter (redirect prüft nur `currentUser != null`). Wirft der Bootstrap oder wird die App vorher beendet, bleibt der Nutzer dauerhaft ohne `users`-Doc + Haushalt, und die (korrekten) Rules lehnen jeden Schreibzugriff ab. -**Leithypothese (noch nicht bestätigt):** Beim Apple-Login wurde **kein Haushalt angelegt**. Mechanismus: `signInWithProvider` meldet „angemeldet" → `authStateChanges` feuert → der Router (`app_router.dart`, redirect prüft nur `currentUser != null`, **nicht** ob ein Haushalt existiert) schiebt sofort in die App. Der Bootstrap (`_bootstrapHouseholdIfNeeded`) läuft erst **danach** in `signInWithApple`. Wirft er, wird der Fehler zwar in `_signInWithApple` (login_screen) gefangen und als `authErrorAppleFailed` gezeigt — aber der Nutzer ist bereits eingeloggt und weggeroutet, sieht die Meldung also faktisch nicht. Ohne Haushalt ist `householdId` null bzw. der Haushalt fehlt in `memberUids` → jeder Schreibzugriff (Storage-Foto-Upload **und** Firestore-Plant-Write) wird von den Rules abgelehnt. **Die Rules selbst sind geprüft und korrekt** (Plant-`create` verlangt nur `isMember && roleOf=='member'`, exakt was der Bootstrap schreibt) — Problem liegt an fehlenden Bootstrap-Daten, nicht an den Rules. +**Der Fix (2026-07-23 implementiert, Option „reaktiver Bootstrap"):** +- `householdBootstrapProvider` (`lib/features/auth/data/household_bootstrap_provider.dart`): beobachtet das `users/{uid}`-Dokument; bei „angemeldet, aber kein Profil" legt er Profil + Haushalt an. Deckt **alle** Login-Wege ab (auch künftig Google). +- `BootstrapGate` (`lib/features/auth/presentation/bootstrap_gate.dart`, eingehängt in `app.dart` builder): hält die App zurück, bis das Profil existiert; zeigt Warte-Bildschirm bzw. Fehler mit „Erneut versuchen". +- `AuthRepository`: Bootstrap jetzt öffentlich (`ensureHouseholdBootstrap`, idempotent + In-Flight-Schutz); `signInWithApple` ruft ihn nicht mehr selbst auf. +- **Selbstheilend:** Der bereits kaputte Apple-Account wird beim nächsten App-Start automatisch repariert (Gate sieht „kein Profil" → legt Haushalt an). +- Analyzer grün, alle 9 Tests grün (neuer Test: „Erster Login ohne Profil → Haushalt wird reaktiv angelegt"). -**Warteschritt (auf Nutzer-Antwort, dann entscheiden):** Nutzer sollte prüfen und melden: -- **A)** App → Menü → Haushalt: wird für den Apple-Account ein Haushalt angezeigt (Mitglied/Besitzer) oder leer? -- **B)** Firebase-Konsole → Firestore: existiert `users/{uid}` für den Apple-Account mit Feld `householdId`? Falls ja, hat `households/{id}.memberUids` die UID + `members`-Map mit `role:"member"`? -- **C)** Hatte die Pflanze ein **Foto** (trennt Storage- von Firestore-Fehler)? - -**Wenn Hypothese bestätigt (Haushalt fehlt) → Fix-Richtung:** Bootstrap **race-/schluck-sicher** machen, statt ihn nur an `signInWithApple` zu hängen. Optionen: (a) reaktiver Bootstrap — ein Provider/Guard, der bei „eingeloggt aber kein `users`-Doc" den Haushalt anlegt und bis dahin einen Lade-/Redirect-Zustand zeigt (deckt auch künftige Provider ab); oder (b) Router-Redirect zusätzlich an „Haushalt vorhanden" koppeln + Bootstrap-Fehler sichtbar machen. Vorschlag (a) bevorzugen. Danach mit frischem Apple-Account (unter Freigabe für „LeafItToMe" entfernen → nächster Login ist wieder „erster Login") Ende-zu-Ende testen. - -**Wenn Haushalt existiert (Hypothese falsch):** dann liegt es doch an Rules/IAM — Storage-`isMember` macht `firestore.get()` (braucht das bereits erteilte IAM-Recht) bzw. Firestore-Plant-`create`. Dann konkret den fehlschlagenden Schritt (Storage vs. Firestore, via Frage C) isolieren. +**Was der Nutzer jetzt testen sollte:** +1. App auf dem iPhone neu bauen/starten, mit dem **bestehenden Apple-Account** anmelden → kurzer „Dein Haushalt wird eingerichtet …"-Bildschirm, danach sollte Pflanze-Anlegen (mit Foto!) funktionieren. +2. Optional Härtetest „echter Erst-Login": unter die Freigabe für „LeafItToMe" entfernen → nächster Login ist wieder „erster Login" → Ende-zu-Ende inkl. Pflanze mit Foto. +3. Falls es **weiter** fehlschlägt, obwohl der Haushalt angezeigt wird (Menü → Haushalt): dann liegt es doch an Storage-Rules/IAM — dann isolieren, ob Foto-Upload (Storage) oder Pflanze-Schreiben (Firestore) scheitert. ### Zuletzt erledigt -- **2026-07-23:** Apple-Login **implementiert und committet** (`signInWithApple` via eingebautem `AppleAuthProvider`, „oder"-Trenner + Button im Login, iOS-Entitlement `com.apple.developer.applesignin`, l10n-Strings, Doku `firebase-einrichtung.md` Schritt 7). Analyzer + alle 8 Tests grün. **Konfiguration vom Nutzer erledigt, Login funktioniert — aber der obige Bug ist offen.** +- **2026-07-23 (2):** Reaktiver Haushalts-Bootstrap + BootstrapGate implementiert (Fix für den Apple-Login-Bug, siehe oben); Architektur-Doku um die Entscheidung ergänzt. +- **2026-07-23 (1):** Apple-Login **implementiert und committet** (`signInWithApple` via eingebautem `AppleAuthProvider`, „oder"-Trenner + Button im Login, iOS-Entitlement `com.apple.developer.applesignin`, l10n-Strings, Doku `firebase-einrichtung.md` Schritt 7). Konfiguration vom Nutzer erledigt, Login funktioniert. - **2026-07-20:** iOS-Push eingerichtet (Apple Developer Account freigeschaltet, APNs/Firebase/Xcode). V1 damit auch auf iOS vollständig. ### Weitere Kandidaten (nach dem Bug) diff --git a/lib/app.dart b/lib/app.dart index 2745025..01850fe 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'core/router/app_router.dart'; import 'core/settings/settings_provider.dart'; import 'core/theme/app_theme.dart'; +import 'features/auth/presentation/bootstrap_gate.dart'; import 'features/household/data/household_providers.dart'; import 'features/notifications/data/push_registration_service.dart'; import 'l10n/generated/app_localizations.dart'; @@ -45,7 +46,9 @@ class LeafItToMeApp extends ConsumerWidget { data: MediaQuery.of(context).copyWith( textScaler: TextScaler.linear(systemScale * settings.textSize.factor), ), - child: child!, + // Lässt frisch angemeldete Nutzer erst in die App, wenn Profil + + // Haushalt existieren (legt beides bei Bedarf an). + child: BootstrapGate(child: child!), ); }, ); diff --git a/lib/features/auth/data/auth_repository.dart b/lib/features/auth/data/auth_repository.dart index 4772f7a..276aa80 100644 --- a/lib/features/auth/data/auth_repository.dart +++ b/lib/features/auth/data/auth_repository.dart @@ -21,26 +21,38 @@ class AuthRepository { email: email, password: password, ); - await _bootstrapHouseholdIfNeeded(credential.user!.uid, email); + await ensureHouseholdBootstrap(credential.user!.uid, email); } /// Anmeldung mit Apple. Nativer Dialog auf iOS, Web-Flow (über den /// Firebase-Auth-Handler) auf Android — beides über den eingebauten - /// [AppleAuthProvider], daher ohne Zusatz-Paket. Beim **ersten** Login - /// existiert noch kein Firestore-Profil, deshalb hängt der Haushalts- - /// Bootstrap hier dran (Apple legt nur den Auth-Nutzer an). + /// [AppleAuthProvider], daher ohne Zusatz-Paket. + /// + /// Der Haushalts-Bootstrap hängt bewusst NICHT mehr hier dran: sobald + /// signInWithProvider zurückkehrt, feuert authStateChanges und der Router + /// leitet in die App weiter — ein danach geworfener Bootstrap-Fehler wäre + /// auf dem Login-Screen unsichtbar und ließe den Nutzer ohne Haushalt + /// zurück. Stattdessen übernimmt der reaktive householdBootstrapProvider. Future signInWithApple() async { final provider = AppleAuthProvider() ..addScope('email') ..addScope('name'); - final credential = await _auth.signInWithProvider(provider); - final user = credential.user!; - await _bootstrapHouseholdIfNeeded(user.uid, user.email); + await _auth.signInWithProvider(provider); } + Future? _bootstrapInFlight; + /// Legt Profil-Dokument und einen eigenen Haushalt an, falls für [uid] - /// noch keins existiert. Idempotent — mehrfaches Anmelden mit Apple oder - /// eine erneute Registrierung überschreibt einen bestehenden Haushalt nie. + /// noch keins existiert. Idempotent und gegen parallele Aufrufe geschützt + /// (Registrierung und reaktiver Bootstrap können sich überlappen) — + /// ein bestehender Haushalt wird nie überschrieben. + Future ensureHouseholdBootstrap(String uid, String? email) { + return _bootstrapInFlight ??= + _bootstrapHouseholdIfNeeded(uid, email).whenComplete(() { + _bootstrapInFlight = null; + }); + } + /// [email] kann bei „E-Mail verbergen“ die Apple-Relay-Adresse sein. Future _bootstrapHouseholdIfNeeded(String uid, String? email) async { final userRef = _firestore.collection('users').doc(uid); diff --git a/lib/features/auth/data/household_bootstrap_provider.dart b/lib/features/auth/data/household_bootstrap_provider.dart new file mode 100644 index 0000000..4bf5a2c --- /dev/null +++ b/lib/features/auth/data/household_bootstrap_provider.dart @@ -0,0 +1,71 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../core/firebase/firebase_providers.dart'; +import 'auth_repository.dart'; + +/// users/{uid}-Dokument des angemeldeten Nutzers, live (null = ausgeloggt). +/// +/// Existiert das Dokument nicht (erster Login), stößt +/// [householdBootstrapProvider] den Haushalts-Bootstrap an. +final userProfileProvider = + StreamProvider>?>((ref) { + final user = ref.watch(authStateProvider).value; + if (user == null) return Stream.value(null); + return ref + .watch(firestoreProvider) + .collection('users') + .doc(user.uid) + .snapshots(); +}); + +/// Reaktiver Haushalts-Bootstrap: legt Profil + eigenen Haushalt an, sobald +/// ein Nutzer angemeldet ist, für den noch kein users-Dokument existiert. +/// +/// Deckt alle Login-Wege ab (E-Mail-Registrierung, Apple, künftig Google) — +/// unabhängig davon, wann der Router weiterleitet. Das BootstrapGate hält +/// die App zurück, bis das Profil da ist, und zeigt Fehler samt +/// „Erneut versuchen“ an. +class HouseholdBootstrap extends Notifier> { + @override + AsyncValue build() { + final user = ref.watch(authStateProvider).value; + final profile = ref.watch(userProfileProvider).value; + // Nur aktiv, wenn der Snapshot sicher zum aktuellen Nutzer gehört und + // das Profil fehlt — bei Kontowechsel hält .value kurz den alten Stand. + if (user == null || + profile == null || + profile.id != user.uid || + profile.exists) { + return const AsyncValue.data(null); + } + _run(user); + return const AsyncValue.loading(); + } + + Future _run(User user) async { + try { + await ref + .read(authRepositoryProvider) + .ensureHouseholdBootstrap(user.uid, user.email); + // Kein Erfolgs-State nötig: das neue users-Dokument kommt über den + // Snapshot-Stream herein und baut diesen Notifier als „fertig“ neu. + } catch (error, stackTrace) { + if (!ref.mounted) return; + state = AsyncValue.error(error, stackTrace); + } + } + + /// Nach einem Fehler erneut versuchen (Knopf im BootstrapGate). + void retry() { + final user = ref.read(authStateProvider).value; + if (user == null) return; + state = const AsyncValue.loading(); + _run(user); + } +} + +final householdBootstrapProvider = + NotifierProvider>( + HouseholdBootstrap.new); diff --git a/lib/features/auth/presentation/bootstrap_gate.dart b/lib/features/auth/presentation/bootstrap_gate.dart new file mode 100644 index 0000000..b027df1 --- /dev/null +++ b/lib/features/auth/presentation/bootstrap_gate.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../core/firebase/firebase_providers.dart'; +import '../../../l10n/generated/app_localizations.dart'; +import '../data/household_bootstrap_provider.dart'; + +/// Hält die App zurück, solange zum angemeldeten Nutzer noch kein +/// users-Dokument (und damit kein Haushalt) existiert. +/// +/// Beim ersten Login (z. B. mit Apple) legt [householdBootstrapProvider] +/// beides an; bis dahin gibt es einen Warte-Bildschirm. Schlägt der +/// Bootstrap fehl, wird der Fehler hier sichtbar — vorher konnte man in +/// der App landen, deren Schreibzugriffe dann alle an den Rules scheiterten. +class BootstrapGate extends ConsumerWidget { + const BootstrapGate({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(authStateProvider).value; + if (user == null) return child; // Login-Screen übernimmt. + + final profile = ref.watch(userProfileProvider).value; + final profileReady = + profile != null && profile.id == user.uid && profile.exists; + if (profileReady) return child; + + final bootstrap = ref.watch(householdBootstrapProvider); + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + return Scaffold( + body: SafeArea( + child: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: bootstrap.hasError + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.cloud_off, + size: 48, color: theme.colorScheme.error), + const SizedBox(height: 16), + Text(l10n.bootstrapFailed, textAlign: TextAlign.center), + const SizedBox(height: 16), + FilledButton( + onPressed: () => ref + .read(householdBootstrapProvider.notifier) + .retry(), + child: Text(l10n.bootstrapRetry), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text(l10n.bootstrapPreparing, + textAlign: TextAlign.center), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 36a7c1b..4514ecf 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -212,6 +212,9 @@ "authErrorWeakPassword": "Das Passwort muss mindestens 6 Zeichen lang sein.", "authErrorInvalidEmail": "Bitte gib eine gültige E-Mail-Adresse ein.", "authErrorGeneric": "Das hat leider nicht geklappt. Bitte versuche es erneut.", + "bootstrapPreparing": "Dein Haushalt wird eingerichtet …", + "bootstrapFailed": "Dein Haushalt konnte nicht eingerichtet werden. Bitte prüfe deine Internetverbindung und versuche es erneut.", + "bootstrapRetry": "Erneut versuchen", "takePhoto": "Foto aufnehmen", "fromGallery": "Aus der Galerie", "recognizing": "Pflanze wird erkannt …", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 7b0f000..3e494cf 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -766,6 +766,24 @@ abstract class AppLocalizations { /// **'Das hat leider nicht geklappt. Bitte versuche es erneut.'** String get authErrorGeneric; + /// No description provided for @bootstrapPreparing. + /// + /// In de, this message translates to: + /// **'Dein Haushalt wird eingerichtet …'** + String get bootstrapPreparing; + + /// No description provided for @bootstrapFailed. + /// + /// In de, this message translates to: + /// **'Dein Haushalt konnte nicht eingerichtet werden. Bitte prüfe deine Internetverbindung und versuche es erneut.'** + String get bootstrapFailed; + + /// No description provided for @bootstrapRetry. + /// + /// In de, this message translates to: + /// **'Erneut versuchen'** + String get bootstrapRetry; + /// No description provided for @takePhoto. /// /// In de, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 372595f..5744420 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -414,6 +414,16 @@ class AppLocalizationsDe extends AppLocalizations { String get authErrorGeneric => 'Das hat leider nicht geklappt. Bitte versuche es erneut.'; + @override + String get bootstrapPreparing => 'Dein Haushalt wird eingerichtet …'; + + @override + String get bootstrapFailed => + 'Dein Haushalt konnte nicht eingerichtet werden. Bitte prüfe deine Internetverbindung und versuche es erneut.'; + + @override + String get bootstrapRetry => 'Erneut versuchen'; + @override String get takePhoto => 'Foto aufnehmen'; diff --git a/test/widget_test.dart b/test/widget_test.dart index 9cd1255..feeedee 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -255,6 +255,50 @@ void main() { expect((members?['u1'] as Map?)?['role'], 'member'); }); + testWidgets( + 'Erster Login ohne Profil (z. B. Apple): Haushalt wird reaktiv angelegt', + (tester) async { + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + // Angemeldet (wie nach signInWithProvider), aber Firestore ist leer – + // genau der Zustand, in dem Pflanzen-Speichern an den Rules scheiterte. + final auth = MockFirebaseAuth( + signedIn: true, + mockUser: MockUser(uid: 'apple1', email: 'apple@example.com'), + ); + final firestore = FakeFirebaseFirestore(); + + await tester.pumpWidget(ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + firebaseAuthProvider.overrideWithValue(auth), + firestoreProvider.overrideWithValue(firestore), + pushRegistrationServiceProvider + .overrideWithValue(_FakePushRegistrationService()), + ], + child: const LeafItToMeApp(), + )); + await tester.pumpAndSettle(); + + // Das BootstrapGate hat Profil + Haushalt angelegt … + final userDoc = await firestore.collection('users').doc('apple1').get(); + expect(userDoc.exists, isTrue); + final householdId = userDoc.data()?['householdId'] as String?; + expect(householdId, isNotNull); + final household = + await firestore.collection('households').doc(householdId).get(); + expect(household.data()?['ownerUid'], 'apple1'); + expect(household.data()?['memberUids'], ['apple1']); + expect( + ((household.data()?['members'] as Map)['apple1'] + as Map)['role'], + 'member'); + + // … und die App ist normal nutzbar (Heute-Screen, keine Aufgaben). + expect(find.text('Heute'), findsOneWidget); + expect(find.text('Alles versorgt!'), findsOneWidget); + }); + testWidgets('Mitglied kann den Haushalt umbenennen', (tester) async { await tester.pumpWidget(await buildTestApp()); await tester.pumpAndSettle();