import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:mc_common_app/classes/app_state.dart'; import 'package:mc_common_app/config/dependency_injection.dart'; import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/main.dart'; import 'package:mc_common_app/services/chat_navigation_service.dart'; import 'package:mc_common_app/utils/navigator.dart'; import 'package:mc_common_app/view_models/chat_view_model.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; abstract class FirebaseMessagingService { Future getFirebaseToken(); Future initializeNotifications(); } class FirebaseMessagingServiceImp implements FirebaseMessagingService { final FlutterLocalNotificationsPlugin flutterLocalNotifications; final FirebaseMessaging firebaseMessaging; FirebaseMessagingServiceImp({required this.firebaseMessaging, required this.flutterLocalNotifications}); @override Future getFirebaseToken() async => await firebaseMessaging.getToken() ?? ""; Future requestPermissions() async { if (Platform.isIOS) { await flutterLocalNotifications.resolvePlatformSpecificImplementation()?.requestPermissions(alert: true, badge: true, sound: true); } else if (Platform.isAndroid) { AndroidFlutterLocalNotificationsPlugin? androidImplementation = flutterLocalNotifications.resolvePlatformSpecificImplementation(); bool? granted = await androidImplementation?.requestNotificationsPermission(); if (granted == false) { await Permission.notification.request(); } } } @override Future initializeNotifications() async { await requestPermissions(); AppState().setDeviceToken = await getFirebaseToken(); AppState().setDeviceType = Platform.isAndroid ? "1" : "2"; await Permission.notification.isDenied.then((bool value) { if (value) { Permission.notification.request(); } }); // Create high-priority notification channel for Android (required for terminated state) if (Platform.isAndroid) { const AndroidNotificationChannel channel = AndroidNotificationChannel( 'mowater_chat_channel', // More descriptive channel ID 'Chat Notifications', description: 'Notifications for chat messages', importance: Importance.max, // Changed to max for Android 14 playSound: true, enableVibration: true, showBadge: true, ); await flutterLocalNotifications.resolvePlatformSpecificImplementation()?.createNotificationChannel(channel); log("✅ Android notification channel created: ${channel.id}"); } // initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('ic_launcher'); DarwinInitializationSettings initializationSettingsDarwin = const DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestCriticalPermission: true, requestProvisionalPermission: true, requestSoundPermission: true, defaultPresentAlert: true, defaultPresentBadge: true, defaultPresentBanner: true, defaultPresentList: true, defaultPresentSound: true, ); final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsDarwin); await flutterLocalNotifications.initialize( initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse response) async { // Handle notification tap when app is in foreground/background log("========== Notification Tapped =========="); log("Notification ID: ${response.id}"); log("Action ID: ${response.actionId}"); log("Input: ${response.input}"); log("Payload: ${response.payload}"); log("========================================="); if (response.payload != null && response.payload!.isNotEmpty) { try { Map data = jsonDecode(response.payload!); log("Parsed payload data: $data"); _handleNotificationNavigation(data); } catch (e) { logger.e("Error parsing notification payload: $e"); } } }, ); RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) _handleNotification(initialMessage); FirebaseMessaging.onMessage.listen((RemoteMessage message) { // Foreground notification - just display, don't navigate automatically if (message.notification != null) _handleForegroundNotification(message); }); FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationOpenedApp); FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler); FirebaseMessaging.instance.onTokenRefresh.listen((String token) async { AppState().setDeviceToken = token; }); } void _onDidReceiveLocalNotification(int id, String? title, String? body, String? payload) async { logger.i(body); } void _handleForegroundNotification(RemoteMessage message) async { log("========== Foreground Notification Received =========="); log("Title: ${message.notification!.title}"); log("Body: ${message.notification!.body}"); log("Data payload: ${message.data}"); log("Message ID: ${message.messageId}"); log("Sent time: ${message.sentTime}"); log("Android config: ${message.toMap()}"); log("======================================================"); // Just display notification - DON'T navigate automatically // Navigation happens only when user taps the notification displayNotification( title: message.notification!.title ?? '', body: message.notification!.body ?? '', payload: jsonEncode(message.data), ); } void _handleNotificationOpenedApp(RemoteMessage message) async { log("========== Notification Opened App =========="); log("Title: ${message.notification!.title}"); log("Body: ${message.notification!.body}"); log("Data payload: ${message.data}"); log("Message ID: ${message.messageId}"); log("Sent time: ${message.sentTime}"); log("============================================="); // User tapped notification - navigate to chat _handleNotificationNavigation(message.data); } void _handleNotification(RemoteMessage message) async { // This is called when app is terminated and opened by notification log("========== App Terminated Notification =========="); log("Title: ${message.notification!.title}"); log("Body: ${message.notification!.body}"); log("Data payload: ${message.data}"); log("Message ID: ${message.messageId}"); log("Sent time: ${message.sentTime}"); log("================================================="); // User opened app from notification - navigate to chat _handleNotificationNavigation(message.data); } void _handleNotificationNavigation(Map data) async { log("========== Handling Notification Navigation =========="); log("Full data received: $data"); if (data.isEmpty) { log("Data is empty, skipping navigation"); log("======================================================"); return; } // Backend sends NotiType as string: "1" = Request, "2" = Ads, "3" = General String? notiTypeStr = data['NotiType']; int? notiType = int.tryParse(notiTypeStr ?? ''); log("NotiType from backend: $notiTypeStr (parsed: $notiType)"); if (notiType == null || notiType < 1 || notiType > 3) { log("Invalid or missing NotiType, skipping navigation"); log("======================================================"); return; } // Map NotiType to chat type String chatType; switch (notiType) { case 1: chatType = 'request_offer'; break; case 2: chatType = 'ad_chat'; break; case 3: chatType = 'general_chat'; break; default: log("Unknown NotiType: $notiType"); log("======================================================"); return; } log("Mapped chat type: $chatType"); // Wait a bit for context to be available await Future.delayed(Duration(milliseconds: 300)); BuildContext? context = navigatorKey.currentContext; if (context == null) { logger.e("Context not available for navigation"); log("======================================================"); return; } // Log all available data fields to understand backend format log("Available data fields:"); data.forEach((key, value) { log(" $key: $value"); }); switch (chatType) { case 'request_offer': log("Navigating to Request Offer Chat..."); await _navigateToRequestOfferChat(context, data); break; case 'ad_chat': log("Navigating to Advertisement Chat..."); await _navigateToAdChat(context, data); break; case 'general': log("Navigating to General Chat..."); await _navigateToGeneralChat(context, data); break; default: logger.e("Unknown chat type: $chatType"); } log("======================================================"); } Future _navigateToRequestOfferChat( BuildContext context, Map data, ) async { try { // Try both backend format and our format int requestId = int.tryParse(data['RequestID']?.toString() ?? data['request_id']?.toString() ?? '0') ?? 0; if (requestId == 0) { log("❌ Invalid or missing RequestID in notification data"); return; } log("✅ Navigating to providers chat list for request: $requestId"); navigateWithName(context, AppRoutes.providersChatListPage); // TODO: If backend sends all required data (ProviderId, CustomerId), // we can navigate directly to the chat screen } catch (e) { logger.e("Error navigating to request chat: $e"); } } Future _navigateToAdChat( BuildContext context, Map data, ) async { try { // Try both backend format and our format int adId = int.tryParse(data['AdID']?.toString() ?? data['ad_id']?.toString() ?? '0') ?? 0; String? receiverUserId = data['ReceiverUserID'] ?? data['receiver_id']; if (adId == 0 || receiverUserId == null) { log("❌ Invalid AdID or ReceiverUserID in notification data"); log(" AdID: ${data['AdID']}, ReceiverUserID: ${data['ReceiverUserID']}"); return; } // Check if user is already on this specific chat // Try to get ChatVM from Provider, but don't fail if not available try { final chatVM = Provider.of(context, listen: false); if (chatVM.isOnAdChat(adId, receiverUserId)) { log("✅ User already on ad chat (adId=$adId), skipping navigation"); return; } } catch (e) { // ChatVM not available in context, proceed with navigation log("⚠️ Could not check chat context: $e"); } log("✅ Navigating to ad chat: adId=$adId"); final chatNavService = injector.get(); await chatNavService.navigateToAdChat( context: context, adsId: adId, receiverUserId: receiverUserId, ); } catch (e) { logger.e("Error navigating to ad chat: $e"); } } Future _navigateToGeneralChat( BuildContext context, Map data, ) async { try { // Try both backend format and our format int requestId = int.tryParse(data['RequestID']?.toString() ?? data['request_id']?.toString() ?? '0') ?? 0; if (requestId == 0) { log("❌ Invalid or missing RequestID in notification data"); return; } log("✅ Navigating to general chat list for request: $requestId"); navigateWithName( context, AppRoutes.generalChatsListForProvider, arguments: requestId, ); } catch (e) { logger.e("Error navigating to general chat: $e"); } } Future displayNotification({ required String title, required String body, String? payload, }) async { const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( 'mowater_chat_channel', // Match the channel ID we created 'Chat Notifications', channelDescription: 'Notifications for chat messages', importance: Importance.max, priority: Priority.high, ticker: 'ticker', ); const DarwinNotificationDetails iOSPlatformChannelSpecifics = DarwinNotificationDetails(threadIdentifier: 'thread_id'); const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails, iOS: iOSPlatformChannelSpecifics); await flutterLocalNotifications.show(0, title, body, notificationDetails, payload: payload ?? 'item x'); } } @pragma('vm:entry-point') Future backgroundMessageHandler(RemoteMessage message) async { await Firebase.initializeApp(); }