Módulo: Notificaciones
Scope
| Tipo de organización | Acceso |
|---|---|
| Empresa | ✅ |
| Agencia | ✅ |
| Reclutador independiente | ✅ |
Disponible para los tres tipos de organización. No requiere permisos especiales — cada usuario ve sus propias notificaciones.
Permisos por rol
Empresa (COMPANY)
| Acción | Disponible | Permiso |
|---|---|---|
| Ver notificaciones | ✅ | Autenticado |
| Marcar como leída | ✅ | Autenticado |
| Marcar todas leídas | ✅ | Autenticado |
| Gestionar preferencias | ✅ | Autenticado |
| Recibir push notifications | ✅ | Token FCM registrado |
Agencia (AGENCY)
| Acción | Disponible | Permiso |
|---|---|---|
| Ver notificaciones | ✅ | Autenticado |
| Marcar como leída | ✅ | Autenticado |
| Marcar todas leídas | ✅ | Autenticado |
| Gestionar preferencias | ✅ | Autenticado |
| Recibir push notifications | ✅ | Token FCM registrado |
Reclutador independiente (RECRUITER)
| Acción | Disponible | Permiso |
|---|---|---|
| Ver notificaciones | ✅ | Autenticado |
| Marcar como leída | ✅ | Autenticado |
| Marcar todas leídas | ✅ | Autenticado |
| Gestionar preferencias | ✅ | Autenticado |
| Recibir push notifications | ✅ | Token FCM registrado |
¿Qué es?
El módulo de Notificaciones gestiona las alertas y comunicaciones para los usuarios dentro de la plataforma. Incluye:
- Inbox de notificaciones — Lista paginada de notificaciones in-app con estados leída/no leída
- Push notifications — Alertas del sistema vía Firebase Cloud Messaging (FCM)
- Notificaciones locales — Mostrar notificaciones cuando la app está en foreground
- Preferencias — Control granular de canales y categorías de notificación
¿Qué puede hacer el usuario?
Inbox
- Ver lista paginada de notificaciones con infinite scroll
- Ver badge de conteo de no leídas en el AppBar del home
- Marcar una notificación como leída (tap)
- Marcar todas las notificaciones como leídas
- Ver título y cuerpo derivados del payload (localizado)
- Distinguir tipo de notificación por icono y color
Push Notifications
- Recibir push cuando la app está en background o terminada
- Ver notificaciones como local notification cuando la app está en foreground
- Tap en notificación abre la app (callback configurado)
- Token FCM se registra automáticamente al iniciar la app
- Token se refresca automáticamente si cambia
Preferencias
- Activar/desactivar canales (push, email, in-app)
- Activar/desactivar categorías (vacantes, candidatos, disputas, pagos, sistema)
- Cambios se persisten optimistamente con rollback en error
Flujo del usuario
1. Accede a notificaciones desde el icono (campana) en el AppBar del home
2. Ve la lista de notificaciones con badge de no leídas
3. Toca una notificación no leída → se marca como leída
4. Usa "Leer todo" → marca todas como leídas
5. Hace scroll al fondo → carga siguiente página (infinite scroll)
6. Recibe push notification → aparece en system tray (background) o local notification (foreground)
7. Toca push notification → abre la app con callback
8. Accede a preferencias desde /notifications/preferences
9. Activa/desactiva switches → se guarda automáticamente
Reglas de negocio
- Las notificaciones están scoped por
userId+organizationId readAtrepresenta el estado de lectura del usuario,statusrepresenta dispatch (PENDING, SENT, FAILED, CANCELLED)- Las notificaciones incluyen
display: { title, body }localizado según el locale del usuario - El badge muestra count de no leídas (máximo "99+")
- Push tokens se registran al login y se desactivan al logout
- Token refresh de FCM se propaga automáticamente al backend
- Los tipos de notificación determinan icono y color en la UI:
vacanc*→ work icon, secondary colorcandidate*→ people icon, accent colordispute*→ gavel icon, error colorsupport*→ support agent icon, warning colorbilling*/payout*→ wallet icon, success colorregistration*/validation*→ verified icon, primary colortest_push*→ notifications active icon- Otros → notifications icon, primary color
Restricciones
| Permitido | No permitido |
|---|---|
| Leer propias notificaciones | Ver notificaciones de otro usuario |
| Marcar propias como leídas | Modificar estado de dispatch |
| Gestionar propias preferencias | Acceder preferencias de otros |
| Registrar/desactivar propio token | Modificar tokens de otros |
Arquitectura
Sigue Clean Architecture con estructura feature-first:
lib/features/notifications/
├── application/
│ ├── notification_preferences_cubit.dart
│ ├── notification_preferences_state.dart
│ ├── notifications_cubit.dart
│ └── notifications_state.dart
├── data/
│ ├── datasources/
│ │ └── notifications_remote_datasource.dart
│ ├── models/
│ │ ├── notification_model.dart
│ │ ├── notification_preferences_model.dart
│ │ └── push_device_token_model.dart
│ └── repositories/
│ └── notifications_repository_impl.dart
├── domain/
│ ├── entities/
│ │ ├── notification_entity.dart
│ │ ├── notification_preferences_entity.dart
│ │ └── push_device_token_entity.dart
│ ├── repositories/
│ │ └── notifications_repository.dart
│ └── usecases/
│ ├── deactivate_push_token_usecase.dart
│ ├── get_notification_preferences_usecase.dart
│ ├── get_unread_count_usecase.dart
│ ├── list_notifications_usecase.dart
│ ├── mark_all_notifications_read_usecase.dart
│ ├── mark_notification_read_usecase.dart
│ ├── register_push_token_usecase.dart
│ └── update_notification_preferences_usecase.dart
└── presentation/
├── pages/
│ ├── notification_preferences_page.dart
│ └── notifications_page.dart
└── widgets/
├── notification_card.dart
├── notifications_empty_view.dart
├── notifications_error_view.dart
└── notifications_list_view.dart
Core Services (lib/core/services/)
├── local_notification_service.dart # flutter_local_notifications wrapper
└── push_notification_service.dart # Firebase Cloud Messaging wrapper
Flujo de datos
UI (Widget) → Cubit → UseCase → Repository → RemoteDatasource → API
Push notifications siguen un flujo diferente:
FCM → PushNotificationService → LocalNotificationService (foreground)
→ onNotificationTap callback (tap)
→ onTokenReceived callback → RegisterPushTokenUseCase → API
Entidades
NotificationEntity
| Campo | Tipo | Descripción |
|---|---|---|
| id | String | UUID de la notificación |
| organizationId | String? | Organización del usuario |
| userId | String | Usuario destinatario |
| type | String | Tipo de notificación (vacancy.published, candidate.submitted, etc.) |
| channel | String | Canal (IN_APP, PUSH, EMAIL) |
| status | String | Estado de dispatch (PENDING, SENT, FAILED, CANCELLED) |
| payload | Map<String, dynamic> | Datos del evento + display localizado |
| isRead | bool | Si fue leída por el usuario |
| createdAt | DateTime | Fecha de creación |
| sentAt | DateTime? | Fecha de envío |
| readAt | DateTime? | Fecha de lectura |
Helpers:
displayTitle→ extrae título del payload (payload.displayTitleopayload.display.title)displayBody→ extrae cuerpo del payload (payload.display.bodyopayload.displayBody)
NotificationPreferencesEntity
| Campo | Tipo | Descripción |
|---|---|---|
| pushEnabled | bool | Push notifications habilitadas |
| emailEnabled | bool | Email notifications habilitadas |
| inAppEnabled | bool | In-app notifications habilitadas |
| vacancyUpdates | bool | Notificaciones de vacantes |
| candidateUpdates | bool | Notificaciones de candidatos |
| disputeUpdates | bool | Notificaciones de disputas |
| billingUpdates | bool | Notificaciones de pagos/wallet |
| systemUpdates | bool | Notificaciones de sistema |
PushDeviceTokenEntity
| Campo | Tipo | Descripción |
|---|---|---|
| id | String | UUID del registro |
| organizationId | String? | Organización |
| userId | String | Usuario propietario |
| platform | String | Plataforma (android, ios) |
| isActive | bool | Si el token está activo |
| lastSeenAt | DateTime | Última actividad |
| createdAt | DateTime | Fecha de registro |
| updatedAt | DateTime | Última actualización |
| deviceId | String? | Identificador del dispositivo |
| appVersion | String? | Versión de la app |
| locale | String? | Idioma del dispositivo |
| disabledAt | DateTime? | Fecha de desactivación |
UseCases
| UseCase | Parámetros | Retorno | Descripción |
|---|---|---|---|
| ListNotificationsUseCase | page, pageSize, search, sort, order, unread | PaginatedResponse<NotificationEntity> | Lista notificaciones paginadas |
| GetUnreadCountUseCase | — | int | Conteo de no leídas |
| MarkNotificationReadUseCase | notificationId | NotificationEntity | Marca una como leída |
| MarkAllNotificationsReadUseCase | — | int (updatedCount) | Marca todas como leídas |
| RegisterPushTokenUseCase | token, platform, deviceId?, appVersion?, locale? | PushDeviceTokenEntity | Registra token FCM |
| DeactivatePushTokenUseCase | tokenId | PushDeviceTokenEntity | Desactiva token FCM |
| GetNotificationPreferencesUseCase | — | NotificationPreferencesEntity | Obtiene preferencias |
| UpdateNotificationPreferencesUseCase | NotificationPreferencesEntity | NotificationPreferencesEntity | Actualiza preferencias |
Estados
NotificationsState
enum NotificationsStatus { initial, loading, success, error }
class NotificationsState {
final NotificationsStatus status;
final List<NotificationEntity> notifications;
final PageMeta? meta;
final int unreadCount;
final int currentPage;
final bool isLoadingMore;
final String? errorMessage;
final String? pushTokenId;
}
NotificationPreferencesState
enum NotificationPreferencesStatus { initial, loading, success, error }
class NotificationPreferencesState {
final NotificationPreferencesStatus status;
final NotificationPreferencesEntity? preferences;
final bool isSaving;
final String? errorMessage;
}
Endpoints API
Notificaciones (sin prefijo /api)
| Método | Endpoint | Descripción |
|---|---|---|
| GET | /notifications | Listar notificaciones (paginado, filtros) |
| GET | /notifications/unread-count | Conteo de no leídas |
| PATCH | /notifications/{id}/read | Marcar como leída |
| PATCH | /notifications/read-all | Marcar todas como leídas |
Push tokens
| Método | Endpoint | Descripción |
|---|---|---|
| POST | /notifications/push-tokens | Registrar token FCM |
| DELETE | /notifications/push-tokens/{id} | Desactivar token |
Preferencias
| Método | Endpoint | Descripción |
|---|---|---|
| GET | /notifications/preferences | Obtener preferencias |
| PATCH | /notifications/preferences | Actualizar preferencias |
Test (desarrollo)
| Método | Endpoint | Descripción |
|---|---|---|
| POST | /notifications/test-push | Enviar push de prueba |
Navegación (go_router)
| Ruta | Nombre | Página |
|---|---|---|
/notifications | notifications | NotificationsPage |
/notifications/preferences | notificationPreferences | NotificationPreferencesPage |
El HomeAppBar navega con context.push('/notifications').
Widgets principales
| Widget | Responsabilidad |
|---|---|
NotificationsPage | Entry point con BlocProvider y vista principal |
NotificationsListView | Lista con infinite scroll y paginación |
NotificationCard | Tarjeta individual con icono, color por tipo, time ago |
NotificationsEmptyView | Estado vacío con icono y mensaje |
NotificationsErrorView | Estado error con retry |
NotificationPreferencesPage | Gestión de preferencias con switches |
Core Services
| Servicio | Responsabilidad |
|---|---|
PushNotificationService | FCM: permisos, token, foreground/background messages, tap |
LocalNotificationService | flutter_local_notifications: mostrar en foreground |
Dependencias
flutter_bloc— State management (Cubit pattern)firebase_messaging— Push notifications vía FCMflutter_local_notifications— Mostrar notificaciones en foregroundintl— Formateo de fechas relativasdio— HTTP client (via ApiClient centralizado)
Registro DI (get_it)
- Core services:
LazySingleton(PushNotificationService, LocalNotificationService) - Datasource:
LazySingleton - Repository:
LazySingleton - UseCases:
LazySingleton(8 use cases) - NotificationsCubit:
Factory(nueva instancia por pantalla) - NotificationPreferencesCubit:
Factory(nueva instancia por pantalla)
Manejo de errores
- Datasource — deja propagar
DioExceptionsin catch - Repository — captura
DioExceptionyAppException, mapea aFailuretipados viaNetworkErrorHandler - Cubit — emite
errorMessageen el estado - UI —
BlocBuildermuestra estado de error con botón retry
Paginación (Infinite Scroll)
NotificationListener<ScrollNotification>detecta scroll a 200px del fondo- Dispara
loadMore()en el cubit currentPagese incrementa y se acumulan itemshasNextPagese deriva delPageMetadel backend- Loader circular al pie mientras carga
- Guard con
isLoadingMorepreviene peticiones duplicadas
Formateo de tiempo relativo
Usa localización (no strings hardcodeados):
< 1 min→context.l10n.notificationsTimeNow< 60 min→context.l10n.notificationsTimeMinutes(n)< 24h→context.l10n.notificationsTimeHours(n)< 7d→context.l10n.notificationsTimeDays(n)≥ 7d→DateFormat('dd MMM', 'es').format(date)
Localización
Todas las cadenas visibles usan context.l10n.keyName. Las claves relevantes están en lib/l10n/app_es.arb y lib/l10n/app_en.arb con prefijos notifications* y notificationPreferences*.
Keys principales
| Key | ES | EN |
|---|---|---|
| notificationsTitle | Notificaciones | Notifications |
| notificationsMarkAllRead | Leer todo | Read all |
| notificationsEmpty | Sin notificaciones | No notifications |
| notificationsEmptyMessage | Aquí aparecerán tus notificaciones | Your notifications will appear here |
| notificationsTimeNow | Ahora | Now |
| notificationsTimeMinutes | Hace {minutes} min | {minutes} min ago |
| notificationsTimeHours | Hace {hours}h | {hours}h ago |
| notificationsTimeDays | Hace {days}d | {days}d ago |
| notificationPreferencesTitle | Preferencias de notificaciones | Notification preferences |
| notificationPreferencesChannels | CANALES | CHANNELS |
| notificationPreferencesCategories | CATEGORÍAS | CATEGORIES |