simulacion de estados y flujo de notificacion, modificacion de estilos en todas las vistas

This commit is contained in:
shinra32
2026-05-23 07:08:49 -06:00
parent ca076607c7
commit 92f570294a
43 changed files with 4335 additions and 2035 deletions

View File

@@ -3,16 +3,20 @@
//
// 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/widgets.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:go_router/go_router.dart';
import '../../core/router/app_router.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;
@@ -21,26 +25,26 @@ class _FcmMessageNotifier extends ChangeNotifier {
notifyListeners();
}
}
// Handler de background/terminated (top-level, fuera de clase)
@pragma('vm:entry-point')
Future<void> _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<void> initialize() async {
// Registrar handler de background
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Solicitar permisos (iOS + Android 13+)
final settings = await _messaging.requestPermission(
alert: true,
@@ -48,7 +52,7 @@ class NotificationService {
sound: true,
);
debugPrint('[FCM] Permission: ${settings.authorizationStatus}');
// Canal Android
const androidChannel = AndroidNotificationChannel(
_kChannelId,
@@ -58,34 +62,61 @@ class NotificationService {
);
await _localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
AndroidFlutterLocalNotificationsPlugin
>()
?.createNotificationChannel(androidChannel);
// Inicializar flutter_local_notifications
const initSettings = InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
iOS: DarwinInitializationSettings(),
);
await _localNotifications.initialize(initSettings);
await _localNotifications.initialize(
initSettings,
onDidReceiveNotificationResponse: (response) {
// Tap del banner mostrado en foreground por flutter_local_notifications
_openNotificationsScreen();
},
);
// 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);
_openNotificationsScreen();
});
// Verificar si la app abrió desde una notificación (terminated)
final initial = await _messaging.getInitialMessage();
if (initial != null) {
onFcmMessage.notify(initial);
// Esperar a que el router termine de montar el árbol antes de navegar.
WidgetsBinding.instance.addPostFrameCallback((_) {
_openNotificationsScreen();
});
}
}
/// Navega a `/notifications` usando el Navigator raíz. Tolerante a que
/// la app aún no esté montada (no hace nada en ese caso).
static void _openNotificationsScreen() {
final ctx = rootNavigatorKey.currentContext;
if (ctx == null) {
debugPrint('[FCM] tap recibido pero el navigator aún no está listo');
return;
}
try {
GoRouter.of(ctx).push('/notifications');
} catch (e) {
debugPrint('[FCM] no se pudo navegar a /notifications: $e');
}
}
/// Suscribir al topic de la ruta del ciudadano.
/// Llamar justo después de que verified = true en el domicilio.
static Future<void> subscribeToRoute(String routeId) async {
@@ -93,18 +124,18 @@ class NotificationService {
await _messaging.subscribeToTopic(topic);
debugPrint('[FCM] Suscrito a $topic');
}
/// Desuscribir (al cambiar de domicilio / colonia)
static Future<void> unsubscribeFromRoute(String routeId) async {
final topic = 'topic_$routeId';
await _messaging.unsubscribeFromTopic(topic);
debugPrint('[FCM] Desuscrito de $topic');
}
static Future<void> _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,
@@ -127,4 +158,4 @@ class NotificationService {
),
);
}
}
}