From edc4ecf4055753196d582b12c06619d21d6e8444 Mon Sep 17 00:00:00 2001 From: cschlaefke Date: Mon, 20 Jul 2026 00:50:22 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20Besitzer-Anzeige=20in=20Alt-Haushalten?= =?UTF-8?q?=20=E2=80=93=20eigene=20E-Mail=20wird=20nachgetragen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Haushalte aus der V1-Zeit haben keinen members-Eintrag für ihren Ersteller, die Besitzer-Anzeige blieb dadurch leer. Die App trägt die eigene E-Mail jetzt beim Start in allen eigenen Haushalten nach; die Rules erlauben dafür nur den eigenen members-Eintrag ohne Rollenwechsel. Bis dahin zeigt die UI „?" statt einer leeren Stelle. Co-Authored-By: Claude Fable 5 --- docs/firebase-einrichtung.md | 2 + docs/handoff.md | 2 +- firestore.rules | 26 ++++++++++--- lib/app.dart | 4 ++ .../household/data/household_providers.dart | 31 +++++++++++++++ .../presentation/household_screen.dart | 11 ++++-- test/widget_test.dart | 39 +++++++++++++++++++ 7 files changed, 106 insertions(+), 9 deletions(-) diff --git a/docs/firebase-einrichtung.md b/docs/firebase-einrichtung.md index 6437c70..7d278fb 100644 --- a/docs/firebase-einrichtung.md +++ b/docs/firebase-einrichtung.md @@ -96,6 +96,8 @@ invites/{code} → Einladung: householdId, Rolle; nur Erstelle 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. diff --git a/docs/handoff.md b/docs/handoff.md index 9bdba89..b161c4b 100644 --- a/docs/handoff.md +++ b/docs/handoff.md @@ -24,7 +24,7 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si ## Offen / Nächstes -1. V2.2 vom Nutzer testen lassen (Umbenennen, Austreten, Entfernen, Drawer-Wechsler, gruppierte Checkliste); Functions- und Rules-Deploy nötig. +1. V2.2 vom Nutzer weitgehend getestet und für gut befunden. Nachgebessert: (a) Invoker-Berechtigung fehlte bei den frisch angelegten Functions `leaveHousehold`/`removeMember` (Cloud-Run-401; Fix = gezielter Neu-Deploy durch den Nutzer), (b) Besitzer-Anzeige blieb bei Alt-Haushalten leer → E-Mail-Nachtrag beim App-Start (`memberEntrySyncProvider`) + Rules-Ausnahme für den eigenen members-Eintrag. **Rules-Deploy für (b) noch nötig**, danach Besitzer-Anzeige auf beiden Geräten prüfen (beide Apps einmal öffnen). 2. iOS-Push, sobald Apple Developer Account existiert. 3. **V3:** Krankheits-Diagnose per Foto, Stellplatz-Bewertung (Standort-Analyse pro Stellplatz + Eignungs-Sterne pro Pflanze — Zwei-Ebenen-Konzept siehe Memory), Umtopf-Erinnerungen. 4. **Backlog:** Button „KI-Zweitmeinung" bei unbefriedigendem PlantNet-Ergebnis; Push-Anzeige auch bei App im Vordergrund; App Check vor App-Store-Release; Englisch (`app_en.arb`). diff --git a/firestore.rules b/firestore.rules index 2aac382..45d749b 100644 --- a/firestore.rules +++ b/firestore.rules @@ -51,13 +51,29 @@ service cloud.firestore { // Vom Client aus dürfen volle Mitglieder nur den Namen ändern; // jede Mitglieder-Verwaltung (Beitritt, Austritt, Entfernen) läuft // ausschließlich über Cloud Functions mit Admin-Rechten. + // Ausnahme: Jeder darf seinen EIGENEN members-Eintrag pflegen (die App + // trägt so die E-Mail in Alt-Haushalten nach), die Rolle aber nicht + // ändern – sonst könnte sich ein Sitter selbst befördern. allow update: if signedIn() && request.auth.uid in resource.data.memberUids - && resource.data.get('members', {}) - .get(request.auth.uid, {'role': 'member'}) - .get('role', 'member') == 'member' - && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['name']); + && ( + (resource.data.get('members', {}) + .get(request.auth.uid, {'role': 'member'}) + .get('role', 'member') == 'member' + && request.resource.data.diff(resource.data).affectedKeys() + .hasOnly(['name'])) + || (request.resource.data.diff(resource.data).affectedKeys() + .hasOnly(['members']) + && request.resource.data.members + .diff(resource.data.get('members', {})) + .affectedKeys() + .hasOnly([request.auth.uid]) + && request.resource.data.members[request.auth.uid] + .get('role', 'member') + == resource.data.get('members', {}) + .get(request.auth.uid, {'role': 'member'}) + .get('role', 'member')) + ); // Anlegen nur, wenn man sich selbst als Mitglied einträgt // (passiert automatisch bei der Registrierung). allow create: if signedIn() diff --git a/lib/app.dart b/lib/app.dart index cd48464..2745025 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/household/data/household_providers.dart'; import 'features/notifications/data/push_registration_service.dart'; import 'l10n/generated/app_localizations.dart'; @@ -19,6 +20,9 @@ class LeafItToMeApp extends ConsumerWidget { // Nutzer angemeldet ist (auch wenn der Login vor dem App-Start bestand). ref.watch(pushSetupProvider); + // Trägt die eigene E-Mail in Alt-Haushalten nach (Besitzer-Anzeige). + ref.watch(memberEntrySyncProvider); + return MaterialApp.router( routerConfig: router, onGenerateTitle: (context) => AppLocalizations.of(context).appTitle, diff --git a/lib/features/household/data/household_providers.dart b/lib/features/household/data/household_providers.dart index 486ad0c..c71a5b3 100644 --- a/lib/features/household/data/household_providers.dart +++ b/lib/features/household/data/household_providers.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_functions/cloud_functions.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/firebase/firebase_providers.dart'; @@ -91,6 +92,36 @@ final myHouseholdsProvider = StreamProvider>((ref) { }); }); +/// Trägt die eigene E-Mail in der members-Map aller eigenen Haushalte nach. +/// +/// Haushalte aus der V1-Zeit haben für ihren Ersteller keinen members-Eintrag +/// (die Map kam erst mit V2) – dadurch blieb die Besitzer-Anzeige leer. +/// Läuft beim App-Start mit, sobald die Haushalte geladen sind; die Rules +/// erlauben jedem genau die Pflege des eigenen Eintrags (ohne Rollenwechsel). +/// Wird in app.dart per ref.watch aktiv gehalten. +final memberEntrySyncProvider = Provider((ref) { + final user = ref.watch(authStateProvider).value; + final email = user?.email; + if (user == null || email == null || email.isEmpty) return; + + final households = ref.watch(myHouseholdsProvider).value ?? const []; + for (final household in households) { + final me = household.memberByUid(user.uid); + if (me != null && me.email.isNotEmpty) continue; + ref + .read(firestoreProvider) + .collection('households') + .doc(household.id) + .update({ + 'members.${user.uid}.email': email, + 'members.${user.uid}.role': (me?.role ?? HouseholdRole.member).name, + }).catchError((Object error) { + // Nur loggen – eine fehlende Anzeige darf die App nicht stören. + debugPrint('E-Mail-Nachtrag für ${household.id} fehlgeschlagen: $error'); + }); + } +}); + /// Rolle des angemeldeten Nutzers im aktuellen Haushalt. final myRoleProvider = Provider((ref) { final household = ref.watch(householdProvider).value; diff --git a/lib/features/household/presentation/household_screen.dart b/lib/features/household/presentation/household_screen.dart index 8081fcf..9f9ffb6 100644 --- a/lib/features/household/presentation/household_screen.dart +++ b/lib/features/household/presentation/household_screen.dart @@ -77,6 +77,11 @@ class _HouseholdScreenState extends ConsumerState { ); } + /// E-Mail mit Fallback: Alt-Einträge ohne E-Mail zeigen „?“, bis die App + /// des Betroffenen sie nachgetragen hat (memberEntrySyncProvider). + String _labelFor(String? email) => + (email == null || email.isEmpty) ? '?' : email; + Future _switchTo(Household target) async { final l10n = AppLocalizations.of(context); final messenger = ScaffoldMessenger.of(context); @@ -277,8 +282,8 @@ class _HouseholdScreenState extends ConsumerState { subtitle: Text( myUid != null && entry.isOwner(myUid) ? l10n.ownerIsMe - : l10n.ownerIs( - entry.memberByUid(entry.ownerUid)?.email ?? '?'), + : l10n.ownerIs(_labelFor( + entry.memberByUid(entry.ownerUid)?.email)), ), trailing: entry.id == household?.id ? Chip(label: Text(l10n.activeHouseholdLabel)) @@ -303,7 +308,7 @@ class _HouseholdScreenState extends ConsumerState { title: Text( member.uid == myUid && member.email.isEmpty ? l10n.meLabel - : member.email, + : _labelFor(member.email), ), subtitle: Text([ member.role == HouseholdRole.sitter diff --git a/test/widget_test.dart b/test/widget_test.dart index f58e587..9cd1255 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -216,6 +216,45 @@ void main() { expect(find.text('Mitglied entfernen?'), findsOneWidget); }); + testWidgets( + 'Alt-Haushalt ohne members-Eintrag: eigene E-Mail wird nachgetragen', + (tester) async { + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + final auth = MockFirebaseAuth( + signedIn: true, + mockUser: MockUser(uid: 'u1', email: 'test@example.com'), + ); + final firestore = FakeFirebaseFirestore(); + await firestore.collection('users').doc('u1').set({ + 'email': 'test@example.com', + 'householdId': 'h1', + }); + // V1-Zeit: kein ownerUid, keine members-Map – nur memberUids. + await firestore.collection('households').doc('h1').set({ + 'name': 'Mein Haushalt', + 'memberUids': ['u1'], + }); + + await tester.pumpWidget(ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + firebaseAuthProvider.overrideWithValue(auth), + firestoreProvider.overrideWithValue(firestore), + pushRegistrationServiceProvider + .overrideWithValue(_FakePushRegistrationService()), + ], + child: const LeafItToMeApp(), + )); + await tester.pumpAndSettle(); + + final doc = await firestore.collection('households').doc('h1').get(); + final members = doc.data()?['members'] as Map?; + expect((members?['u1'] as Map?)?['email'], + 'test@example.com'); + expect((members?['u1'] as Map?)?['role'], 'member'); + }); + testWidgets('Mitglied kann den Haushalt umbenennen', (tester) async { await tester.pumpWidget(await buildTestApp()); await tester.pumpAndSettle();