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).
|
- **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").
|
- 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:**
|
**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.
|
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
|
### Zuletzt erledigt
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class AuthRepository {
|
||||||
Future<void>? _bootstrapInFlight;
|
Future<void>? _bootstrapInFlight;
|
||||||
|
|
||||||
/// Legt Profil-Dokument und einen eigenen Haushalt an, falls für [uid]
|
/// 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) —
|
/// (Registrierung und reaktiver Bootstrap können sich überlappen) —
|
||||||
/// ein bestehender Haushalt wird nie überschrieben.
|
/// ein bestehender Haushalt wird nie überschrieben.
|
||||||
Future<void> ensureHouseholdBootstrap(String uid, String? email) {
|
Future<void> ensureHouseholdBootstrap(String uid, String? email) {
|
||||||
|
|
@ -54,9 +54,14 @@ class AuthRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [email] kann bei „E-Mail verbergen“ die Apple-Relay-Adresse sein.
|
/// [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 {
|
Future<void> _bootstrapHouseholdIfNeeded(String uid, String? email) async {
|
||||||
final userRef = _firestore.collection('users').doc(uid);
|
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 safeEmail = email ?? '';
|
||||||
final householdRef = _firestore.collection('households').doc();
|
final householdRef = _firestore.collection('households').doc();
|
||||||
|
|
@ -74,11 +79,17 @@ class AuthRepository {
|
||||||
},
|
},
|
||||||
'createdAt': FieldValue.serverTimestamp(),
|
'createdAt': FieldValue.serverTimestamp(),
|
||||||
});
|
});
|
||||||
batch.set(userRef, {
|
// merge: von der Push-Registrierung bereits geschriebene Felder
|
||||||
|
// (fcmTokens, timezone, reminderTime) bleiben erhalten.
|
||||||
|
batch.set(
|
||||||
|
userRef,
|
||||||
|
{
|
||||||
'email': safeEmail,
|
'email': safeEmail,
|
||||||
'householdId': householdRef.id,
|
'householdId': householdRef.id,
|
||||||
'createdAt': FieldValue.serverTimestamp(),
|
'createdAt': FieldValue.serverTimestamp(),
|
||||||
});
|
},
|
||||||
|
SetOptions(merge: true),
|
||||||
|
);
|
||||||
await batch.commit();
|
await batch.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,13 @@ class HouseholdBootstrap extends Notifier<AsyncValue<void>> {
|
||||||
final user = ref.watch(authStateProvider).value;
|
final user = ref.watch(authStateProvider).value;
|
||||||
final profile = ref.watch(userProfileProvider).value;
|
final profile = ref.watch(userProfileProvider).value;
|
||||||
// Nur aktiv, wenn der Snapshot sicher zum aktuellen Nutzer gehört und
|
// 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 ||
|
if (user == null ||
|
||||||
profile == null ||
|
profile == null ||
|
||||||
profile.id != user.uid ||
|
profile.id != user.uid ||
|
||||||
profile.exists) {
|
profile.data()?['householdId'] != null) {
|
||||||
return const AsyncValue.data(null);
|
return const AsyncValue.data(null);
|
||||||
}
|
}
|
||||||
_run(user);
|
_run(user);
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,11 @@ class BootstrapGate extends ConsumerWidget {
|
||||||
if (user == null) return child; // Login-Screen übernimmt.
|
if (user == null) return child; // Login-Screen übernimmt.
|
||||||
|
|
||||||
final profile = ref.watch(userProfileProvider).value;
|
final profile = ref.watch(userProfileProvider).value;
|
||||||
final profileReady =
|
// householdId statt bloßer Dokument-Existenz: die Push-Registrierung
|
||||||
profile != null && profile.id == user.uid && profile.exists;
|
// legt das users-Dokument auch ohne Haushalt an.
|
||||||
|
final profileReady = profile != null &&
|
||||||
|
profile.id == user.uid &&
|
||||||
|
profile.data()?['householdId'] != null;
|
||||||
if (profileReady) return child;
|
if (profileReady) return child;
|
||||||
|
|
||||||
final bootstrap = ref.watch(householdBootstrapProvider);
|
final bootstrap = ref.watch(householdBootstrapProvider);
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,48 @@ void main() {
|
||||||
expect(find.text('Alles versorgt!'), findsOneWidget);
|
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 {
|
testWidgets('Mitglied kann den Haushalt umbenennen', (tester) async {
|
||||||
await tester.pumpWidget(await buildTestApp());
|
await tester.pumpWidget(await buildTestApp());
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue