import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:f_r_e_hub/actions/actions.dart'; import 'package:f_r_e_hub/app_state.dart'; import 'package:f_r_e_hub/backend/api_requests/api_calls.dart'; import 'package:f_r_e_hub/components/templates_components/access_notification_modal_template_component/access_notification_modal_template_component_widget.dart'; import 'package:f_r_e_hub/components/templates_components/message_notificaion_modal_template_component/message_notification_widget.dart'; import 'package:f_r_e_hub/components/templates_components/visit_request_template_component/visit_request_template_component_widget.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:rxdart/rxdart.dart'; class PushNotificationService { final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final Subject _onMessage = BehaviorSubject(); final BehaviorSubject _context = BehaviorSubject(); final BehaviorSubject> _notificationDetails = BehaviorSubject>(); PushNotificationService() { _initializeLocalNotifications(_context); _createNotificationChannels(); } Subject getOnMessage() { return _onMessage; } Future initialize(BuildContext context) async { _context.add(context); await _requestPermissions(); _listenToForegroundMessages(context); _listenToBackgroundMessages(); _listenToNotificationClicks(context); await updateDeviceToken(); } Future _requestPermissions() async { NotificationSettings settings = await _firebaseMessaging.requestPermission( alert: true, badge: true, sound: true, ); if (settings.authorizationStatus == AuthorizationStatus.authorized) { debugPrint('User granted permission'); } else { debugPrint('User declined or has not accepted permission'); } } Map validJsonFromString(String? jsonString) { if (jsonString == null || jsonString.isEmpty) { return {}; } // Passo 1 e 2: Adiciona aspas duplas em torno das chaves e valores que não estão corretamente delimitados String correctedJson = jsonString.replaceAllMapped( RegExp(r'([a-zA-Z0-9_]+)\s*:\s*([^",\}\]]+)'), (match) { var key = '"${match[1]!}"'; // Chaves sempre recebem aspas var value = match[2]!.trim(); // Verifica se o valor é uma string (não numérica, booleana, nula ou objeto JSON) bool isStringValue = !RegExp(r'^-?\d+(\.\d+)?$').hasMatch(value) && value != 'true' && value != 'false' && value != 'null' && !value.startsWith('{') && !value.endsWith('}'); // Adiciona aspas duplas em torno do valor se for uma string String quotedValue = isStringValue ? '"$value"' : value; return '$key: $quotedValue'; }); // Passo 3: Tratar corretamente strings JSON aninhadas correctedJson = correctedJson.replaceAllMapped(RegExp(r'"{([^"]+)}"'), (match) { // Remove as aspas duplas extras em torno de objetos JSON aninhados return '{${match[1]!}}'; }); try { // Passo 4: Decodificar o JSON corrigido return jsonDecode(correctedJson); } catch (e) { print('Error decoding JSON: $e'); return {}; } } void _initializeLocalNotifications( BehaviorSubject context) async { while (context.valueOrNull == null) { await Future.delayed(Duration(milliseconds: 100)); } var initializationSettingsAndroid = AndroidInitializationSettings('mipmap/ic_fre_black'); var initializationSettingsIOS = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); var initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS, ); _flutterLocalNotificationsPlugin.initialize( initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse response) async { debugPrint('Response payload:${response.payload}'); if (response.payload != null) { try { Map message = validJsonFromString(response.payload!); debugPrint('Notification payload: $message'); var data = _notificationDetails; // Assuming getOnMessage() now returns the latest RemoteMessage debugPrint('Extra: ${data.value}'); _handleNotificationClick(message, extra: data.value ?? {'extra': 'extra'}); } catch (e) { debugPrint('Error decoding notification payload: $e'); } } }, ); } void _createNotificationChannels() { List actions = [ 'visit_request', 'visit_response', 'access', 'mensagem', 'enroll_cond', 'miscellaneous' ]; for (String action in actions) { _createNotificationChannel(action, "Channel for $action"); } } void _createNotificationChannel(String channelId, String channelName) { _flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel( AndroidNotificationChannel( channelId, // Use o click_action como ID do canal channelName, // Nome descritivo baseado no click_action description: 'Channel for $channelName notifications', importance: Importance.max, ), ); } void _listenToForegroundMessages(BuildContext context) { FirebaseMessaging.onMessage.listen((RemoteMessage message) { debugPrint('Got a message whilst in the foreground!'); debugPrint('Message data: ${message.toMap()}'); _onMessage.add(message); _notificationDetails.add(message.toMap()['notification']); _showNotification(message); }); } void _listenToBackgroundMessages() { FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); } void _listenToNotificationClicks(BuildContext context) { FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { debugPrint('Notification clicked!'); _onMessage.add(message); debugPrint('Extra: ${message.notification?.body}'); NotificationHandler().handleMessage(message.data, context); }); } void configureTokenRefresh() { _firebaseMessaging.onTokenRefresh.listen(_handleTokenUpdate).onError((err) { debugPrint("Error refreshing token: $err"); }); } Future _updateToken(String token) async { FFAppState().token = token; final ApiCallResponse? response = await _updateTokenOnServer(token); if (_isTokenUpdateSuccessful(response)) { debugPrint('Token updated successfully on server. Token: $token'); } else { debugPrint('Error updating token on server'); } } Future _handleTokenUpdate(String newToken) async { debugPrint('Token refreshed: $newToken'); await _updateToken(newToken); } Future updateDeviceToken() async { configureTokenRefresh(); final NotificationSettings settings = await _requestNotificationPermission(); if (Platform.isIOS) await _fetchAndLogApnsToken(settings); final String? deviceToken = await _firebaseMessaging.getToken(); if (deviceToken != null) { debugPrint('Push Messaging token: $deviceToken'); await _updateToken(deviceToken); } else { debugPrint('Failed to get Firebase Messaging token'); } } Future _requestNotificationPermission() async { final NotificationSettings settings = await _firebaseMessaging.requestPermission(); debugPrint(settings.authorizationStatus == AuthorizationStatus.authorized ? 'User granted permission' : 'User declined or has not accepted permission'); return settings; } Future _fetchAndLogApnsToken(NotificationSettings settings) async { if (settings.authorizationStatus == AuthorizationStatus.authorized) { final String? apnsToken = await _firebaseMessaging.getAPNSToken(); debugPrint(apnsToken != null ? 'APNS Token: $apnsToken' : 'Failed to get APNS token'); } } Future _updateTokenOnServer(String deviceToken) async { return await PhpGroup.updToken.call( token: deviceToken, devid: FFAppState().devUUID, useruuid: FFAppState().userUUID, ); } bool _isTokenUpdateSuccessful(ApiCallResponse? response) { return PhpGroup.updToken.error((response?.jsonBody ?? '')) == false; } String _getChannelIdBasedOnClickAction(String clickAction) { final baseId = clickAction.hashCode; return 'channel_$baseId'; } void _showNotification(RemoteMessage message) async { String channelId = _getChannelIdBasedOnClickAction(message.data['click_action']); var androidDetails = AndroidNotificationDetails( channelId, 'Channel Name for $channelId', channelDescription: 'Channel Description for $channelId', importance: Importance.max, priority: Priority.high, ); var iOSDetails = DarwinNotificationDetails(); var generalNotificationDetails = NotificationDetails(android: androidDetails, iOS: iOSDetails); debugPrint('Showing notification: ${message.messageId.hashCode}'); await _flutterLocalNotificationsPlugin.show( // DateTime.now().millisecondsSinceEpoch % (1 << 31), Random().nextInt(1 << 30), message.notification?.title, message.notification?.body, generalNotificationDetails, payload: message.data.toString(), ); } _handleNotificationClick(Map payload, {Map extra = const {}}) { switch (payload.isNotEmpty) { case true: // Print the 'data' property debugPrint('Notification payload: $payload'); debugPrint('Extra: $extra'); // Handle the message data as needed NotificationHandler().handleMessage(payload, _context.value, extra: extra.isEmpty ? {} : extra); // Access the 'data' property of 'RemoteMessage' case false: debugPrint('Notification payload is empty'); // Handle the message notification as needed break; } } static Future _firebaseMessagingBackgroundHandler( RemoteMessage message) async { debugPrint('Handling a background message: ${message.messageId}'); } } class NotificationHandler { void handleMessage(Map message, BuildContext context, {Map extra = const {}}) { debugPrint('Notification Received!'); message.forEach((key, value) { debugPrint('$key: $value'); }); switch (message['click_action']) { case 'visit_request': _showVisitRequestDialog(message, context); break; case '': break; case 'access': _showAcessNotificationModal(message, context, extra); break; case 'mensagem': _showMessageNotificationDialog(message, context, extra); break; case 'enroll_cond': debugPrint('enroll_cond'); break; default: debugPrint('Notification type not recognized'); } } String _getIdBasedOnUserType(Map message) { if (message['USR_TIPO'].toString() == 'O') { // Retorna USR_ID se não estiver vazio/nulo, caso contrário retorna '0' return message['USR_ID'].toString().isEmpty ? '0' : message['USR_ID'].toString(); } else { // Retorna USR_DOCUMENTO se não estiver vazio/nulo, caso contrário retorna '0' return message['USR_DOCUMENTO'].toString().isEmpty ? '0' : message['USR_DOCUMENTO'].toString(); } } void _showAcessNotificationModal( Map message, BuildContext context, Map extra) { debugPrint('Showing access notification dialog'); debugPrint('USR_TIPO: ${message['USR_TIPO']}'); debugPrint('USR_ID: ${message['USR_ID']}'); debugPrint('USR_DOCUMENTO: ${message['USR_DOCUMENTO']}'); showDialog( context: context, builder: (BuildContext context) { String id = _getIdBasedOnUserType(message); return Dialog( backgroundColor: Colors.transparent, child: AccessNotificationModalTemplateComponentWidget( datetime: message['ACE_DATAHORA'].toString(), drive: message['ACI_DESCRICAO'].toString(), id: message['USR_TIPO'].toString() == 'O' ? message['USR_ID'].toString() == '' ? '0' : message['USR_ID'].toString() : message['USR_DOCUMENTO'].toString() == '' ? '0' : message['USR_DOCUMENTO'].toString(), name: message['PES_NOME'].toString(), type: message['USR_TIPO'], )); }, ); } void _showMessageNotificationDialog( Map message, BuildContext context, Map extra) { debugPrint('Showing message notification dialog'); showDialog( context: context, builder: (BuildContext context) { return Dialog( backgroundColor: Colors.transparent, child: MessageNotificationModalTemplateComponentWidget( id: message['local']['CLI_ID'].toString(), from: message['remetente'].toString(), to: message['destinatario'].toString() == 'O' ? 'Morador' : 'Visitante', message: extra['body'].toString(), ), ); }, ); } void _showVisitRequestDialog( Map message, BuildContext context) { debugPrint('Showing visit request notification dialog'); showDialog( context: context, builder: (BuildContext context) { String id = _getIdBasedOnUserType(message); return Dialog( backgroundColor: Colors.transparent, child: VisitRequestTemplateComponentWidget( vteName: message['mensagem'] ?? 'Unknown', vteReason: message['motivo'] ?? 'Unknown', vteMsg: message['mensagem'] ?? 'Unknown', vteDocument: message['documento'] ?? '', vteUUID: message['idVisitante'].toString() ?? '0', vawRef: message['referencia'].toString() ?? '0', vawStatus: 'S', changeStatusAction: changeStatusAction, ), ); }, ); } } class PushNotificationManager { final StreamController _onMessageReceivedController = StreamController.broadcast(); Stream get onMessageReceived => _onMessageReceivedController.stream; PushNotificationManager() { FirebaseMessaging.onMessage.listen((RemoteMessage message) { _onMessageReceivedController.add(message); }); } void dispose() { _onMessageReceivedController.close(); } }