flutter_firebase_push_notification_provider_web 3.0.0
flutter_firebase_push_notification_provider_web: ^3.0.0 copied to clipboard
A Flutter package for managing Firebase Cloud Messaging (FCM) push notifications for web applications. Handles permissions, token management, and message routing.
Flutter Firebase Push Notification Provider #
Un paquete Flutter que proporciona una interfaz unificada y optimizada para gestionar notificaciones push de Firebase Cloud Messaging (FCM). Maneja permisos, tokens y mensajes en todos los estados de la aplicación (foreground, background y terminada) con gestión automática de memoria y prevención de leaks.
Features #
- ✅ Manejo automático de notificaciones en foreground, background y cuando la app está cerrada
- ✅ Stream unificado para recibir todas las notificaciones
- ✅ Gestión automática de permisos de notificación
- ✅ Actualización automática de tokens FCM en el servidor MongoDB
- ✅ Filtrado de mensajes por
IdPushNotification - ✅ Prevención de memory leaks con gestión apropiada de subscriptions
- ✅ Protección contra inicialización múltiple
- ✅ Manejo robusto de errores con propagación apropiada
- ✅ Integración automática con información del dispositivo y versión de la app
- ✅ Debug logging integrado con formato consistente
- ✅ Verificación de tokens existentes para evitar duplicados
- ✅ Gestión de estados de registros (activo/bloqueado)
- ✅ Anulación de tokens con eliminación de FCM y actualización de estado
Getting started #
Prerequisitos #
- Flutter SDK >= 1.17.0
- Dart SDK >= 3.3.2
- Firebase configurado en tu proyecto (Guía de configuración)
- Servidor MongoDB configurado con la colección requerida (ver sección "Configuración del Servidor MongoDB")
- Paquetes adicionales requeridos (disponibles en pub.flutter-io.cn):
api_rest_flutter_webflutter_utils_providersflutter_models_provider
Instalación #
Agrega el paquete a tu pubspec.yaml:
dependencies:
flutter_firebase_push_notification_provider_web: ^3.0.0
Luego ejecuta:
flutter pub get
Configuración del Servidor MongoDB #
⚠️ REQUISITO FUNDAMENTAL #
Antes de usar este paquete, es requisito fundamental que tu servidor MongoDB tenga configurada la siguiente colección según se define en EnvironmentPushNotification:
Nombre de la Colección
tokenFirebase
Estructura de la Colección MongoDB
La colección debe tener los siguientes campos en el orden especificado:
| Campo | Tipo | Descripción | Ejemplo |
|---|---|---|---|
data.idUsuario |
String | ID del usuario en la aplicación | "user_12345" |
data.token o data.tokenFirebase |
String | Token de Firebase Cloud Messaging | "dGh1c2lzYXRva2Vu..." |
data.dispositivo |
String | Información del dispositivo | "Samsung Galaxy S21" |
data.plataforma |
String | Plataforma del dispositivo | "android" o "ios" |
data.appVersion |
String | Versión de la aplicación | "1.0.0" |
estado |
String | Estado del registro | "activo" o "bloqueado" |
⚠️ Nota sobre nomenclatura: Actualmente existe una inconsistencia en el código donde:
- Al crear el registro se usa el campo
data.token(definido enEnvironmentPushNotification.camposColeccionTokenFirebase) - Al consultar el registro se usa el campo
data.tokenFirebase(en métodosanularToken()y_verificarTokenExistente())
Recomendación: Asegúrate de que tu colección MongoDB use data.tokenFirebase como nombre del campo, o actualiza el código para usar consistentemente data.token.
Ejemplo de Documento en MongoDB (Data)
{
"_id": "507f1f77bcf86cd799439011",
"idServer": "507f1f77bcf86cd799439011",
"idMobile": "mobile_123",
"estado": "activo",
"creadoEl": "2025-01-15T10:30:00Z",
"data": {
"idUsuario": "user_12345",
"tokenFirebase": "dGh1c2lzYXRva2VuZnJvbWZpcmViYXNl...",
"dispositivo": "Samsung Galaxy S21",
"plataforma": "android",
"appVersion": "1.0.0"
}
}
Nota: El ejemplo muestra el campo como tokenFirebase según la implementación actual en las consultas.
Índices Recomendados
Para optimizar el rendimiento, crea los siguientes índices en la colección:
// Índice único compuesto para evitar duplicados
db.tokenFirebase.createIndex(
{ "data.idUsuario": 1, "data.tokenFirebase": 1 },
{ unique: true }
)
// Índice para búsquedas por usuario
db.tokenFirebase.createIndex({ "data.idUsuario": 1 })
// Índice para búsquedas por token
db.tokenFirebase.createIndex({ "data.tokenFirebase": 1 })
// Índice para búsquedas por estado
db.tokenFirebase.createIndex({ "estado": 1 })
// Índice compuesto para consultas de tokens activos
db.tokenFirebase.createIndex(
{ "data.tokenFirebase": 1, "estado": 1 }
)
⚠️ IMPORTANTE: Orden de Campos
El orden de los campos en EnvironmentPushNotification.camposColeccionTokenFirebase es crítico:
static const camposColeccionTokenFirebase = [
'idUsuario', // 1. Primero
'token', // 2. Segundo
'dispositivo', // 3. Tercero
'plataforma', // 4. Cuarto
'appVersion', // 5. Quinto
];
Este orden debe coincidir con el orden en que se pasan los datos en el código. No alterar este orden.
Usage #
1. Inicialización en main.dart #
Importante: Ya no necesitas proporcionar coleccionFireBaseTokens porque ahora se usa la configuración de EnvironmentPushNotification.
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_firebase_push_notification_provider/flutter_firebase_push_notification_provider.dart';
import 'package:api_rest_flutter/api_rest.dart';
import 'package:api_rest_flutter/util/utils_data_api_rest.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Inicializar Firebase primero
await Firebase.initializeApp();
// Crear instancias necesarias
final apiRest = ApiRest(/* tu configuración */);
final utilsDataApiRest = UtilsDataApiRest(/* tu configuración */);
// Inicializar el manejador de notificaciones (usando el Singleton)
try {
final notificationProvider = FlutterFireBasePushNotificationProvider();
await notificationProvider.initializeApp(
apiRest: apiRest,
utilsDataApiRest: utilsDataApiRest,
idUsuario: 'USER_ID', // ID del usuario actual
coleccionAuth: 'auth', // Colección de autenticación
);
print('✅ Notificaciones inicializadas correctamente');
} catch (e) {
print('❌ Error al inicializar notificaciones: $e');
}
runApp(MyApp());
}
2. Escuchar mensajes en tu widget #
import 'dart:async';
import 'package:flutter_firebase_push_notification_provider/flutter_firebase_push_notification_provider.dart';
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
StreamSubscription<Map<String, dynamic>>? _subscription;
late final FlutterFireBasePushNotificationProvider _notificationProvider;
@override
void initState() {
super.initState();
// Obtener la instancia del Singleton (siempre devuelve la misma instancia)
_notificationProvider = FlutterFireBasePushNotificationProvider();
// Verificar que esté inicializado antes de suscribirse
if (_notificationProvider.isInitialized) {
_suscribirseANotificaciones();
} else {
print('⚠️ El provider no está inicializado');
}
}
void _suscribirseANotificaciones() {
// Escuchar mensajes del stream
_subscription = _notificationProvider.messagesStream.listen(
(mensaje) {
// Procesar notificación
final idNotification = mensaje['data']?['IdPushNotification'];
if (idNotification != null) {
print('📩 ID Notificación: $idNotification');
print('📩 Título: ${mensaje['notification']?['title'] ?? 'Sin título'}');
print('📩 Mensaje: ${mensaje['notification']?['body'] ?? 'Sin mensaje'}');
// Aquí puedes navegar a una pantalla específica o mostrar un dialog
_procesarNotificacion(mensaje);
}
},
onError: (error) {
print('❌ Error en el stream de notificaciones: $error');
},
);
}
void _procesarNotificacion(Map<String, dynamic> mensaje) {
// Implementa tu lógica de negocio aquí
// Por ejemplo: navegación, actualización de UI, etc.
}
@override
void dispose() {
// Cancelar la suscripción para evitar memory leaks
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Notificaciones')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Estado: ${_notificationProvider.isInitialized ? "Inicializado" : "No inicializado"}'),
SizedBox(height: 16),
ElevatedButton(
onPressed: _notificationProvider.hasPermission
? () => print('✅ Permisos otorgados')
: () => print('⚠️ Sin permisos de notificación'),
child: Text('Verificar Permisos'),
),
],
),
),
);
}
}
3. Obtener el Token FCM #
Existen dos formas de obtener el token FCM:
3.1. Acceder al token actual (Forma recomendada)
Puedes acceder directamente al token actual almacenado sin hacer una llamada asíncrona:
final notificationProvider = FlutterFireBasePushNotificationProvider();
// Acceso directo al token actual
final currentToken = notificationProvider.currentToken;
if (currentToken.isNotEmpty) {
print('Token FCM actual: $currentToken');
// Hacer algo con el token
} else {
print('No hay token disponible');
}
3.2. Obtener el token manualmente (Llamada asíncrona)
Si necesitas obtener el token directamente desde Firebase (por ejemplo, para forzar una actualización):
final notificationProvider = FlutterFireBasePushNotificationProvider();
final token = await notificationProvider.getToken();
if (token.isNotEmpty) {
print('Token FCM: $token');
// Hacer algo con el token
} else {
print('No se pudo obtener el token');
}
Nota: La propiedad currentToken es más eficiente ya que accede directamente al token almacenado sin necesidad de hacer una llamada asíncrona a Firebase.
4. Anular un Token FCM (Cerrar Sesión/Dispositivo) #
Cuando un usuario cierra sesión o necesitas desactivar las notificaciones push en un dispositivo específico, debes anular el token de Firebase. Este método:
- Elimina el token de Firebase Cloud Messaging en el dispositivo
- Marca el token como bloqueado en tu base de datos MongoDB (cambia el estado de
activoabloqueado)
Importante: El método anularToken() usa automáticamente el token actual almacenado (currentToken), por lo que NO necesitas pasar el token como parámetro.
¿Cuándo usar anularToken()?
- 🔐 Cierre de sesión: Cuando el usuario hace logout
- 📱 Cambio de dispositivo: Cuando el usuario se muda a otro dispositivo
- 🔒 Seguridad: Cuando se detecta actividad sospechosa y necesitas revocar acceso
- 🚫 Desactivar notificaciones: Cuando el usuario desea dejar de recibir notificaciones
Ejemplo de Uso
import 'package:flutter_firebase_push_notification_provider/flutter_firebase_push_notification_provider.dart';
Future<void> cerrarSesionUsuario() async {
final notificationProvider = FlutterFireBasePushNotificationProvider();
try {
// Anular el token en Firebase y marcar como bloqueado en MongoDB
// El método usa automáticamente el token actual almacenado
await notificationProvider.anularToken(
apiRest: apiRest,
utilsDataApiRest: utilsDataApiRest,
);
print('✅ Token anulado correctamente');
print('📱 El dispositivo ya no recibirá notificaciones');
// Continuar con el proceso de cierre de sesión
// Navigator.pushReplacement(...);
} catch (e) {
print('❌ Error al anular token: $e');
// Manejar el error apropiadamente
// Podrías mostrar un dialog o continuar con el logout de todas formas
}
}
Ejemplo Completo en un Botón de Logout
ElevatedButton(
onPressed: () async {
// Mostrar diálogo de confirmación
final confirmar = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Cerrar Sesión'),
content: Text('¿Estás seguro que deseas cerrar sesión?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('Cancelar'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('Sí, cerrar sesión'),
),
],
),
);
if (confirmar == true) {
// Mostrar indicador de carga
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Center(child: CircularProgressIndicator()),
);
try {
final notificationProvider = FlutterFireBasePushNotificationProvider();
// Anular token FCM (usa automáticamente el token actual almacenado)
await notificationProvider.anularToken(
apiRest: apiRest,
utilsDataApiRest: utilsDataApiRest,
);
// Limpiar sesión local, SharedPreferences, etc.
// await _limpiarSesionLocal();
// Cerrar indicador de carga
Navigator.pop(context);
// Navegar a pantalla de login
Navigator.pushReplacementNamed(context, '/login');
} catch (e) {
// Cerrar indicador de carga
Navigator.pop(context);
// Mostrar error
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error al cerrar sesión: $e'),
backgroundColor: Colors.red,
),
);
}
}
},
child: Text('Cerrar Sesión'),
)
¿Qué hace anularToken() internamente?
- Verifica el token actual: Comprueba que
currentTokenno esté vacío - Elimina el token de FCM: Llama a
FirebaseMessaging.instance.deleteToken()para eliminar el token del dispositivo - Consulta MongoDB: Busca todos los registros en la colección
tokenFirebasedonde:tokenFirebase= token actual almacenado (_currentToken)estado="activo"
- Actualiza el estado: Cambia el estado de todos los registros encontrados a
"bloqueado" - Logs detallados: Registra cada operación para debugging
Gestión de Estados
El sistema utiliza dos estados para los registros de tokens:
| Estado | Descripción | Cuándo se usa |
|---|---|---|
activo |
Token válido y en uso | Cuando se crea o actualiza un token |
bloqueado |
Token desactivado | Cuando se anula un token con anularToken() |
Esto permite mantener un historial de tokens sin eliminarlos físicamente de la base de datos, lo cual es útil para:
- 📊 Auditoría: Saber qué dispositivos tenía el usuario
- 🔍 Debugging: Identificar problemas de tokens duplicados
- 📈 Análisis: Estadísticas de uso por dispositivo/plataforma
Manejo de Errores
try {
await notificationProvider.anularToken(
apiRest: apiRest,
utilsDataApiRest: utilsDataApiRest,
);
} on Exception catch (e) {
// Error específico de la operación
if (e.toString().contains('Error al leer registro')) {
print('⚠️ No se pudo consultar la base de datos');
} else if (e.toString().contains('Error al cambiar estado')) {
print('⚠️ No se pudo actualizar el estado del token');
} else {
print('❌ Error desconocido: $e');
}
// Decidir si continuar con logout de todas formas
// o mostrar error al usuario
} catch (e) {
print('❌ Error inesperado: $e');
}
Notas Importantes
- ⚠️ No reinicializar: Después de anular el token, NO intentes inicializar nuevamente sin reiniciar la app
- 🔄 Nuevo login: Si el usuario inicia sesión nuevamente, la app debe reiniciarse o el token debe regenerarse
- 📝 Logging: Todos los pasos se registran en logs de debug para facilitar troubleshooting
- 🗄️ MongoDB: Los registros NO se eliminan, solo se marcan como
bloqueado
5. Formato del mensaje #
Los mensajes recibidos en el stream tienen el siguiente formato:
{
'senderId': String,
'category': String?,
'collapseKey': String?,
'contentAvailable': bool,
'data': Map<String, dynamic>, // Contiene IdPushNotification
'from': String?,
'messageId': String?,
'messageType': String?,
'mutableContent': bool,
'notification': {
'title': String?,
'body': String?,
'android': {...},
'apple': {...}
},
'sentTime': int?, // millisecondsSinceEpoch
'threadId': String?,
'ttl': int?
}
Nota importante: Solo se procesarán mensajes que incluyan el campo IdPushNotification en el objeto data.
6. Verificar estado y permisos #
final notificationProvider = FlutterFireBasePushNotificationProvider();
// Verificar si está inicializado
if (notificationProvider.isInitialized) {
print('✅ Provider inicializado');
// Verificar permisos
if (notificationProvider.hasPermission) {
print('✅ Permisos de notificación otorgados');
} else {
print('⚠️ Sin permisos de notificación');
}
} else {
print('⚠️ Provider no inicializado');
}
// Verificar si está disposed
if (notificationProvider.isDisposed) {
print('⚠️ Provider ya fue disposed, necesita reinicio de la app');
}
7. Actualización automática de tokens #
El paquete maneja automáticamente la actualización de tokens en tu servidor MongoDB cuando:
- Se obtiene el token inicial
- El token es actualizado por Firebase (token refresh)
Proceso de Actualización
- Verificación: El paquete primero verifica si el token ya existe en la base de datos
- Creación: Si no existe, crea un nuevo documento con toda la información
- Información Guardada:
idUsuario: ID del usuario actualtoken: Token FCMdispositivo: Marca y modelo del dispositivo (ej: "Samsung Galaxy S21")plataforma: "android" o "ios"appVersion: Versión de la app (ej: "1.0.0")
No necesitas llamar manualmente ningún método, todo se maneja automáticamente una vez inicializado.
8. Manejo del ciclo de vida de la app #
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
// SOLO disponer si la app se está cerrando completamente
// NO llamar en dispose normal de widgets
// final notificationProvider = FlutterFireBasePushNotificationProvider();
// notificationProvider.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Mi App',
home: HomeScreen(),
);
}
}
Manejo de Errores #
El paquete propaga errores apropiadamente, lo que permite manejarlos en tu código:
try {
await notificationProvider.initializeApp(
apiRest: apiRest,
utilsDataApiRest: utilsDataApiRest,
idUsuario: userId,
coleccionAuth: 'auth',
);
} on StateError catch (e) {
// Error de estado (ya inicializado, disposed, Firebase no inicializado)
print('Error de estado: $e');
} on FirebaseException catch (e) {
// Error de Firebase
print('Error de Firebase: ${e.message}');
} catch (e) {
// Otros errores (red, API, esquema no encontrado, etc.)
print('Error inesperado: $e');
}
Características Avanzadas #
Prevención de Memory Leaks #
El paquete gestiona automáticamente todas las subscriptions de Firebase:
FirebaseMessaging.onMessageFirebaseMessaging.onMessageOpenedAppFirebaseMessaging.onTokenRefresh
Todas se cancelan apropiadamente cuando se llama a dispose().
Protección contra Inicialización Múltiple #
El método initializeApp() puede llamarse múltiples veces de forma segura. Las llamadas adicionales serán ignoradas con un log de debug.
Manejo de Background Messages #
Los mensajes en background se procesan automáticamente incluso cuando la app está cerrada, gracias al handler @pragma('vm:entry-point') que se ejecuta en un isolate separado.
Verificación de Tokens Duplicados #
Antes de crear un nuevo registro en MongoDB, el paquete verifica si el token ya existe para evitar duplicados. Esto optimiza el uso de recursos y mantiene la base de datos limpia.
Additional information #
Notas técnicas #
- El manejador de mensajes en background se ejecuta en un isolate separado
- Los mensajes se filtran por el campo
IdPushNotificationen data - El logging solo aparece en modo debug (
kDebugMode) - Todas las subscriptions se manejan internamente para prevenir memory leaks
- Los tokens se actualizan automáticamente en el servidor con información del dispositivo
- La consulta MongoDB utiliza escapado de caracteres para prevenir inyecciones
- Los métodos
_getDeviceInfo()y_getAppVersion()se ejecutan en paralelo para mejor performance - La colección
tokenFirebaseestá configurada enEnvironmentPushNotification.coleccionTokenFirebase - El orden de campos en
EnvironmentPushNotification.camposColeccionTokenFirebasees crítico y no debe alterarse - Los tokens se gestionan con estados
activoybloqueadopara mantener historial - El método
anularToken()elimina el token de FCM y marca registros como bloqueados en MongoDB - Las consultas de verificación filtran solo tokens con estado
activo - Los tokens bloqueados se mantienen en la base de datos para auditoría y análisis
Solución de Problemas Comunes #
Error: "Firebase no está inicializado"
// Asegúrate de inicializar Firebase antes
await Firebase.initializeApp();
Error: "No se puede inicializar: la instancia ya fue disposed"
// Necesitas reiniciar la app. El singleton no puede ser reinicializado después de dispose()
Error: "No se pudo obtener esquema de la colección"
// Verifica que tu servidor tenga configurada la colección 'tokenFirebase'
// y que apiRest.obtieneEsquema() pueda acceder a ella
No recibo notificaciones en background
// Verifica que el mensaje incluya IdPushNotification en data
{
"notification": {
"title": "Título",
"body": "Mensaje"
},
"data": {
"IdPushNotification": "unique-id-123" // ← Requerido
}
}
Permisos de notificación denegados
// En iOS, los permisos solo se pueden solicitar una vez
// El usuario debe ir a Settings -> Tu App -> Notifications para habilitarlos
// En Android, se pueden solicitar múltiples veces
Token no se actualiza en el servidor
// Verifica:
// 1. Que la colección 'tokenFirebase' exista en MongoDB
// 2. Que el esquema sea correcto
// 3. Que apiRest tenga las credenciales correctas
// 4. Revisa los logs de debug para ver el error específico
Error: "Token ya existe en la base de datos"
// Esto es normal. El paquete verifica si el token existe antes de crear uno nuevo
// Este mensaje solo aparece en logs de debug y no es un error
Error al anular token
// Si anularToken() falla:
// 1. Verifica que haya un token actual almacenado (currentToken no vacío)
// 2. Verifica la conexión a MongoDB
// 3. Revisa los logs de debug para ver el error específico
// 4. Considera continuar con el logout incluso si falla (seguridad)
try {
await notificationProvider.anularToken(
apiRest: apiRest,
utilsDataApiRest: utilsDataApiRest,
);
} catch (e) {
print('Error al anular token: $e');
// Continuar con logout de todas formas
_procederConLogout();
}
Token bloqueado sigue recibiendo notificaciones
// Los tokens bloqueados en MongoDB NO bloquean el envío desde Firebase
// Para detener las notificaciones completamente debes:
// 1. Usar anularToken() que elimina el token del dispositivo
// 2. NO enviar notificaciones a tokens marcados como 'bloqueado' desde tu servidor
// 3. Filtrar tokens activos al enviar notificaciones masivas
Ejemplo de Implementación Completa con BLoC #
Este ejemplo muestra cómo integrar el paquete en una aplicación Flutter usando el patrón BLoC para gestionar el estado de las notificaciones push.
1. Listener de Autenticación #
Primero, configuramos los listeners en relación al login para inicializar Firebase o anular el token cuando el usuario cierra sesión:
///[AUTH]
BlocListener<AuthBloc, AuthState>(
listenWhen: (previous, current) => !current.isWorking,
listener: (context, state) {
if (state.error.isEmpty) {
if (state.accion == AuthBloc.blocOnLoginAuth) {
final estadoUsuario =
FlutterUtilsProvider.determinaValorCampo([
EnvironmentApiRest.estado,
], state.usuarioAuth);
if (estadoUsuario == EstadoRegistro.bloqueado.valor) {
///TODO - Mostrar dialogo de cuenta bloqueada
return;
}
final DataShpProvider shp = DataShpProvider();
if (state.isLoggedIn &&
!shp.keyLogin.startsWith(
EnvironmentApiRest.userFlutter,
)) {
//
context.read<ConfigAppBloc>().add(
OnInitPushNotificaciones(
usuarioAuth: state.usuarioAuth,
),
);
}
}
if (state.accion == AuthBloc.blocOnLogoutAuth) {
context.read<ConfigAppBloc>().add(OnAnulaTokenFirebase());
}
}
},
)
2. Eventos del BLoC #
Define los eventos que se dispararán para manejar las notificaciones push:
///Evento que inicializa las push notificaciones de Firebase
class OnInitPushNotificaciones extends ConfigAppEvent {
final Map<String, dynamic> usuarioAuth;
OnInitPushNotificaciones({required this.usuarioAuth});
@override
List<Object> get props => [usuarioAuth];
}
///Evento que se lanza cuando llega una nueva notificacion desde Firebase
class OnNuevaNotificacionFirebase extends ConfigAppEvent {
final Map<String, dynamic> firebaseNotification;
OnNuevaNotificacionFirebase({required this.firebaseNotification});
@override
List<Object> get props => [firebaseNotification];
}
///Evento que se lanza cuando se cierra sesion entonces se debe anular el token
///de firebase para que no lleguen mas notificaciones a este usuario
class OnAnulaTokenFirebase extends ConfigAppEvent {
OnAnulaTokenFirebase();
@override
List<Object> get props => [];
}
3. Manejadores de Eventos en el BLoC #
Implementa los métodos que responden a estos eventos:
Future<void> _onInitPushNotificaciones(
OnInitPushNotificaciones event,
Emitter<ConfigAppState> emit,
) async {
await _handleAction(onInitPushNotificaciones, emit, () async {
if (notificationFireBaseProvider.isInitialized) {
debugPrint(
'Push notifications ya inicializadas, se omite reinicialización.',
);
return;
}
debugPrint('🔔 notificationFireBaseProvider.initializeApp Inicializa');
await notificationFireBaseProvider.initializeApp(
apiRest: apiRest,
utilsDataApiRest: UtilsDataApiRest(apiRest: apiRest),
idUsuario: event.usuarioAuth[EnvironmentApiRest.idServer],
coleccionAuth: Environment.origenColeccion,
);
debugPrint('🔔 notificationFireBaseProvider.initializeApp');
notificationFireBaseProvider.messagesStream.listen((mensaje) {
debugPrint('🔔 Mensaje Firebase recibido: $mensaje');
add(OnNuevaNotificacionFirebase(firebaseNotification: mensaje));
});
});
}
Future<void> _onNuevaNotificacionFirebase(
OnNuevaNotificacionFirebase event,
Emitter<ConfigAppState> emit,
) async {
await _handleAction(onNuevaNotificacionFirebase, emit, () async {
emit(state.copyWith(firebaseNotification: event.firebaseNotification));
});
}
Future<void> _onAnulaTokenFirebase(
OnAnulaTokenFirebase event,
Emitter<ConfigAppState> emit,
) async {
await _handleAction(onAnulaTokenFirebase, emit, () async {
if (!notificationFireBaseProvider.isInitialized) {
debugPrint(
'Push notifications no están inicializadas, no hay nada que anular.',
);
return;
}
await notificationFireBaseProvider.anularToken(
apiRest: apiRest,
utilsDataApiRest: UtilsDataApiRest(apiRest: apiRest),
);
});
}
Flujo Completo #
- Login: Cuando el usuario inicia sesión exitosamente, se dispara
OnInitPushNotificaciones - Inicialización: El BLoC inicializa el provider de notificaciones con el ID del usuario
- Escucha de Mensajes: Se suscribe al stream de mensajes y dispara
OnNuevaNotificacionFirebasecuando llega una notificación - Actualización de Estado: El evento actualiza el estado del BLoC con la nueva notificación
- Logout: Cuando el usuario cierra sesión, se dispara
OnAnulaTokenFirebaseque elimina el token y lo marca como bloqueado en MongoDB
Ventajas de este Patrón #
- ✅ Separación de Responsabilidades: La lógica de notificaciones está separada de la UI
- ✅ Testeable: Los eventos y estados pueden ser fácilmente testeados
- ✅ Mantenible: Cambios en la lógica de notificaciones no afectan otros componentes
- ✅ Escalable: Fácil agregar nuevos eventos o estados relacionados con notificaciones
- ✅ Reactivo: La UI se actualiza automáticamente cuando cambia el estado
Arquitectura Interna #
Flujo de Actualización de Token #
┌─────────────────┐
│ Inicialización │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Obtener Token │
└────────┬────────┘
│
▼
┌──────────────────────┐
│ Verificar Existencia │ ──► Si existe: Log y Retornar
└──────────┬───────────┘
│
▼ No existe
┌──────────────────────┐
│ Obtener Info Device │ ──┐
│ & App Version │ │ (Paralelo)
└──────────┬───────────┘ │
│ │
▼ ▼
┌──────────────────────────┐
│ Crear Documento MongoDB │
└──────────┬───────────────┘
│
▼
┌──────────────────────┐
│ Guardar en Servidor │
└──────────────────────┘
Licencia #
Este proyecto está bajo la licencia especificada en el archivo LICENSE.
Versión #
v3.0.0 - Versión estable para web con:
- ✅ Gestión automática de memoria
- ✅ Prevención de memory leaks
- ✅ Protección contra inicialización múltiple
- ✅ Manejo robusto de errores
- ✅ Actualización automática de tokens
- ✅ Integración con información del dispositivo
- ✅ Verificación de tokens existentes
- ✅ Configuración centralizada en
EnvironmentPushNotification - ✅ Escapado de consultas MongoDB para seguridad
- ✅ Performance optimizado con llamadas paralelas
- ✅ Método
getToken()para obtener token manualmente - ✅ Método
anularToken()para eliminar y bloquear tokens - ✅ Gestión de estados (activo/bloqueado) para tokens
- ✅ Historial de tokens con estado bloqueado
Changelog #
Ver CHANGELOG.md para el historial completo de cambios.