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 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(); late final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; late final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin(); bool _isInitialized = false; Future 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; _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 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 _handleNotificationTap(Map 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 _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 _requestPermissions() async { NotificationSettings settings = await _firebaseMessaging.requestPermission( alert: true, badge: true, sound: true, ); LoggerService.info( 'User granted permission: ${settings.authorizationStatus}', ); } Future 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 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 _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, ); } } }