flutter-freaccess-hub/lib/backend/push_notification/push_notification_service.dart

628 lines
22 KiB
Dart

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<RemoteMessage> _onMessage =
BehaviorSubject<RemoteMessage>();
final BehaviorSubject<Map<String, dynamic>> _notificationDetails =
BehaviorSubject<Map<String, dynamic>>();
Subject<RemoteMessage> getOnMessage() {
return _onMessage;
}
PushNotificationService() {
log('PushNotificationService created');
}
Future<void> 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<void> _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<String, dynamic> 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<String, dynamic> message =
validJsonFromString(response.payload ?? '');
var data = _notificationDetails.value;
_handleNotificationClick(message, extra: data);
} catch (e) {
log('Error handling background notification: $e');
}
}
}
void _createNotificationChannels() {
List<String> 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<void> _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<void> _handleTokenUpdate(String newToken) async {
await _updateToken(newToken);
}
Future<void> 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<NotificationSettings> _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<void> _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<ApiCallResponse?> _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<void> _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<String, dynamic> payload,
{Map<String, dynamic> 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<void> _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<String, dynamic> message, BuildContext? context,
{Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> message,
BuildContext context, Map<String, dynamic> 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<String, dynamic> 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<String, String>.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<RemoteMessage> _onMessageReceivedController =
StreamController<RemoteMessage>.broadcast();
Stream<RemoteMessage> get onMessageReceived =>
_onMessageReceivedController.stream;
PushNotificationManager() {
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
_onMessageReceivedController.add(message);
});
}
void dispose() {
_onMessageReceivedController.close();
}
}