Fix: Reaktiver Haushalts-Bootstrap + BootstrapGate (Apple-Login legte keinen Haushalt an)
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 <noreply@anthropic.com>
This commit is contained in:
parent
3673256105
commit
5ed3a68e45
10 changed files with 255 additions and 22 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <https://appleid.apple.com> 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 <https://appleid.apple.com> 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)
|
||||
|
|
|
|||
|
|
@ -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!),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<void> 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<void>? _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<void> 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<void> _bootstrapHouseholdIfNeeded(String uid, String? email) async {
|
||||
final userRef = _firestore.collection('users').doc(uid);
|
||||
|
|
|
|||
71
lib/features/auth/data/household_bootstrap_provider.dart
Normal file
71
lib/features/auth/data/household_bootstrap_provider.dart
Normal file
|
|
@ -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<DocumentSnapshot<Map<String, dynamic>>?>((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<AsyncValue<void>> {
|
||||
@override
|
||||
AsyncValue<void> 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<void> _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, AsyncValue<void>>(
|
||||
HouseholdBootstrap.new);
|
||||
69
lib/features/auth/presentation/bootstrap_gate.dart
Normal file
69
lib/features/auth/presentation/bootstrap_gate.dart
Normal file
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 …",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -255,6 +255,50 @@ void main() {
|
|||
expect((members?['u1'] as Map<String, dynamic>?)?['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<String, dynamic>)['apple1']
|
||||
as Map<String, dynamic>)['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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue