Fix: Besitzer-Anzeige in Alt-Haushalten – eigene E-Mail wird nachgetragen
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 <noreply@anthropic.com>
This commit is contained in:
parent
ab0e47ff61
commit
edc4ecf405
7 changed files with 106 additions and 9 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<List<Household>>((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<void>((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<HouseholdRole>((ref) {
|
||||
final household = ref.watch(householdProvider).value;
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ class _HouseholdScreenState extends ConsumerState<HouseholdScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
/// 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<void> _switchTo(Household target) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
|
|
@ -277,8 +282,8 @@ class _HouseholdScreenState extends ConsumerState<HouseholdScreen> {
|
|||
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<HouseholdScreen> {
|
|||
title: Text(
|
||||
member.uid == myUid && member.email.isEmpty
|
||||
? l10n.meLabel
|
||||
: member.email,
|
||||
: _labelFor(member.email),
|
||||
),
|
||||
subtitle: Text([
|
||||
member.role == HouseholdRole.sitter
|
||||
|
|
|
|||
|
|
@ -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<String, dynamic>?;
|
||||
expect((members?['u1'] as Map<String, dynamic>?)?['email'],
|
||||
'test@example.com');
|
||||
expect((members?['u1'] as Map<String, dynamic>?)?['role'], 'member');
|
||||
});
|
||||
|
||||
testWidgets('Mitglied kann den Haushalt umbenennen', (tester) async {
|
||||
await tester.pumpWidget(await buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
|
|
|||
Loading…
Reference in a new issue