leafittome/lib/features/notifications/data/push_registration_service.dart
cschlaefke 876ed06a97 Fix: flutter_timezone 5.x liefert TimezoneInfo-Objekt – identifier speichern statt Objekt
Der Firestore-Write warf zur Laufzeit eine Exception (nicht serialisierbares
Objekt), wodurch auch der FCM-Token nie gespeichert wurde. Fehler bei der
Push-Registrierung werden jetzt geloggt statt verschluckt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:43:02 +02:00

91 lines
3.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import '../../../core/firebase/firebase_providers.dart';
/// Registriert das Gerät für Push-Erinnerungen und hält die
/// Erinnerungs-Einstellungen im Nutzer-Dokument aktuell.
///
/// Abstrakte Basis, damit Tests eine No-Op-Variante einsetzen können.
abstract class PushRegistrationService {
Future<void> registerDevice(User user);
Future<void> syncReminderTime(TimeOfDay time);
}
class FirebasePushRegistrationService implements PushRegistrationService {
FirebasePushRegistrationService(this._firestore, this._auth);
final FirebaseFirestore _firestore;
final FirebaseAuth _auth;
bool _tokenListenerAttached = false;
@override
Future<void> registerDevice(User user) async {
final messaging = FirebaseMessaging.instance;
// Fragt beim ersten Mal die Push-Berechtigung ab (iOS-Dialog,
// Android 13+ Systemdialog); danach ist der Aufruf ein No-Op.
final settings = await messaging.requestPermission();
if (settings.authorizationStatus == AuthorizationStatus.denied) return;
final token = await messaging.getToken();
if (token != null) {
await _saveToken(user.uid, token);
}
if (!_tokenListenerAttached) {
_tokenListenerAttached = true;
messaging.onTokenRefresh.listen((newToken) {
final current = _auth.currentUser;
if (current != null) _saveToken(current.uid, newToken);
});
}
}
Future<void> _saveToken(String uid, String token) async {
final timezone = await FlutterTimezone.getLocalTimezone();
await _firestore.collection('users').doc(uid).set({
'fcmTokens': FieldValue.arrayUnion([token]),
// IANA-Kennung, z. B. "Europe/Berlin" das Objekt selbst ist nicht
// Firestore-serialisierbar.
'timezone': timezone.identifier,
}, SetOptions(merge: true));
}
@override
Future<void> syncReminderTime(TimeOfDay time) async {
final user = _auth.currentUser;
if (user == null) return;
final formatted =
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
await _firestore.collection('users').doc(user.uid).set({
'reminderTime': formatted,
}, SetOptions(merge: true));
}
}
final pushRegistrationServiceProvider = Provider<PushRegistrationService>((ref) {
return FirebasePushRegistrationService(
ref.watch(firestoreProvider),
ref.watch(firebaseAuthProvider),
);
});
/// Sorgt dafür, dass das Gerät registriert ist, sobald (und solange) ein
/// Nutzer angemeldet ist — auch wenn der Login schon vor dem App-Start
/// bestand. Wird in app.dart per ref.watch aktiv gehalten.
final pushSetupProvider = Provider<void>((ref) {
final user = ref.watch(authStateProvider).value;
if (user != null) {
ref.read(pushRegistrationServiceProvider).registerDevice(user).catchError(
(Object error, StackTrace stack) {
// Bewusst nur loggen: Push-Registrierung darf den App-Start nie
// blockieren, aber Fehler müssen im Log sichtbar sein.
debugPrint('Push-Registrierung fehlgeschlagen: $error');
},
);
}
});