628 lines
22 KiB
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();
|
|
}
|
|
}
|