leafittome/lib/features/auth/data/auth_repository.dart
cschlaefke 0cc39f69b1 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>
2026-07-23 21:43:23 +02:00

104 lines
3.7 KiB
Dart

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';
/// Anmeldung, Registrierung und der Haushalts-Bootstrap.
class AuthRepository {
AuthRepository(this._auth, this._firestore);
final FirebaseAuth _auth;
final FirebaseFirestore _firestore;
Future<void> signIn({required String email, required String password}) async {
await _auth.signInWithEmailAndPassword(email: email, password: password);
}
/// Registrierung: legt den Auth-Nutzer an und danach Profil + Haushalt.
Future<void> signUp({required String email, required String password}) async {
final credential = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
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.
///
/// 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');
await _auth.signInWithProvider(provider);
}
Future<void>? _bootstrapInFlight;
/// Legt Profil-Dokument und einen eigenen Haushalt an, falls für [uid]
/// 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) {
return _bootstrapInFlight ??=
_bootstrapHouseholdIfNeeded(uid, email).whenComplete(() {
_bootstrapInFlight = null;
});
}
/// [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);
final existing = await userRef.get();
if (existing.data()?['householdId'] != null) return;
final safeEmail = email ?? '';
final householdRef = _firestore.collection('households').doc();
final batch = _firestore.batch();
batch.set(householdRef, {
'name': 'Mein Haushalt',
'ownerUid': uid,
'memberUids': [uid],
'members': {
uid: {
'role': 'member',
'email': safeEmail,
'joinedAt': FieldValue.serverTimestamp(),
},
},
'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();
}
Future<void> signOut() => _auth.signOut();
}
final authRepositoryProvider = Provider<AuthRepository>((ref) {
return AuthRepository(
ref.watch(firebaseAuthProvider),
ref.watch(firestoreProvider),
);
});