import 'dart:async'; import 'dart:developer'; import 'dart:io'; import 'dart:math' as math; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:hub/actions/actions.dart'; import 'package:hub/backend/api_requests/api_calls.dart'; import 'package:hub/components/templates_components/access_notification_modal_template_component/access_notification_modal_template_component_widget.dart'; import 'package:hub/components/templates_components/details_component/details_component_widget.dart'; import 'package:hub/components/templates_components/message_notificaion_modal_template_component/message_notification_widget.dart'; import 'package:hub/flutter_flow/flutter_flow_icon_button.dart'; import 'package:hub/flutter_flow/flutter_flow_theme.dart'; import 'package:hub/flutter_flow/flutter_flow_util.dart'; import 'package:hub/flutter_flow/flutter_flow_widgets.dart'; import 'package:rxdart/rxdart.dart'; class PushNotificationService { final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final BehaviorSubject _onMessage = BehaviorSubject(); final BehaviorSubject> _notificationDetails = BehaviorSubject>(); PushNotificationService() { _initializeFirebase(); _initializeLocalNotifications(); _createNotificationChannels(); } Subject getOnMessage() { return _onMessage; } Future _initializeFirebase() async { // await Firebase.initializeApp( // options: DefaultFirebaseOptions.currentPlatform); FirebaseMessaging.onBackgroundMessage((RemoteMessage message) => _firebaseMessagingBackgroundHandler( message, _flutterLocalNotificationsPlugin)); } Future initialize() async { await _requestPermissions(); WidgetsBinding.instance.addPostFrameCallback((_) { _listenToForegroundMessages(); _listenToBackgroundMessages(); _listenToNotificationClicks(); }); await updateDeviceToken(); } Future _requestPermissions() async { NotificationSettings settings = await _firebaseMessaging.requestPermission( alert: true, badge: true, sound: true, ); if (settings.authorizationStatus == AuthorizationStatus.authorized) { log('Notification permissions granted'); } else { log('Notification permissions denied'); } } static Map validJsonFromString(String? jsonString) { if (jsonString == null || jsonString.isEmpty) { return {}; } String correctedJson = jsonString.replaceAllMapped( RegExp(r'([a-zA-Z0-9_]+)\s*:\s*([^",\}\]]+)'), (match) { var key = '"${match[1]!}"'; var value = match[2]!.trim(); bool isStringValue = !RegExp(r'^-?\d+(\.\d+)?$').hasMatch(value) && value != 'true' && value != 'false' && value != 'null' && !value.startsWith('{') && !value.endsWith('}'); String quotedValue = isStringValue ? '"$value"' : value; return '$key: $quotedValue'; }); correctedJson = correctedJson.replaceAllMapped(RegExp(r'"{([^"]+)}"'), (match) { return '{${match[1]!}}'; }); try { return jsonDecode(correctedJson); } catch (e) { return {}; } } void _initializeLocalNotifications() async { try { while (AppState().context == null) { await Future.delayed(const Duration(milliseconds: 100)); } log("Context: ${AppState().context}"); var initializationSettingsAndroid = const AndroidInitializationSettings('mipmap/ic_fre_black'); var initializationSettingsIOS = const DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); var initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS, ); _flutterLocalNotificationsPlugin.initialize( initializationSettings, onDidReceiveBackgroundNotificationResponse: (response) => notificationTapBackground(response, _notificationDetails), onDidReceiveNotificationResponse: (response) => notificationTapBackground(response, _notificationDetails), ); } on Exception catch (e, s) { log("Error initializing local notifications: $e"); log(s.toString()); } } @pragma('vm:entry-point') static void notificationTapBackground( NotificationResponse response, _notificationDetails) async { log("Notification: ${response.payload}"); if (response.payload != null) { try { Map message = validJsonFromString(response.payload ?? ''); var data = _notificationDetails.value; _handleNotificationClick(message, extra: data); } catch (e) { log('Error handling background notification: $e'); } } } void _createNotificationChannels() { List actions = [ 'visit_request', 'visit_response', 'access', 'mensagem', 'enroll_cond', 'miscellaneous' ]; for (String action in actions) { _createNotificationChannel('channel_$action', "channel_$action"); } } void _createNotificationChannel(String channelId, String channelName) { final androidNotificationChannel = AndroidNotificationChannel( channelId, channelName, description: 'Channel for $channelName notifications', importance: Importance.max, ); AndroidFlutterLocalNotificationsPlugin() .createNotificationChannel(androidNotificationChannel) .catchError((error, stackTrace) { log('Error creating notification channel: $error'); log(stackTrace.toString()); }); } void _listenToForegroundMessages() { FirebaseMessaging.onMessage.listen((RemoteMessage message) { log("Message Foreground: ${message.data}"); _onMessage.add(message); _notificationDetails.add(message.toMap()['notification']); _showNotification(message); }).onError((error) { log("Error listening to foreground messages: $error"); }); } void _listenToBackgroundMessages() { FirebaseMessaging.onBackgroundMessage((RemoteMessage message) => _firebaseMessagingBackgroundHandler( message, _flutterLocalNotificationsPlugin)); } void _listenToNotificationClicks() { FirebaseMessaging.onMessageOpenedApp .listen((RemoteMessage message) => _firebaseMessagingBackgroundHandler) .onError((err) { log("Error listening to notification clicks: $err"); }); } void configureTokenRefresh() { _firebaseMessaging.onTokenRefresh .listen(_handleTokenUpdate) .onError((err) {}); } Future _updateToken(String token) async { AppState().token = token; final ApiCallResponse? response = await _updateTokenOnServer(token); if (_isTokenUpdateSuccessful(response)) { log('Token update successful'); } else { log('Token update failed'); } } Future _handleTokenUpdate(String newToken) async { 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) { await _updateToken(deviceToken); } else { log('Failed to get device token'); } } Future _requestNotificationPermission() async { final NotificationSettings settings = await _firebaseMessaging.requestPermission(); log(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(); log(apnsToken != null ? 'APNS Token: $apnsToken' : 'Failed to get APNS token'); } } Future _updateTokenOnServer(String deviceToken) async { return await PhpGroup.updToken.call( token: deviceToken, devid: AppState().devUUID, useruuid: AppState().userUUID, ); } bool _isTokenUpdateSuccessful(ApiCallResponse? response) { return PhpGroup.updToken.error((response?.jsonBody ?? '')) == false; } String _getChannelIdBasedOnClickAction(String clickAction) { final baseId = clickAction.hashCode; return 'channel_$baseId'; } Future _showNotification(RemoteMessage message) async { final String channelId = 'high_importance_channel'; final AndroidNotificationDetails androidDetails = AndroidNotificationDetails( channelId, 'High Importance Notifications', channelDescription: 'This channel is used for important notifications.', importance: Importance.high, priority: Priority.high, ); final NotificationDetails generalNotificationDetails = NotificationDetails(android: androidDetails); await _flutterLocalNotificationsPlugin.show( math.Random().nextInt(1 << 30), message.notification?.title, message.notification?.body, generalNotificationDetails, payload: jsonEncode(message.data), ); } static _handleNotificationClick(Map payload, {Map extra = const {}}) { log("Payload _handleNotificationClick: $payload"); switch (payload.isNotEmpty) { case true: NotificationHandler().handleMessage(payload, AppState().context, extra: extra.isEmpty ? {} : extra); break; case false: break; } } @pragma('vm:entry-point') static Future _firebaseMessagingBackgroundHandler(RemoteMessage message, FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin) async { await Firebase.initializeApp(); showFlutterNotification(message, _flutterLocalNotificationsPlugin); log('Handling a background message ${message.messageId}'); } static void showFlutterNotification(RemoteMessage message, FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin) { final RemoteNotification? notification = message.notification; final AndroidNotification? android = message.notification?.android; if (notification != null && android != null && !kIsWeb) { _flutterLocalNotificationsPlugin.show( notification.hashCode, notification.title, notification.body, NotificationDetails( android: AndroidNotificationDetails( 'high_importance_channel', 'High Importance Notifications', channelDescription: 'This channel is used for important notifications.', icon: 'launch_background', ), ), ); } } } class NotificationHandler { void handleMessage(Map message, BuildContext? context, {Map extra = const {}}) { message.forEach((key, value) {}); log("Message: $message"); log("Extra: $extra"); if (context != null) { switch (message['click_action']) { case 'visit_request': _showVisitRequestDialog(message, context); break; case '': break; case 'access': _showAcessNotificationModal(message, context); break; case 'mensagem': log("Extra Handle Message: $extra"); _showMessageNotificationDialog(message, context, extra); break; case 'enroll_cond': break; default: } } } String _getIdBasedOnUserType(Map message) { if (message['USR_TIPO'].toString() == 'O') { return message['USR_ID'].toString().isEmpty ? '0' : message['USR_ID'].toString(); } else { return message['USR_DOCUMENTO'].toString().isEmpty ? '0' : message['USR_DOCUMENTO'].toString(); } } void _showAcessNotificationModal( Map message, BuildContext context) { showDialog( context: context, builder: (BuildContext context) { _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) { showDialog( useSafeArea: true, barrierDismissible: true, context: context, builder: (BuildContext context) { String localId = ''; try { localId = jsonDecode(message['local'])['CLI_ID']; } catch (e) { localId = message['local']['CLI_ID'].toString(); } log("Mensagem: $message"); log("Extra: $extra"); return GestureDetector( onTap: () => Navigator.of(context).pop(), child: SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: Dialog( backgroundColor: Colors.transparent, child: GestureDetector( onTap: () => Navigator.of(context).pop(), child: SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: MessageNotificationModalTemplateComponentWidget( id: localId, from: message['remetente'].toString(), to: message['destinatario'].toString() == 'O' ? 'Morador' : 'Visitante', message: extra['body'].toString().isEmpty ? 'Unknown' : extra['body'].toString(), ), ), ), ), ), ); }, ); } void _showVisitRequestDialog( Map message, BuildContext context) { showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { _getIdBasedOnUserType(message); return Dialog( backgroundColor: Colors.transparent, child: DetailsComponentWidget( buttons: [ FlutterFlowIconButton( icon: const Icon(Icons.done), onPressed: () async { showDialog( context: context, builder: (context) { return AlertDialog( title: Text( FFLocalizations.of(context).getVariableText( ptText: 'Aprovar Visita', enText: 'Approve Visit', ), ), content: Text( FFLocalizations.of(context).getVariableText( ptText: 'Você tem certeza que deseja aprovar essa visita?', enText: 'Are you sure you want to approve this visit?', ), ), backgroundColor: FlutterFlowTheme.of(context).primaryBackground, actions: [ FFButtonWidget( text: FFLocalizations.of(context).getVariableText( enText: 'No', ptText: 'Não', ), onPressed: () { Navigator.pop(context); }, options: FFButtonOptions( width: 100, height: 40, color: FlutterFlowTheme.of(context) .primaryBackground, textStyle: TextStyle( color: FlutterFlowTheme.of(context) .primaryText, ), borderSide: BorderSide( color: FlutterFlowTheme.of(context) .primaryBackground, width: 1, ), borderRadius: BorderRadius.circular(10)), ), FFButtonWidget( text: FFLocalizations.of(context).getVariableText( enText: 'Yes', ptText: 'Sim', ), onPressed: () async { await answersRequest.call( context, message['referencia'].toString(), 'L', 'Mensagem', message['idVisitante'].toString(), ); }, options: FFButtonOptions( width: 100, height: 40, color: FlutterFlowTheme.of(context) .primaryBackground, textStyle: TextStyle( color: FlutterFlowTheme.of(context).primaryText, ), borderSide: BorderSide( color: FlutterFlowTheme.of(context) .primaryBackground, width: 1, ), borderRadius: BorderRadius.circular(10), ), ), ], ); }); }, ), FlutterFlowIconButton( icon: const Icon(Icons.close), onPressed: () async { showAlertDialog( context, FFLocalizations.of(context).getVariableText( ptText: 'Bloquear Visita', enText: 'Block Visit', ), FFLocalizations.of(context).getVariableText( ptText: 'Você tem certeza que deseja bloquear essa visita?', enText: 'Are you sure you want to block this visit?', ), () async { await answersRequest.call( context, message['referencia'].toString(), 'B', 'Mensagem', message['idVisitante'].toString(), ); }); }, ), ], labelsHashMap: Map.from({ FFLocalizations.of(context).getVariableText( enText: 'Visitor', ptText: 'Visitante', ): message['nomevisita'], FFLocalizations.of(context).getVariableText( enText: 'Reason', ptText: 'Motivo', ): message['motivo'], FFLocalizations.of(context).getVariableText( enText: 'Message', ptText: 'Mensagem', ): message['mensagem'], }), imagePath: 'https://freaccess.com.br/freaccess/getImage.php?cliID=${AppState().cliUUID}&atividade=getFoto&Documento=${message['documento'] ?? ''}&tipo=E', statusHashMap: [ { FFLocalizations.of(context).getVariableText( enText: 'Active', ptText: 'Ativo', ): FlutterFlowTheme.of(context).warning, }, ], ), ); }, ); } } 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(); } }