diff --git a/docs/handoff.md b/docs/handoff.md index fd5e554..f21d271 100644 --- a/docs/handoff.md +++ b/docs/handoff.md @@ -27,27 +27,22 @@ Private Flutter-App (iOS+Android) zur Pflanzenpflege für Haushalt + Pflanzen-Si ## Offen / Nächstes -### 🟡 Apple-Login-Bug: Fix implementiert, wartet auf Gerätetest durch den Nutzer +### ✅ Apple-Login-Bug behoben und vom Nutzer bestätigt (2026-07-23) -**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. +**Der Bug in Kurzform:** Mit einem neuen Apple-Account schlug jedes Speichern fehl. Zwei Ursachen-Schichten: +1. Der Haushalts-Bootstrap hing an `signInWithApple` — der Router leitet aber schon bei „angemeldet" weiter; scheiterte der Bootstrap danach, blieb der Account dauerhaft ohne Haushalt (Rules lehnen dann korrekt jeden Write ab). +2. **Die eigentliche Falle:** Die Push-Registrierung (`_saveToken`, `set(..., merge: true)`) hatte das `users`-Dokument beim ersten Apple-Login längst angelegt (fcmTokens/timezone, aber ohne `householdId`). Eine Reparatur, die nur auf „users-Doc existiert" prüft, hielt den Account deshalb fälschlich für fertig. -**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"). - -**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 → 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: 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. +**Der Fix (alles committet und gepusht):** +- `householdBootstrapProvider` (`lib/features/auth/data/household_bootstrap_provider.dart`): reaktiver Bootstrap — legt Profil + Haushalt an, sobald „angemeldet, aber **kein `householdId`** im users-Doc". Maßstab ist überall `householdId`, nie die Dokument-Existenz. Schreibt per **merge** (Push-Felder bleiben erhalten). Deckt alle Login-Wege ab (auch künftig Google). +- `BootstrapGate` (`lib/features/auth/presentation/bootstrap_gate.dart`, in `app.dart` eingehängt): legt bis dahin einen deckenden Warte-Bildschirm **über** die App (Stack, kein Austausch des App-Baums — ein Remount mitten im Provider-Update löste „markNeedsBuild during build" aus); Fehler werden mit „Erneut versuchen" sichtbar. +- `AuthRepository`: `ensureHouseholdBootstrap` öffentlich, idempotent, In-Flight-Schutz; `signInWithApple` ruft ihn nicht mehr selbst auf. Selbstheilend für bereits kaputte Accounts. +- Speicher-Fehler in `plant_form_screen` zeigen jetzt den Firebase-Fehlercode (plugin/code) statt einer Generik. +- **Vom Nutzer auf dem iPhone bestätigt:** Anmeldung + Pflanze anlegen funktionieren mit dem Apple-Account. ### Zuletzt erledigt -- **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 (2):** Apple-Login-Bug diagnostiziert und in zwei Schritten behoben (reaktiver Bootstrap + householdId-Maßstab, siehe oben); Riverpod-„markNeedsBuild during build"-Warnung per Overlay-Gate beseitigt; Architektur-Doku um die Bootstrap-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. diff --git a/lib/features/auth/presentation/bootstrap_gate.dart b/lib/features/auth/presentation/bootstrap_gate.dart index 3f989ae..8515690 100644 --- a/lib/features/auth/presentation/bootstrap_gate.dart +++ b/lib/features/auth/presentation/bootstrap_gate.dart @@ -6,10 +6,12 @@ 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. +/// Haushalt existiert. /// /// Beim ersten Login (z. B. mit Apple) legt [householdBootstrapProvider] -/// beides an; bis dahin gibt es einen Warte-Bildschirm. Schlägt der +/// Profil + Haushalt an; bis dahin liegt ein deckender Warte-Bildschirm +/// ÜBER der App (Stack statt Austausch — ein Remount des App-Baums mitten +/// im Provider-Update löst „markNeedsBuild during build“ aus). 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 { @@ -30,6 +32,19 @@ class BootstrapGate extends ConsumerWidget { profile.data()?['householdId'] != null; if (profileReady) return child; + return Stack( + children: [ + child, + _BootstrapSplash(), + ], + ); + } +} + +/// Deckender Warte-/Fehler-Bildschirm, solange der Bootstrap läuft. +class _BootstrapSplash extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { final bootstrap = ref.watch(householdBootstrapProvider); final l10n = AppLocalizations.of(context); final theme = Theme.of(context);