From 0cc39f69b1d92b4042aed25056825b099b00045f Mon Sep 17 00:00:00 2001 From: cschlaefke Date: Thu, 23 Jul 2026 21:43:23 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20Bootstrap-Ma=C3=9Fstab=20ist=20household?= =?UTF-8?q?Id,=20nicht=20users-Doc-Existenz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Push-Registrierung legt users/{uid} per merge selbst an (fcmTokens, timezone) — beim kaputten Apple-Account existierte das Dokument daher ohne householdId, und Gate + Bootstrap hielten den Account für fertig. Jetzt prüfen beide auf householdId; der Bootstrap schreibt das users-Dokument mit merge, damit die Push-Felder erhalten bleiben. Neuer Test bildet exakt diesen Zustand ab. Co-Authored-By: Claude Fable 5 --- docs/handoff.md | 6 ++- lib/features/auth/data/auth_repository.dart | 25 +++++++---- .../data/household_bootstrap_provider.dart | 6 ++- .../auth/presentation/bootstrap_gate.dart | 7 +++- test/widget_test.dart | 42 +++++++++++++++++++ 5 files changed, 73 insertions(+), 13 deletions(-) diff --git a/docs/handoff.md b/docs/handoff.md index a1a8f5b..fd5e554 100644 --- a/docs/handoff.md +++ b/docs/handoff.md @@ -38,10 +38,12 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si - **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"). +**Nachbesserung (gleicher Tag, nach erstem Fehlversuch des Nutzers):** Mit dem Gate-Build schlug Speichern **weiter** fehl, und zwar ohne dass der Einrichtungs-Bildschirm je erschien (mit und ohne Foto). Befund: Die **Push-Registrierung** (`_saveToken`, `set(..., merge: true)`) hatte das `users`-Dokument beim ersten Apple-Login längst angelegt (fcmTokens/timezone, aber ohne `householdId`) — Gate und Bootstrap prüften nur die **Dokument-Existenz** und hielten den Account deshalb für fertig. Fix: Überall ist jetzt **`householdId` vorhanden** der Maßstab (Gate, Bootstrap-Provider, `_bootstrapHouseholdIfNeeded`), und der Bootstrap schreibt das users-Dokument mit **merge**, damit Push-Felder erhalten bleiben. Außerdem zeigt die Speichern-Fehlermeldung jetzt den konkreten Firebase-Fehlercode an (plugin/code statt Generik). Neuer Test stellt exakt den kaputten Zustand nach (users-Doc nur mit fcmTokens) — alle 10 Tests grün. + **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. +1. App auf dem iPhone neu bauen/starten, mit dem **bestehenden Apple-Account** anmelden → jetzt sollte kurz „Dein Haushalt wird eingerichtet …" erscheinen (diesmal wirklich, da das Gate jetzt auf `householdId` prüft), danach Pflanze-Anlegen mit und ohne Foto. 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. +3. Falls es **weiter** fehlschlägt: Die Fehlermeldung nennt jetzt den Code — `firebase_storage/…` heißt Foto-Upload/Storage-Rules/IAM, `cloud_firestore/…` heißt Plant-Write/Firestore-Rules. Den Code melden, dann gezielt weiter. ### Zuletzt erledigt diff --git a/lib/features/auth/data/auth_repository.dart b/lib/features/auth/data/auth_repository.dart index 276aa80..51b2515 100644 --- a/lib/features/auth/data/auth_repository.dart +++ b/lib/features/auth/data/auth_repository.dart @@ -43,7 +43,7 @@ class AuthRepository { Future? _bootstrapInFlight; /// Legt Profil-Dokument und einen eigenen Haushalt an, falls für [uid] - /// noch keins existiert. Idempotent und gegen parallele Aufrufe geschützt + /// noch kein Haushalt 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) { @@ -54,9 +54,14 @@ class AuthRepository { } /// [email] kann bei „E-Mail verbergen“ die Apple-Relay-Adresse sein. + /// + /// Maßstab ist das Feld `householdId`, nicht die bloße Existenz des + /// users-Dokuments: Die Push-Registrierung legt das Dokument nebenbei an + /// (fcmTokens/timezone, merge), ohne dass ein Haushalt existiert. Future _bootstrapHouseholdIfNeeded(String uid, String? email) async { final userRef = _firestore.collection('users').doc(uid); - if ((await userRef.get()).exists) return; + final existing = await userRef.get(); + if (existing.data()?['householdId'] != null) return; final safeEmail = email ?? ''; final householdRef = _firestore.collection('households').doc(); @@ -74,11 +79,17 @@ class AuthRepository { }, 'createdAt': FieldValue.serverTimestamp(), }); - batch.set(userRef, { - 'email': safeEmail, - 'householdId': householdRef.id, - 'createdAt': FieldValue.serverTimestamp(), - }); + // merge: von der Push-Registrierung bereits geschriebene Felder + // (fcmTokens, timezone, reminderTime) bleiben erhalten. + batch.set( + userRef, + { + 'email': safeEmail, + 'householdId': householdRef.id, + 'createdAt': FieldValue.serverTimestamp(), + }, + SetOptions(merge: true), + ); await batch.commit(); } diff --git a/lib/features/auth/data/household_bootstrap_provider.dart b/lib/features/auth/data/household_bootstrap_provider.dart index 4bf5a2c..67d1c98 100644 --- a/lib/features/auth/data/household_bootstrap_provider.dart +++ b/lib/features/auth/data/household_bootstrap_provider.dart @@ -33,11 +33,13 @@ class HouseholdBootstrap extends Notifier> { 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. + // der Haushalt fehlt — bei Kontowechsel hält .value kurz den alten Stand. + // Maßstab ist householdId, nicht die Dokument-Existenz: die Push- + // Registrierung legt das users-Dokument auch ohne Haushalt an. if (user == null || profile == null || profile.id != user.uid || - profile.exists) { + profile.data()?['householdId'] != null) { return const AsyncValue.data(null); } _run(user); diff --git a/lib/features/auth/presentation/bootstrap_gate.dart b/lib/features/auth/presentation/bootstrap_gate.dart index b027df1..3f989ae 100644 --- a/lib/features/auth/presentation/bootstrap_gate.dart +++ b/lib/features/auth/presentation/bootstrap_gate.dart @@ -23,8 +23,11 @@ class BootstrapGate extends ConsumerWidget { if (user == null) return child; // Login-Screen übernimmt. final profile = ref.watch(userProfileProvider).value; - final profileReady = - profile != null && profile.id == user.uid && profile.exists; + // householdId statt bloßer Dokument-Existenz: die Push-Registrierung + // legt das users-Dokument auch ohne Haushalt an. + final profileReady = profile != null && + profile.id == user.uid && + profile.data()?['householdId'] != null; if (profileReady) return child; final bootstrap = ref.watch(householdBootstrapProvider); diff --git a/test/widget_test.dart b/test/widget_test.dart index feeedee..d3041d1 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -299,6 +299,48 @@ void main() { expect(find.text('Alles versorgt!'), findsOneWidget); }); + testWidgets( + 'users-Doc existiert ohne householdId (Push-Registrierung war schneller): ' + 'Haushalt wird nachgezogen, Push-Felder bleiben erhalten', + (tester) async { + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + final auth = MockFirebaseAuth( + signedIn: true, + mockUser: MockUser(uid: 'apple1', email: 'apple@example.com'), + ); + final firestore = FakeFirebaseFirestore(); + // Exakt der Zustand des kaputten Apple-Accounts: Die Push-Registrierung + // hat das users-Dokument angelegt, der Bootstrap kam nie durch. + await firestore.collection('users').doc('apple1').set({ + 'fcmTokens': ['token-1'], + 'timezone': 'Europe/Berlin', + }); + + await tester.pumpWidget(ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + firebaseAuthProvider.overrideWithValue(auth), + firestoreProvider.overrideWithValue(firestore), + pushRegistrationServiceProvider + .overrideWithValue(_FakePushRegistrationService()), + ], + child: const LeafItToMeApp(), + )); + await tester.pumpAndSettle(); + + final userDoc = await firestore.collection('users').doc('apple1').get(); + final householdId = userDoc.data()?['householdId'] as String?; + expect(householdId, isNotNull); + // Die Push-Felder wurden nicht überschrieben (merge statt set). + expect(userDoc.data()?['fcmTokens'], ['token-1']); + expect(userDoc.data()?['timezone'], 'Europe/Berlin'); + final household = + await firestore.collection('households').doc(householdId).get(); + expect(household.data()?['memberUids'], ['apple1']); + expect(find.text('Heute'), findsOneWidget); + }); + testWidgets('Mitglied kann den Haushalt umbenennen', (tester) async { await tester.pumpWidget(await buildTestApp()); await tester.pumpAndSettle();