// lib/features/notifications/notification_service.dart // Gestiona FCM: suscripción a topic, handlers foreground/background. // // Regla de privacidad: los payloads de push NUNCA contienen lat/lng. // El backend solo manda title/body desde notificaciones.json. import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; // Canal Android de alta prioridad para alertas de proximidad const _kChannelId = 'recolecta_alerts'; const _kChannelName = 'Alertas de recolección'; const _kChannelDesc = 'Notificaciones de llegada del camión recolector'; /// Notifier simple: la EtaScreen lo escucha para refrescar sin polling. class _FcmMessageNotifier extends ChangeNotifier { RemoteMessage? lastMessage; void notify(RemoteMessage msg) { lastMessage = msg; notifyListeners(); } } // Handler de background/terminated (top-level, fuera de clase) @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { // Solo loguear; la EtaScreen se refrescará cuando la app vuelva a foreground. debugPrint('[FCM background] ${message.notification?.title}'); } class NotificationService { NotificationService._(); static final _messaging = FirebaseMessaging.instance; static final _localNotifications = FlutterLocalNotificationsPlugin(); static final onFcmMessage = _FcmMessageNotifier(); /// Inicializar una sola vez en main.dart static Future initialize() async { // Registrar handler de background FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); // Solicitar permisos (iOS + Android 13+) final settings = await _messaging.requestPermission( alert: true, badge: true, sound: true, ); debugPrint('[FCM] Permission: ${settings.authorizationStatus}'); // Canal Android const androidChannel = AndroidNotificationChannel( _kChannelId, _kChannelName, description: _kChannelDesc, importance: Importance.high, ); await _localNotifications .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(androidChannel); // Inicializar flutter_local_notifications const initSettings = InitializationSettings( android: AndroidInitializationSettings('@mipmap/ic_launcher'), iOS: DarwinInitializationSettings(), ); await _localNotifications.initialize(initSettings); // Foreground: mostrar notificación local + notificar EtaScreen FirebaseMessaging.onMessage.listen((message) { _showLocalNotification(message); onFcmMessage.notify(message); }); // Tap en notificación cuando la app estaba en background FirebaseMessaging.onMessageOpenedApp.listen((message) { onFcmMessage.notify(message); }); // Verificar si la app abrió desde una notificación (terminated) final initial = await _messaging.getInitialMessage(); if (initial != null) { onFcmMessage.notify(initial); } } /// Suscribir al topic de la ruta del ciudadano. /// Llamar justo después de que verified = true en el domicilio. static Future subscribeToRoute(String routeId) async { final topic = 'topic_$routeId'; await _messaging.subscribeToTopic(topic); debugPrint('[FCM] Suscrito a $topic'); } /// Desuscribir (al cambiar de domicilio / colonia) static Future unsubscribeFromRoute(String routeId) async { final topic = 'topic_$routeId'; await _messaging.unsubscribeFromTopic(topic); debugPrint('[FCM] Desuscrito de $topic'); } static Future _showLocalNotification(RemoteMessage message) async { final notification = message.notification; if (notification == null) return; // El payload del backend es solo title+body; NUNCA contiene coordenadas. await _localNotifications.show( notification.hashCode, notification.title, notification.body, NotificationDetails( android: AndroidNotificationDetails( _kChannelId, _kChannelName, channelDescription: _kChannelDesc, importance: Importance.high, priority: Priority.high, // Sin ningún campo de mapa o ubicación ), iOS: const DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ), ), ); } }