import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; import '../models/conversation_model.dart'; import '../screens/chat_screen.dart'; import 'whatsapp_service.dart'; @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { // We can't access GetX services easily here since it's a separate isolate, // but Firebase automatically shows the notification. print("Handling a background message: ${message.messageId}"); } class FirebaseService extends GetxService { final FirebaseMessaging _messaging = FirebaseMessaging.instance; final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin(); Future init() async { // 1. Request Permission await _messaging.requestPermission( alert: true, badge: true, sound: true, ); // 2. Initialize Local Notifications (for foreground) const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher'); const iosInit = DarwinInitializationSettings(); const initSettings = InitializationSettings(android: androidInit, iOS: iosInit); await _localNotifications.initialize( initSettings, onDidReceiveNotificationResponse: _onNotificationTap, ); // 3. Background Handler FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); // 4. Foreground Message Handler FirebaseMessaging.onMessage.listen((RemoteMessage message) { final chatId = message.data['chatId']; final activeChatId = Get.find().activeChatId.value; // Smart Notification: Only show if we are NOT currently in this chat if (chatId != null && activeChatId == chatId) { print('[FCM] Silent notification, user is currently in chat $chatId'); return; // Silent } _showLocalNotification(message); }); // 5. Handle tapping on a background notification when app opens FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { _handleNotificationClick(message.data); }); // 6. Check if app was opened from a terminated state via a notification final initialMessage = await _messaging.getInitialMessage(); if (initialMessage != null) { Future.delayed(const Duration(seconds: 1), () { _handleNotificationClick(initialMessage.data); }); } // 7. Request and register FCM Token on the server _setupFcmTokenRegistration(); return this; } void _setupFcmTokenRegistration() async { try { // For iOS, wait until the APNS token is successfully initialized by the OS before calling getToken() if (Platform.isIOS) { String? apnsToken = await _messaging.getAPNSToken(); int retries = 0; while (apnsToken == null && retries < 15) { print('[FCM] APNS token not set yet. Waiting 1 second... (Attempt ${retries + 1}/15)'); await Future.delayed(const Duration(seconds: 1)); apnsToken = await _messaging.getAPNSToken(); retries++; } if (apnsToken != null) { print('[FCM] APNS Token successfully obtained: $apnsToken'); } else { print('[FCM WARNING] Failed to obtain APNS token after 15 attempts. FCM registration might fail.'); } } final token = await _messaging.getToken(); if (token != null) { final displayToken = token.length > 15 ? token.substring(0, 15) : token; print('[FCM] Token obtained: $displayToken...'); _registerTokenOnServer(token); } } catch (e) { print('[FCM ERROR] Could not get token: $e'); } _messaging.onTokenRefresh.listen((newToken) { print('[FCM] Token refreshed'); _registerTokenOnServer(newToken); }); } void _registerTokenOnServer(String token) { final wsSvc = Get.find(); if (wsSvc.status.value == WsStatus.waReady || wsSvc.status.value == WsStatus.connected) { print('[FCM] Sending token to server...'); wsSvc.sendFcmToken(token); } else { // Listen to status changes and send once connected late StreamSubscription sub; sub = wsSvc.status.listen((status) { if (status == WsStatus.waReady || status == WsStatus.connected) { print('[FCM] WebSocket connected, sending token to server...'); wsSvc.sendFcmToken(token); sub.cancel(); } }); } } void _showLocalNotification(RemoteMessage message) { final notification = message.notification; if (notification == null) return; const androidDetails = AndroidNotificationDetails( 'whatsapp_channel', 'WhatsApp Messages', importance: Importance.max, priority: Priority.high, ticker: 'ticker', ); const iosDetails = DarwinNotificationDetails(); const details = NotificationDetails(android: androidDetails, iOS: iosDetails); _localNotifications.show( notification.hashCode, notification.title, notification.body, details, payload: jsonEncode(message.data), ); } void showLocalNotificationFromData(Map data) { final chatId = data['chatId']; final name = data['name'] ?? 'WhatsApp'; final body = data['body'] ?? 'New Message'; // Smart Notification: Only show if we are NOT currently in this chat final activeChatId = Get.find().activeChatId.value; if (chatId != null && activeChatId == chatId) { return; // Silent } const androidDetails = AndroidNotificationDetails( 'whatsapp_channel', 'WhatsApp Messages', importance: Importance.max, priority: Priority.high, ticker: 'ticker', ); const iosDetails = DarwinNotificationDetails(); const details = NotificationDetails(android: androidDetails, iOS: iosDetails); _localNotifications.show( DateTime.now().microsecond, name, body, details, payload: jsonEncode({'chatId': chatId, 'name': name}), ); } void _onNotificationTap(NotificationResponse response) { if (response.payload != null) { final data = jsonDecode(response.payload!); _handleNotificationClick(data); } } void _handleNotificationClick(Map data) { final chatId = data['chatId']; final name = data['name'] ?? 'Chat'; if (chatId != null) { final dummyChat = ConversationModel( id: chatId, name: name, isGroup: false, unreadCount: 0, timestamp: 0, pinned: false, isMuted: false, ); Get.to(() => ChatScreen(conversation: dummyChat), preventDuplicates: false); } } }