Fix: Bootstrap-Maßstab ist householdId, nicht users-Doc-Existenz
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 <noreply@anthropic.com>
This commit is contained in:
parent
5b8a99a26e
commit
0cc39f69b1
5 changed files with 73 additions and 13 deletions
|
|
@ -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 <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.
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class AuthRepository {
|
|||
Future<void>? _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<void> 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<void> _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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,11 +33,13 @@ class HouseholdBootstrap extends Notifier<AsyncValue<void>> {
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue