257 lines
8.8 KiB
Dart
257 lines
8.8 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
import 'package:firebase_core/firebase_core.dart';
|
|
import 'package:travel_mate/services/logger_service.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:travel_mate/services/error_service.dart';
|
|
import 'package:travel_mate/repositories/group_repository.dart';
|
|
import 'package:travel_mate/repositories/account_repository.dart';
|
|
import 'package:travel_mate/components/group/chat_group_content.dart';
|
|
import 'package:travel_mate/components/account/group_expenses_page.dart';
|
|
|
|
@pragma('vm:entry-point')
|
|
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|
await Firebase.initializeApp();
|
|
LoggerService.info('Handling a background message: ${message.messageId}');
|
|
}
|
|
|
|
class NotificationService {
|
|
static final NotificationService _instance = NotificationService._internal();
|
|
factory NotificationService() => _instance;
|
|
NotificationService._internal();
|
|
|
|
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
|
|
final FlutterLocalNotificationsPlugin _localNotifications =
|
|
FlutterLocalNotificationsPlugin();
|
|
|
|
bool _isInitialized = false;
|
|
|
|
Future<void> initialize() async {
|
|
if (_isInitialized) return;
|
|
|
|
// Request permissions
|
|
await _requestPermissions();
|
|
|
|
// Initialize local notifications
|
|
const androidSettings = AndroidInitializationSettings(
|
|
'@mipmap/ic_launcher',
|
|
);
|
|
const iosSettings = DarwinInitializationSettings();
|
|
const initSettings = InitializationSettings(
|
|
android: androidSettings,
|
|
iOS: iosSettings,
|
|
);
|
|
|
|
await _localNotifications.initialize(
|
|
initSettings,
|
|
onDidReceiveNotificationResponse: (details) {
|
|
// Handle notification tap
|
|
LoggerService.info('Notification tapped: ${details.payload}');
|
|
if (details.payload != null) {
|
|
try {
|
|
final data = json.decode(details.payload!) as Map<String, dynamic>;
|
|
_handleNotificationTap(data);
|
|
} catch (e) {
|
|
LoggerService.error('Error parsing notification payload', error: e);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
// Handle foreground messages
|
|
FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
|
|
|
|
// Handle token refresh
|
|
FirebaseMessaging.instance.onTokenRefresh.listen(_onTokenRefresh);
|
|
|
|
// Setup interacted message (Deep Linking)
|
|
// We don't call this here anymore, it will be called from HomePage
|
|
// await setupInteractedMessage();
|
|
|
|
_isInitialized = true;
|
|
LoggerService.info('NotificationService initialized');
|
|
|
|
// Print current token for debugging
|
|
final token = await getFCMToken();
|
|
LoggerService.info('Current FCM Token: $token');
|
|
}
|
|
|
|
/// Sets up the background message listener.
|
|
/// Should be called when the app is ready to handle navigation.
|
|
void startListening() {
|
|
// Handle any interaction when the app is in the background via a
|
|
// Stream listener
|
|
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
|
_handleNotificationTap(message.data);
|
|
});
|
|
}
|
|
|
|
/// Checks for an initial message (app opened from terminated state)
|
|
/// and handles it if present.
|
|
Future<void> handleInitialMessage() async {
|
|
// Get any messages which caused the application to open from
|
|
// a terminated state.
|
|
RemoteMessage? initialMessage = await _firebaseMessaging
|
|
.getInitialMessage();
|
|
|
|
if (initialMessage != null) {
|
|
LoggerService.info('Found initial message: ${initialMessage.data}');
|
|
_handleNotificationTap(initialMessage.data);
|
|
}
|
|
}
|
|
|
|
Future<void> _handleNotificationTap(Map<String, dynamic> data) async {
|
|
LoggerService.info('Handling notification tap with data: $data');
|
|
// DEBUG: Show snackbar to verify payload
|
|
// ErrorService().showSnackbar(message: 'Debug: Payload $data', isError: false);
|
|
|
|
final type = data['type'];
|
|
|
|
try {
|
|
if (type == 'message') {
|
|
final groupId = data['groupId'];
|
|
if (groupId != null) {
|
|
final groupRepository = GroupRepository();
|
|
final group = await groupRepository.getGroupById(groupId);
|
|
if (group != null) {
|
|
ErrorService.navigatorKey.currentState?.push(
|
|
MaterialPageRoute(
|
|
builder: (context) => ChatGroupContent(group: group),
|
|
),
|
|
);
|
|
} else {
|
|
LoggerService.error('Group not found: $groupId');
|
|
// ErrorService().showError(message: 'Groupe introuvable: $groupId');
|
|
}
|
|
} else {
|
|
LoggerService.error('Missing groupId in payload');
|
|
// ErrorService().showError(message: 'Payload invalide: groupId manquant');
|
|
}
|
|
} else if (type == 'expense') {
|
|
final tripId = data['tripId'];
|
|
if (tripId != null) {
|
|
final accountRepository = AccountRepository();
|
|
final groupRepository = GroupRepository();
|
|
|
|
final account = await accountRepository.getAccountByTripId(tripId);
|
|
final group = await groupRepository.getGroupByTripId(tripId);
|
|
|
|
if (account != null && group != null) {
|
|
ErrorService.navigatorKey.currentState?.push(
|
|
MaterialPageRoute(
|
|
builder: (context) =>
|
|
GroupExpensesPage(account: account, group: group),
|
|
),
|
|
);
|
|
} else {
|
|
LoggerService.error('Account or Group not found for trip: $tripId');
|
|
// ErrorService().showError(message: 'Compte ou Groupe introuvable');
|
|
}
|
|
}
|
|
} else {
|
|
LoggerService.info('Unknown notification type: $type');
|
|
}
|
|
} catch (e) {
|
|
LoggerService.error('Error handling notification tap: $e');
|
|
ErrorService().showError(message: 'Erreur navigation: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _onTokenRefresh(String newToken) async {
|
|
LoggerService.info('FCM Token refreshed: $newToken');
|
|
// We need the user ID to save the token.
|
|
// Since this service is a singleton, we might not have direct access to the user ID here
|
|
// without injecting the repository or bloc.
|
|
// For now, we rely on the AuthBloc to update the token on login/start.
|
|
// Ideally, we should save it here if we have the user ID.
|
|
}
|
|
|
|
Future<void> _requestPermissions() async {
|
|
NotificationSettings settings = await _firebaseMessaging.requestPermission(
|
|
alert: true,
|
|
badge: true,
|
|
sound: true,
|
|
);
|
|
|
|
LoggerService.info(
|
|
'User granted permission: ${settings.authorizationStatus}',
|
|
);
|
|
}
|
|
|
|
Future<String?> getFCMToken() async {
|
|
try {
|
|
if (Platform.isIOS) {
|
|
String? apnsToken = await _firebaseMessaging.getAPNSToken();
|
|
int retries = 0;
|
|
while (apnsToken == null && retries < 10) {
|
|
LoggerService.info(
|
|
'Waiting for APNS token... (Attempt ${retries + 1}/10)',
|
|
);
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
apnsToken = await _firebaseMessaging.getAPNSToken();
|
|
retries++;
|
|
}
|
|
if (apnsToken == null) {
|
|
LoggerService.error('APNS token not available after retries');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
final token = await _firebaseMessaging.getToken();
|
|
LoggerService.info('NotificationService - FCM Token: $token');
|
|
return token;
|
|
} catch (e) {
|
|
LoggerService.error('Error getting FCM token: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<void> saveTokenToFirestore(String userId) async {
|
|
try {
|
|
final token = await getFCMToken();
|
|
if (token != null) {
|
|
await FirebaseFirestore.instance.collection('users').doc(userId).set({
|
|
'fcmToken': token,
|
|
}, SetOptions(merge: true));
|
|
LoggerService.info('FCM Token saved to Firestore for user: $userId');
|
|
}
|
|
} catch (e) {
|
|
LoggerService.error('Error saving FCM token to Firestore: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _handleForegroundMessage(RemoteMessage message) async {
|
|
LoggerService.info('Got a message whilst in the foreground!');
|
|
LoggerService.info('Message data: ${message.data}');
|
|
|
|
if (message.notification != null) {
|
|
LoggerService.info(
|
|
'Message also contained a notification: ${message.notification}',
|
|
);
|
|
|
|
// Show local notification
|
|
const androidDetails = AndroidNotificationDetails(
|
|
'high_importance_channel',
|
|
'High Importance Notifications',
|
|
importance: Importance.max,
|
|
priority: Priority.high,
|
|
);
|
|
const iosDetails = DarwinNotificationDetails();
|
|
const details = NotificationDetails(
|
|
android: androidDetails,
|
|
iOS: iosDetails,
|
|
);
|
|
|
|
await _localNotifications.show(
|
|
message.hashCode,
|
|
message.notification?.title,
|
|
message.notification?.body,
|
|
details,
|
|
);
|
|
}
|
|
}
|
|
}
|