diff --git a/lib/backend/push_notification/push_notification.dart b/lib/backend/push_notification/push_notification.dart index a16c20a7..de4eefa7 100644 --- a/lib/backend/push_notification/push_notification.dart +++ b/lib/backend/push_notification/push_notification.dart @@ -1,157 +1,181 @@ -import 'dart:convert'; +import 'dart:async'; import 'dart:developer'; +import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:hub/app_state.dart'; -import 'package:hub/backend/api_requests/api_calls.dart'; -import 'package:hub/backend/api_requests/api_manager.dart'; -import 'package:hub/components/templates_components/message_notificaion_modal_template_component/message_notification_widget.dart'; -import 'package:hub/shared/utils/log_util.dart'; +import 'package:hub/firebase_options.dart'; +class NotificationService { + // Singleton instance + static final NotificationService _instance = NotificationService._internal(); -class PushNotification { + // Factory constructor + factory NotificationService() => _instance; - static final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; - static final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + // Private constructor + NotificationService._internal(); + + static final FirebaseMessaging _firebaseMessaging = + FirebaseMessaging.instance; + static final FlutterLocalNotificationsPlugin + _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + static bool _initialized = false; Future initialize() async { - await _requestPermissions(); + if (_initialized) return; + _initialized = true; - // Mensagens - _backgroundMessage(); - _forgroundMessage(); - _openMessage(); + try { + await _requestPermissions(); + await _initializeLocalNotifications(); - // Token - _refreshToken(); - _updateDeviceToken(); + FirebaseMessaging.onBackgroundMessage( + _firebaseMessagingBackgroundHandler); + + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + _printNotification(message, 'onMessage'); + }); + + FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { + _printNotification(message, 'onMessageOpenedApp'); + }); + } catch (e) { + log('Error initializing notification manager: $e'); + } } - Future _updateDeviceToken() async { - try { - final String? deviceToken = await _firebaseMessaging.getToken(); + Future _printNotification(RemoteMessage message, String type) async { + log('Notification $type: ${message.messageId}'); + // Handle the notification display logic here + } - if (deviceToken != null) { - AppState().token = deviceToken; + Future _initializeLocalNotifications() async { + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/ic_launcher'); + final DarwinInitializationSettings initializationSettingsIOS = + DarwinInitializationSettings( + onDidReceiveLocalNotification: + (int id, String? title, String? body, String? payload) async { + // Handle the notification received in foreground + }, + ); + final InitializationSettings initializationSettings = + InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + ); - final ApiCallResponse? response = await PhpGroup.updToken.call(token: AppState().token, devid: AppState().devUUID, useruuid: AppState().userUUID); - - if (PhpGroup.updToken.error((response?.jsonBody ?? '')) == false) { - log('Token Atualizado com Sucesso!'); - } else { - log('Falha ao Atualizar Token: ${response?.jsonBody}'); - } - - } else { - log('Falha ao Pegar Token do Firebase'); - } - } catch (e, s) { - LogUtil.requestAPIFailed("updToken.php", "", "Atualizar Token", e, s); - } + await _flutterLocalNotificationsPlugin.initialize(initializationSettings, + onDidReceiveNotificationResponse: (payload) async => + log('Notification tapped: $payload'), + onDidReceiveBackgroundNotificationResponse: (payload) async => + log('Notification tapped in background: $payload')); } Future _requestPermissions() async { NotificationSettings settings = await _firebaseMessaging.requestPermission( - alert: true, - badge: true, - sound: true, - provisional: true, - criticalAlert: false, - carPlay: false, - announcement: false + alert: true, + announcement: false, + badge: true, + carPlay: false, + criticalAlert: false, + provisional: false, + sound: true, ); - log('Permisão de Notificação: ${settings.authorizationStatus == AuthorizationStatus.authorized ? "Liberado" : "Negado"}'); + + log('User granted permission: ${settings.authorizationStatus}'); } - void _backgroundMessage() { - FirebaseMessaging.onBackgroundMessage((RemoteMessage message) => _printNotification(message, 'background')); - } - - void _forgroundMessage() { - FirebaseMessaging.onMessage.listen((RemoteMessage message) => _printNotification(message, 'forground')); - } - - void _openMessage() { - FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) => _processNotification(message)); - } - - void _refreshToken() { - _firebaseMessaging.onTokenRefresh.listen((token) => log('Novo Token: $token')); - } - - Future _printNotification(RemoteMessage message, String type) async { - print("Tipo da Notificação: $type"); - print("Mensagem: $message"); - } - - Future _processNotification(RemoteMessage message) async { - - switch (message.category) { - case 'mensagem': - _showMessageNotificationDialog(message.data, AppState().context!, message.notification?.body ?? ''); + Future subscribeToTopic(String topic) async { + try { + await _firebaseMessaging.subscribeToTopic(topic); + log('Subscribed to topic: $topic'); + } catch (e) { + log('Error subscribing to topic $topic: $e'); } - } - void _showMessageNotificationDialog(Map message, BuildContext context, String 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(); - } - - 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.isEmpty ? 'Unknown' : extra.toString(), - ), - ), - ), - ), - ), - ); - }, - ); + Future unsubscribeFromTopic(String topic) async { + try { + await _firebaseMessaging.unsubscribeFromTopic(topic); + log('Unsubscribed from topic: $topic'); + } catch (e) { + log('Error unsubscribing from topic $topic: $e'); + } } +} +@pragma('vm:entry-point') +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await setupFlutterNotifications(); + showFlutterNotification(message); + // If you're going to use other Firebase services in the background, such as Firestore, + // make sure you call `initializeApp` before using other Firebase services. + log('Handling a background message ${message.messageId}'); +} - Future _showMessage(RemoteMessage message) async { - print("Dados: ${message.data}"); - print("From: ${message.from}"); - print("Notification: ${message.notification?.body}"); - print("Category: ${message.category}"); +/// Create a [AndroidNotificationChannel] for heads up notifications +late AndroidNotificationChannel channel; - showDialog( - context: AppState().context!, - builder: (context) => Dialog( - child: Container( - child: Text("Notificação"), +bool isFlutterLocalNotificationsInitialized = false; + +Future setupFlutterNotifications() async { + if (isFlutterLocalNotificationsInitialized) { + return; + } + channel = const AndroidNotificationChannel( + 'high_importance_channel', // id + 'High Importance Notifications', // title + description: + 'This channel is used for important notifications.', // description + importance: Importance.high, + ); + + flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + + /// Create an Android Notification Channel. + /// + /// We use this channel in the `AndroidManifest.xml` file to override the + /// default FCM channel to enable heads up notifications. + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.createNotificationChannel(channel); + + /// Update the iOS foreground notification presentation options to allow + /// heads up notifications. + await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions( + alert: true, + badge: true, + sound: true, + ); + isFlutterLocalNotificationsInitialized = true; +} + +void showFlutterNotification(RemoteMessage message) { + RemoteNotification? notification = message.notification; + AndroidNotification? android = message.notification?.android; + if (notification != null && android != null && !kIsWeb) { + flutterLocalNotificationsPlugin.show( + notification.hashCode, + notification.title, + notification.body, + NotificationDetails( + android: AndroidNotificationDetails( + channel.id, + channel.name, + channelDescription: channel.description, + // TODO add a proper drawable resource to android, for now using + // one that already exists in example app. + icon: 'launch_background', ), - ) + ), ); } +} -} \ No newline at end of file +/// Initialize the [FlutterLocalNotificationsPlugin] package. +late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; diff --git a/lib/backend/push_notification/push_notification_service.dart b/lib/backend/push_notification/push_notification_service.dart index 62168020..a7ca841f 100644 --- a/lib/backend/push_notification/push_notification_service.dart +++ b/lib/backend/push_notification/push_notification_service.dart @@ -1,625 +1,627 @@ -// 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(); -// } -// -// // OK -// 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(); -// } -// } +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>(); + Subject getOnMessage() { + return _onMessage; + } + + PushNotificationService() { + log('PushNotificationService created'); + } + + Future initialize() async { + log('Initializing PushNotificationService'); + + FirebaseMessaging.onBackgroundMessage( + (RemoteMessage message) => _firebaseMessagingBackgroundHandler( + message, + _flutterLocalNotificationsPlugin, + ), + ); + _initializeLocalNotifications(); + _createNotificationChannels(); + await _requestPermissions(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _listenToForegroundMessages(); + _listenToBackgroundMessages(); + _listenToNotificationClicks(); + }); + await updateDeviceToken(); + log('PushNotificationService initialized'); + } + + // OK + 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(); + } +} diff --git a/lib/flutter_flow/flutter_flow_util.dart b/lib/flutter_flow/flutter_flow_util.dart index 8d1ffe11..5c5239b9 100644 --- a/lib/flutter_flow/flutter_flow_util.dart +++ b/lib/flutter_flow/flutter_flow_util.dart @@ -1,14 +1,14 @@ import 'dart:io'; +import 'dart:math' show pow, pi, sin; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:collection/collection.dart'; import 'package:from_css_color/from_css_color.dart'; import 'package:hub/flutter_flow/flutter_flow_theme.dart'; import 'package:hub/flutter_flow/flutter_flow_widgets.dart'; import 'package:hub/flutter_flow/internationalization.dart'; -import 'dart:math' show pow, pi, sin; import 'package:intl/intl.dart'; import 'package:json_path/json_path.dart'; import 'package:timeago/timeago.dart' as timeago; @@ -16,19 +16,21 @@ import 'package:url_launcher/url_launcher.dart'; import '../main.dart'; +export 'dart:convert' show jsonEncode, jsonDecode; +export 'dart:math' show min, max; +export 'dart:typed_data' show Uint8List; + +export 'package:intl/intl.dart'; +export 'package:page_transition/page_transition.dart'; + +export '/app_state.dart'; +export 'custom_icons.dart' show FFIcons; +export 'flutter_flow_model.dart'; +export 'internationalization.dart' show FFLocalizations; export 'keep_alive_wrapper.dart'; export 'lat_lng.dart'; export 'place.dart'; export 'uploaded_file.dart'; -export '/app_state.dart'; -export 'flutter_flow_model.dart'; -export 'dart:math' show min, max; -export 'dart:typed_data' show Uint8List; -export 'dart:convert' show jsonEncode, jsonDecode; -export 'package:intl/intl.dart'; -export 'package:page_transition/page_transition.dart'; -export 'custom_icons.dart' show FFIcons; -export 'internationalization.dart' show FFLocalizations; T valueOrDefault(T? value, T defaultValue) => (value is String && value.isEmpty) || value == null ? defaultValue : value; @@ -501,10 +503,10 @@ extension IterableExt on Iterable { } void setAppLanguage(BuildContext context, String language) => - MyApp.of(context).setLocale(language); + App.of(context).setLocale(language); void setDarkModeSetting(BuildContext context, ThemeMode themeMode) => - MyApp.of(context).setThemeMode(themeMode); + App.of(context).setThemeMode(themeMode); void showSnackbar( BuildContext context, diff --git a/lib/main.dart b/lib/main.dart index 33b62bed..f8bcd7ba 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,42 +10,79 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:hub/app_state.dart'; -import 'package:hub/backend/push_notification/push_notification.dart'; import 'package:hub/firebase_options.dart'; import 'package:hub/flutter_flow/flutter_flow_theme.dart'; import 'package:hub/flutter_flow/internationalization.dart'; import 'package:hub/flutter_flow/nav/nav.dart'; +import 'package:hub/shared/utils/notification_util.dart'; import 'package:provider/provider.dart'; import 'package:responsive_framework/responsive_framework.dart'; final GlobalKey navigatorKey = GlobalKey(); -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - // FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); - - // if (!kIsWeb) { - // await setupFlutterNotifications(); - // } - - - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - - await init().then((_) { - runApp(ChangeNotifierProvider( - create: (context) => AppState(), - child: const MyApp(), - )); - }); +@pragma('vm:entry-point') +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + if (!isFlutterLocalNotificationsInitialized) + await setupFlutterNotifications(); + if (!Firebase.apps.isNotEmpty) + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform); + if (message.data.isNotEmpty) { + log('Handling a background message ${message.messageId}'); + log('Message data: ${message.data}'); + showFlutterNotification(message); + } + log('Handling a background message ${message.messageId}'); } -Future init() async { - WidgetsBinding.instance.addPostFrameCallback((_) async => await PushNotification().initialize()); +late AndroidNotificationChannel channel; +bool isFlutterLocalNotificationsInitialized = false; +late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; +late final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; + +Future setupFlutterNotifications() async { + if (isFlutterLocalNotificationsInitialized) return; + + channel = const AndroidNotificationChannel( + 'high_importance_channel', // id + 'High Importance Notifications', // title + description: + 'This channel is used for important notifications.', // description + importance: Importance.high, + ); + + flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.createNotificationChannel(channel); + flutterLocalNotificationsPlugin.initialize( + InitializationSettings( + android: const AndroidInitializationSettings('ic_launcher'), + iOS: DarwinInitializationSettings(onDidReceiveLocalNotification: + (int id, String? title, String? body, String? payload) async { + log('Test'); + }), + ), + onDidReceiveBackgroundNotificationResponse: (details) => log('Test'), + onDidReceiveNotificationResponse: (details) => log('Test'), + ); + + isFlutterLocalNotificationsInitialized = true; +} + +Future initializeFirebase() async {} + +Future initializeApp() async { + WidgetsFlutterBinding.ensureInitialized(); + // await initializeFirebase(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); + await setupFlutterNotifications(); + setUrlStrategy(const PathUrlStrategy()); + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); if (kDebugMode) { log("Aplicativo em Debug Mode, crashlytics desabilitado!"); } else { @@ -63,14 +100,20 @@ Future init() async { usePathUrlStrategy(); } -class MyApp extends StatefulWidget { - const MyApp({super.key}); - @override - State createState() => _MyAppState(); - static _MyAppState of(BuildContext context) => context.findAncestorStateOfType<_MyAppState>()!; +void main() async { + await initializeApp(); + runApp(const App()); } -class _MyAppState extends State { +class App extends StatefulWidget { + const App({super.key}); + @override + State createState() => _AppState(); + static _AppState of(BuildContext context) => + context.findAncestorStateOfType<_AppState>()!; +} + +class _AppState extends State { Locale? _locale = FFLocalizations.getStoredLocale(); ThemeMode _themeMode = FlutterFlowTheme.themeMode; late AppStateNotifier _appStateNotifier; @@ -103,62 +146,67 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { - return MaterialApp.router( - title: 'FREHub', - builder: (context, widget) => ResponsiveBreakpoints.builder( - child: BouncingScrollWrapper.builder(context, widget!), - breakpoints: [ - const Breakpoint(start: 0, end: 450, name: MOBILE), - const Breakpoint(start: 451, end: 800, name: TABLET), - const Breakpoint(start: 801, end: 1920, name: DESKTOP), - const Breakpoint(start: 1921, end: double.infinity, name: '4K'), + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => AppState()), + ], + child: MaterialApp.router( + title: 'FREHub', + builder: (context, widget) => ResponsiveBreakpoints.builder( + child: BouncingScrollWrapper.builder(context, widget!), + breakpoints: [ + const Breakpoint(start: 0, end: 450, name: MOBILE), + const Breakpoint(start: 451, end: 800, name: TABLET), + const Breakpoint(start: 801, end: 1920, name: DESKTOP), + const Breakpoint(start: 1921, end: double.infinity, name: '4K'), + ], + ), + localizationsDelegates: const [ + FFLocalizationsDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, ], - ), - localizationsDelegates: const [ - FFLocalizationsDelegate(), - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - locale: _locale, - supportedLocales: const [ - Locale('pt'), - Locale('en'), - ], - theme: ThemeData( - brightness: Brightness.light, - scrollbarTheme: ScrollbarThemeData( - thumbVisibility: WidgetStateProperty.all(false), - interactive: false, - thumbColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.dragged)) { + locale: _locale, + supportedLocales: const [ + Locale('pt'), + Locale('en'), + ], + theme: ThemeData( + brightness: Brightness.light, + scrollbarTheme: ScrollbarThemeData( + thumbVisibility: WidgetStateProperty.all(false), + interactive: false, + thumbColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.dragged)) { + return const Color(0xff1aab5f); + } + if (states.contains(WidgetState.hovered)) { + return const Color(0xff1aab5f); + } return const Color(0xff1aab5f); - } - if (states.contains(WidgetState.hovered)) { - return const Color(0xff1aab5f); - } - return const Color(0xff1aab5f); - }), + }), + ), ), - ), - darkTheme: ThemeData( - brightness: Brightness.dark, - scrollbarTheme: ScrollbarThemeData( - thumbVisibility: WidgetStateProperty.all(false), - interactive: false, - thumbColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.dragged)) { + darkTheme: ThemeData( + brightness: Brightness.dark, + scrollbarTheme: ScrollbarThemeData( + thumbVisibility: WidgetStateProperty.all(false), + interactive: false, + thumbColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.dragged)) { + return const Color(0xff1aab5f); + } + if (states.contains(WidgetState.hovered)) { + return const Color(0xff1aab5f); + } return const Color(0xff1aab5f); - } - if (states.contains(WidgetState.hovered)) { - return const Color(0xff1aab5f); - } - return const Color(0xff1aab5f); - }), + }), + ), ), + themeMode: _themeMode, + routerConfig: _router, ), - themeMode: _themeMode, - routerConfig: _router, ); } } diff --git a/lib/pages/home_page/home_page_widget.dart b/lib/pages/home_page/home_page_widget.dart index f4acf0f8..d297bd63 100644 --- a/lib/pages/home_page/home_page_widget.dart +++ b/lib/pages/home_page/home_page_widget.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hub/actions/actions.dart'; @@ -7,7 +8,6 @@ import 'package:hub/backend/schema/enums/enums.dart'; import 'package:hub/components/organism_components/bottom_arrow_linked_locals_component/bottom_arrow_linked_locals_component_widget.dart'; import 'package:hub/components/organism_components/local_profile_component/local_profile_component_widget.dart'; import 'package:hub/components/organism_components/menu_component/menu_component_widget.dart'; -import 'package:hub/components/organism_components/message_well_component/message_well_component_widget.dart'; import 'package:hub/flutter_flow/custom_functions.dart'; import 'package:hub/flutter_flow/flutter_flow_icon_button.dart'; import 'package:hub/flutter_flow/flutter_flow_theme.dart'; @@ -15,9 +15,9 @@ import 'package:hub/flutter_flow/flutter_flow_util.dart'; import 'package:hub/flutter_flow/flutter_flow_widgets.dart'; import 'package:hub/flutter_flow/nav/nav.dart'; import 'package:hub/pages/home_page/home_page_model.dart'; +import 'package:hub/shared/utils/notification_util.dart'; import 'package:provider/provider.dart'; - class HomePageWidget extends StatefulWidget { const HomePageWidget({super.key}); @@ -29,6 +29,11 @@ class _HomePageWidgetState extends State { late HomePageModel _model; bool localStatus = false; final scaffoldKey = GlobalKey(); + String? _token; + String? initialMessage; + bool _resolved = false; + static late final FirebaseMessaging _firebaseMessaging = + FirebaseMessaging.instance; Future checkLocalStatus() async { localStatus = await checkLocals( @@ -45,6 +50,36 @@ class _HomePageWidgetState extends State { AppState().context = context; + () async => await _firebaseMessaging.requestPermission( + alert: true, + announcement: false, + badge: true, + carPlay: false, + criticalAlert: false, + provisional: false, + sound: true, + ); + + _firebaseMessaging.getInitialMessage().then( + (value) => setState( + () { + _resolved = true; + initialMessage = value?.data.toString(); + }, + ), + ); + + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + log(message.data.toString()); + showFlutterNotification(message); + }); + + FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { + log(message.data.toString()); + print('A new onMessageOpenedApp event was published!'); + showAlertDialog(context, 'Test', 'Test', () async {}); + }); + WidgetsBinding.instance.addPostFrameCallback((_) async { @override void initState() { @@ -155,17 +190,17 @@ class _HomePageWidgetState extends State { item: MenuItem.button, ), ), - Align( - alignment: const AlignmentDirectional(0.0, 0.0), - child: Provider( - create: (_) => MessageWellNotifier(), - child: wrapWithModel( - model: _model.messageWellComponentModel, - updateCallback: () => setState(() {}), - child: const MessageWellComponentWidget(), - ), - ), - ), + // Align( + // alignment: const AlignmentDirectional(0.0, 0.0), + // child: Provider( + // create: (_) => MessageWellNotifier(), + // child: wrapWithModel( + // model: _model.messageWellComponentModel, + // updateCallback: () => setState(() {}), + // child: const MessageWellComponentWidget(), + // ), + // ), + // ), //footer const SizedBox( height: 100, @@ -585,4 +620,3 @@ class _HomePageWidgetState extends State { ); } } - diff --git a/lib/shared/utils/notification_util.dart b/lib/shared/utils/notification_util.dart new file mode 100644 index 00000000..48e919e5 --- /dev/null +++ b/lib/shared/utils/notification_util.dart @@ -0,0 +1,25 @@ +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:hub/main.dart'; + +void showFlutterNotification(RemoteMessage message) { + RemoteNotification? notification = message.notification; + AndroidNotification? android = message.notification?.android; + if (notification != null && android != null && !kIsWeb) { + flutterLocalNotificationsPlugin.show( + notification.hashCode, + notification.title, + notification.body, + NotificationDetails( + android: AndroidNotificationDetails( + channel.id, + channel.name, + channelDescription: channel.description, + // one that already exists in example app. + icon: 'launch_background', + ), + ), + ); + } +} diff --git a/log b/log index cb8d0d64..484f4ee9 100644 --- a/log +++ b/log @@ -1,102 +1,2 @@ -Launching lib/main.dart on M2102J20SG in debug mode... -✓ Built build/app/outputs/flutter-apk/app-debug.apk -Connecting to VM Service at ws://127.0.0.1:36725/jp8cB4aqi1E=/ws -Connected to the VM Service. -[log] Initializing Firebase... -[log] Firebase initialized. -[log] Setting up Crashlytics... -[log] Crashlytics set up. -[log] Initializing FlutterFlowTheme... -[log] FlutterFlowTheme initialized. -[log] Initializing FFLocalizations... -[log] FFLocalizations initialized. -[log] Initializing app state... -[log] App state initialized. -[log] Initializing GoRouter... -[log] GoRouter initialized. -[GoRouter] setting initial location / -[GoRouter] Full paths for routes: - ├─/ - ├─/homePage - ├─/messageHistoryPage - ├─/registerVisitorPage - ├─/scheduleCompleteVisitPage - ├─/scheduleProvisionalVisitPage - ├─/fastPassPage - ├─/preferencesSettings - ├─/peopleOnThePropertyPage - ├─/acessHistoryPage - ├─/liberationHistory - ├─/signInPage - ├─/signUpPage - ├─/welcomePage - ├─/qrCodePage - └─/preferencesPage - known full paths for route names: - _initialize => / - homePage => /homePage - messageHistoryPage => /messageHistoryPage - registerVisitorPage => /registerVisitorPage - scheduleCompleteVisitPage => /scheduleCompleteVisitPage - scheduleProvisionalVisitPage => /scheduleProvisionalVisitPage - fastPassPage => /fastPassPage - preferencesSettings => /preferencesSettings - peopleOnThePropertyPage => /peopleOnThePropertyPage - acessHistoryPage => /acessHistoryPage - liberationHistory => /liberationHistory - signInPage => /signInPage - signUpPage => /signUpPage - welcomePage => /welcomePage - qrCodePage => /qrCodePage - preferencesPage => /preferencesPage -D/ProfileInstaller(23527): Installing profile for com.freaccess.hub - - - - -E/flutter (23527): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: 'package:flutter_local_notifications/src/platform_flutter_local_notifications.dart': Failed assertion: line 1030 pos 12: 'callback != null': The backgroundHandler needs to be either a static function or a top -@REM /// The background handler for when the app is in the background or terminated -@REM Future _evaluateBackgroundNotificationCallback() async { -@REM if (backgroundNotificationCallback != null) { -@REM final NotificationAppLaunchDetails notificationAppLaunchDetails = -@REM await _getNotificationAppLaunchDetails(); -@REM await backgroundNotificationCallback!(notificationAppLaunchDetails); -@REM } -@REM } - -E/flutter (23527): level function to be accessible as a Flutter entry point. -E/flutter (23527): #0 _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:50:61) -E/flutter (23527): #1 _AssertionError._throwNew (dart:core-patch/errors_patch.dart:40:5) -E/flutter (23527): #2 _evaluateBackgroundNotificationCallback (package:flutter_local_notifications/src/platform_flutter_local_notifications.dart:1030:12) -E/flutter (23527): #3 AndroidFlutterLocalNotificationsPlugin.initialize (package:flutter_local_notifications/src/platform_flutter_local_notifications.dart:140:5) -E/flutter (23527): #4 FlutterLocalNotificationsPlugin.initialize (package:flutter_local_notifications/src/flutter_local_notifications_plugin.dart:140:13) -E/flutter (23527): #5 PushNotificationService._initializeLocalNotifications (package:hub/backend/push_notification/push_notification_service.dart:112:40) -E/flutter (23527): #6 new PushNotificationService (package:hub/backend/push_notification/push_notification_service.dart:29:5) -E/flutter (23527): #7 _MyAppState.initState. (package:hub/main.dart:98:13) -E/flutter (23527): #8 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1397:15) -E/flutter (23527): #9 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1331:11) -E/flutter (23527): #10 SchedulerBinding.scheduleWarmUpFrame. (package:flutter/src/scheduler/binding.dart:1040:9) -E/flutter (23527): #11 PlatformDispatcher.scheduleWarmUpFrame. (dart:ui/platform_dispatcher.dart:837:16) -E/flutter (23527): #12 Timer._createTimer. (dart:async-patch/timer_patch.dart:18:15) -E/flutter (23527): #13 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:398:19) -E/flutter (23527): #14 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:429:5) -E/flutter (23527): #15 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12) -E/flutter (23527): -D/DecorView[](23527): onWindowFocusChanged hasWindowFocus true -[log] Context: HomePageWidget(dependencies: [InheritedCupertinoTheme, MediaQuery, _InheritedProviderScope, _InheritedTheme, _LocalizationsScope-[GlobalKey#e99e5]], state: _HomePageWidgetState#edcf9) -[log] Notification permissions granted -[log] User granted permission -E/flutter (23527): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Null check operator used on a null value -E/flutter (23527): #0 MethodChannelFirebaseMessaging.registerBackgroundMessageHandler (package:firebase_messaging_platform_interface/src/method_channel/method_channel_messaging.dart:201:53) -E/flutter (23527): #1 FirebaseMessagingPlatform.onBackgroundMessage= (package:firebase_messaging_platform_interface/src/platform_interface/platform_interface_messaging.dart:107:16) -E/flutter (23527): #2 FirebaseMessaging.onBackgroundMessage (package:firebase_messaging/src/messaging.dart:73:31) -E/flutter (23527): #3 PushNotificationService._listenToBackgroundMessages (package:hub/backend/push_notification/push_notification_service.dart:191:23) -E/flutter (23527): #4 PushNotificationService.initialize. (package:hub/backend/push_notification/push_notification_service.dart:41:7) -E/flutter (23527): #5 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1397:15) -E/flutter (23527): #6 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1331:11) -E/flutter (23527): #7 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1176:5) -E/flutter (23527): #8 _invoke (dart:ui/hooks.dart:312:13) -E/flutter (23527): #9 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:419:5) -E/flutter (23527): #10 _drawFrame (dart:ui/hooks.dart:283:31) -E/flutter (23527): -[log] Token update successful +[log] Error initializing notification manager: 'package:flutter_local_notifications/src/platform_flutter_local_notifications.dart': Failed assertion: line 1033 pos 12: 'callback != null': The backgroundHandler needs to be either a static function or a top + level function to be accessible as a Flutter entry point. \ No newline at end of file