//................................................................................................................................. // In the Below code Backend should send the "category": "call_category" in the APN body to show the Accept and reject Buttons in IOS.... //................................................................................................................................. import 'dart:convert'; import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; import 'package:onufitness/constants/api_endpoints.dart'; import 'package:onufitness/constants/api_enum_constant.dart'; import 'package:onufitness/constants/color_constant.dart'; import 'package:onufitness/constants/constant.dart'; import 'package:onufitness/firebase_options.dart'; import 'package:onufitness/routes/route_constant.dart'; import 'package:onufitness/services/api_services/base_api_services.dart'; import 'package:onufitness/services/local_storage_services/shared_services.dart'; import 'package:onufitness/services/notification_services/navigation_controller.dart'; import 'package:onufitness/services/logger_service.dart'; import 'package:onufitness/services/agora/call_services.dart'; import 'package:onufitness/controller/get_agora_token_controller.dart'; import 'package:permission_handler/permission_handler.dart'; import 'dart:async'; import 'package:shared_preferences/shared_preferences.dart'; // ============================================================================ // GLOBAL SHARED INSTANCES // ============================================================================ final FlutterLocalNotificationsPlugin globalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final Map activeCallNotifications = {}; // NEW: Track if there's an active call notification // bool hasActiveCallNotification = false; // String? activeCallChannelName; // Global flag to track app launch state bool wasLaunchedFromTerminatedState = false; Map? pendingCallAction; // ============================================================================ // BACKGROUND MESSAGE HANDLER // ============================================================================ @pragma('vm:entry-point') Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); preferences = await SharedPreferences.getInstance(); await initializeNotificationPlugin(); final notificationType = parseNotificationType( message.data['notificationType'], ); // Handle call cancellation - now shows missed call if (notificationType == ApiEnum.CallingNotificationCancel) { await handleCallCancellation(message); return; } // Handle incoming call notification - with active call check if (notificationType == ApiEnum.CallingNotification) { await showCallNotification(message); return; } } // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ int? parseNotificationType(dynamic notificationType) { if (notificationType is int) return notificationType; if (notificationType is String) return int.tryParse(notificationType); return null; } Map parseCallPayload(Map data) { Map callPayload = {...data}; if (data['dataId'] != null) { try { if (data['dataId'] is String) { Map parsedDataId = jsonDecode(data['dataId']); callPayload.addAll(parsedDataId); } else if (data['dataId'] is Map) { callPayload.addAll(Map.from(data['dataId'])); } } catch (e, stackTrace) { final logger = LoggerService(); logger.error( "Failed to parse call payload dataId", error: e, stackTrace: stackTrace, ); } } return callPayload; } String? extractChannelName(Map data) { try { if (data['ChannelName'] != null) return data['ChannelName']; if (data['dataId'] != null) { if (data['dataId'] is String) { final parsed = jsonDecode(data['dataId']); return parsed['ChannelName']; } else if (data['dataId'] is Map) { return data['dataId']['ChannelName']; } } return null; } catch (e, stackTrace) { final logger = LoggerService(); logger.error( "Failed to extract channel name from notification data", error: e, stackTrace: stackTrace, ); return null; } } // ============================================================================ // BACKGROUND NOTIFICATION INITIALIZATION // ============================================================================ Future initializeNotificationPlugin() async { const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); // iOS settings with notification categories for call actions final DarwinInitializationSettings iosSettings = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, notificationCategories: [ DarwinNotificationCategory( 'call_category', actions: [ DarwinNotificationAction.plain( 'accept_call', 'Accept', options: { DarwinNotificationActionOption.foreground, }, ), DarwinNotificationAction.plain( 'reject_call', 'Reject', options: { DarwinNotificationActionOption.destructive, }, ), ], options: { DarwinNotificationCategoryOption.hiddenPreviewShowTitle, }, ), ], ); final InitializationSettings initSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, ); await globalNotificationsPlugin.initialize(initSettings); // Create notification channels if (Platform.isAndroid) { // Call notification channel const AndroidNotificationChannel callChannel = AndroidNotificationChannel( 'high_importance_channel', 'High Importance Notifications', description: 'This channel is used for important notifications.', importance: Importance.max, ); // NEW: Missed call notification channel const AndroidNotificationChannel missedCallChannel = AndroidNotificationChannel( 'missed_call_channel', 'Missed Call Notifications', description: 'Notifications for missed calls', importance: Importance.high, ); final androidPlugin = globalNotificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin >(); await androidPlugin?.createNotificationChannel(callChannel); await androidPlugin?.createNotificationChannel(missedCallChannel); } if (Platform.isIOS) { await globalNotificationsPlugin .resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin >() ?.requestPermissions(alert: true, badge: true, sound: true); } } // ============================================================================ // CALL NOTIFICATION DISPLAY // ============================================================================ Future showCallNotification(RemoteMessage message) async { final callPayload = parseCallPayload(message.data); String title = 'Incoming Call'; String body = 'You have an incoming call'; if (callPayload['CallerName'] != null) { title = 'Call from ${callPayload['CallerName']}'; } if (callPayload['CallType'] != null) { final callType = callPayload['CallType'].toString().toLowerCase(); body = callType == 'video' ? 'Incoming video call' : 'Incoming voice call'; } // Pre-fetch RTC token in background while showing notification final channelName = extractChannelName(callPayload); preferences = await SharedPreferences.getInstance(); if (channelName != null && SharedServices.isLoggedIn()) { preFetchRTCToken(channelName, callPayload); } AndroidNotificationDetails androidDetails = AndroidNotificationDetails( 'high_importance_channel', 'High Importance Notifications', channelDescription: 'Notifications for incoming calls', importance: Importance.max, priority: Priority.max, showWhen: true, ongoing: false, autoCancel: true, fullScreenIntent: true, category: AndroidNotificationCategory.call, enableVibration: true, enableLights: true, color: Color(primaryColor), icon: '@drawable/ic_stat_notification', actions: [ AndroidNotificationAction( 'accept_call', '📞 Accept', showsUserInterface: true, titleColor: Colors.green, ), AndroidNotificationAction( 'reject_call', '✖️ Reject', cancelNotification: true, titleColor: Colors.red, showsUserInterface: true, ), ], ); // iOS notification details with category for actions const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, categoryIdentifier: 'call_category', interruptionLevel: InterruptionLevel.timeSensitive, ); NotificationDetails platformDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); final notificationId = message.hashCode; await globalNotificationsPlugin.show( notificationId, title, body, platformDetails, payload: jsonEncode(callPayload), ); if (channelName != null) { activeCallNotifications[channelName] = notificationId; } } // ============================================================================ // CALL CANCELLATION HANDLER - NOW SHOWS MISSED CALL // ============================================================================ Future handleCallCancellation(RemoteMessage message) async { try { final callPayload = parseCallPayload(message.data); final channelName = extractChannelName(callPayload); if (channelName != null) { final notificationId = activeCallNotifications[channelName]; if (notificationId != null) { await globalNotificationsPlugin.cancel(notificationId); activeCallNotifications.remove(channelName); } await showMissedCallNotification(callPayload); } } catch (e, stackTrace) { final logger = LoggerService(); logger.error( "Failed to handle call cancellation", error: e, stackTrace: stackTrace, ); } } // ============================================================================ // NEW: MISSED CALL NOTIFICATION // ============================================================================ Future showMissedCallNotification( Map callPayload, ) async { String callerName = callPayload['CallerName'] ?? 'Unknown'; String callType = callPayload['CallType']?.toString().toLowerCase() ?? 'voice'; String title = 'Missed Call'; String body = 'You missed a $callType call from $callerName'; AndroidNotificationDetails androidDetails = AndroidNotificationDetails( 'missed_call_channel', 'Missed Call Notifications', channelDescription: 'Notifications for missed calls', importance: Importance.high, priority: Priority.high, showWhen: true, autoCancel: true, color: Color(primaryColor), icon: '@drawable/ic_stat_notification', styleInformation: BigTextStyleInformation(body), ); const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); NotificationDetails platformDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); // Generate unique ID for missed call notification final missedCallId = DateTime.now().millisecondsSinceEpoch ~/ 1000; await globalNotificationsPlugin.show( missedCallId, title, body, platformDetails, payload: jsonEncode(callPayload), ); } // ============================================================================ // NOTIFICATION SERVICE CLASS // ============================================================================ class NotificationService extends GetxService { static NotificationService get instance => Get.find(); final logger = LoggerService(); String? _deviceToken; String? get deviceToken => _deviceToken; bool _isInitialized = false; @override Future onInit() async { if (_isInitialized) { return; } _isInitialized = true; super.onInit(); // Follow the Below Order............................................ // await Firebase.initializeApp(); await getDeviceToken(); await FirebaseMessaging.instance.getAPNSToken(); await initializeLocalNotifications(); await processInitialNotificationLaunch(); await processNotificationActionButtonTerminatedApp(); await setupMessageHandlers(); await FirebaseMessaging.instance.setAutoInitEnabled(true); // .................................................................... } // ============================================================================ // INITIAL LAUNCH HANDLING // ============================================================================ Future processInitialNotificationLaunch() async { try { final details = await globalNotificationsPlugin.getNotificationAppLaunchDetails(); if (details?.didNotificationLaunchApp ?? false) { wasLaunchedFromTerminatedState = true; final payload = details!.notificationResponse?.payload; final actionId = details.notificationResponse?.actionId; if (payload != null) { try { final data = jsonDecode(payload); if (actionId == 'accept_call' || actionId == 'reject_call') { pendingCallAction = {'action': actionId, 'data': data}; } else { await handleNotificationNavigation(data); } } catch (e, stackTrace) { logger.error( "Failed to parse initial launch notification payload", error: e, stackTrace: stackTrace, ); } } } else { wasLaunchedFromTerminatedState = false; } } catch (e, stackTrace) { logger.error( "Failed to process initial notification launch", error: e, stackTrace: stackTrace, ); } } Future processNotificationActionButtonTerminatedApp() async { if (pendingCallAction == null) { return; } int retries = 0; while (!Get.isRegistered() && retries < 30) { await Future.delayed(Duration(milliseconds: 100)); retries++; } if (!Get.isRegistered()) { pendingCallAction = null; return; } final agoraService = Get.find(); await agoraService.init(); final action = pendingCallAction!['action']; final data = pendingCallAction!['data']; if (action == 'accept_call') { await handleAcceptCall(data); } else if (action == 'reject_call') { await handleRejectCall(data); } pendingCallAction = null; } // ============================================================================ // FIREBASE & LOCAL NOTIFICATIONS SETUP // ============================================================================ Future initializeLocalNotifications() async { const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); // iOS settings with notification categories for call actions final DarwinInitializationSettings iosSettings = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, notificationCategories: [ DarwinNotificationCategory( 'call_category', actions: [ DarwinNotificationAction.plain( 'accept_call', '📞 Accept', options: { DarwinNotificationActionOption.foreground, }, ), DarwinNotificationAction.plain( 'reject_call', '✖️ Reject', options: { DarwinNotificationActionOption.destructive, }, ), ], options: { DarwinNotificationCategoryOption.hiddenPreviewShowTitle, }, ), ], ); final InitializationSettings initSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, ); await globalNotificationsPlugin.initialize( initSettings, onDidReceiveNotificationResponse: onNotificationTapped, ); if (Platform.isAndroid) { await createNotificationChannel(); } else if (Platform.isIOS) { await createNotificationChannelIOS(); } } Future createNotificationChannel() async { // Call notification channel const AndroidNotificationChannel callChannel = AndroidNotificationChannel( 'high_importance_channel', 'High Importance Notifications', description: 'This channel is used for important notifications.', importance: Importance.max, ); // NEW: Missed call channel const AndroidNotificationChannel missedCallChannel = AndroidNotificationChannel( 'missed_call_channel', 'Missed Call Notifications', description: 'Notifications for missed calls', importance: Importance.high, ); final androidPlugin = globalNotificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin >(); await androidPlugin?.createNotificationChannel(callChannel); await androidPlugin?.createNotificationChannel(missedCallChannel); } Future createNotificationChannelIOS() async { await globalNotificationsPlugin .resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin >() ?.requestPermissions(alert: true, badge: true, sound: true); } // ============================================================================ // PERMISSION HANDLING // ============================================================================ Future requestPermissions() async { await FirebaseMessaging.instance.requestPermission( alert: true, badge: true, sound: true, carPlay: false, criticalAlert: false, provisional: false, ); await Future.delayed(const Duration(milliseconds: 1000)); if (Platform.isAndroid) { final status = await Permission.notification.status; if (status != PermissionStatus.granted) { final requestStatus = await Permission.notification.request(); if (requestStatus.isPermanentlyDenied) { await showPermissionSettingsDialog(); } } } } Future showPermissionSettingsDialog() async { if (Get.context != null) { final result = await Get.dialog( AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), title: Row( children: [ Icon(Icons.notifications_off, color: Colors.orange, size: 24), const SizedBox(width: 8), const Text( 'Notification Permission', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'To receive important notifications, please enable notifications in your device settings.', style: TextStyle(fontSize: 16), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'How to enable:', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), ), const SizedBox(height: 4), Text( Platform.isIOS ? '1. Go to Settings\n2. Find OnuFitness\n3. Tap Notifications\n4. Enable Allow Notifications' : '1. Go to Settings\n2. Find Apps\n3. Select OnuFitness\n4. Enable Notifications', style: const TextStyle(fontSize: 13), ), ], ), ), ], ), actions: [ TextButton( onPressed: () => Get.back(result: false), child: const Text( 'Maybe Later', style: TextStyle(color: Colors.grey), ), ), ElevatedButton( onPressed: () => Get.back(result: true), style: ElevatedButton.styleFrom( backgroundColor: const Color(primaryColor), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: const Text( 'Open Settings', style: TextStyle(color: Colors.black), ), ), ], ), barrierDismissible: false, ); if (result == true) { await openAppSettings(); } } else { await openAppSettings(); } } // ============================================================================ // MESSAGE HANDLERS // ============================================================================ Future setupMessageHandlers() async { FirebaseMessaging.onMessage.listen(handleForegroundMessage); FirebaseMessaging.onMessageOpenedApp.listen( willOpenTerminatedAppOnNotificationTap, ); RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { await willOpenTerminatedAppOnNotificationTap(initialMessage); } } Future handleForegroundMessage(RemoteMessage message) async { final notificationType = parseNotificationType( message.data['notificationType'], ); if (notificationType == ApiEnum.CallingNotification) { return; } if ((notificationType == ApiEnum.ChatNotification || notificationType == ApiEnum.GroupChatNotification) && (Get.currentRoute == RouteConstant.chatListScreen || Get.currentRoute == "/ChatDetailScreen")) { return; } if (notificationType == ApiEnum.CallingNotificationCancel) { await handleCallCancellation(message); return; } await showLocalNotification(message); } Future willOpenTerminatedAppOnNotificationTap( RemoteMessage message, ) async { await handleNotificationNavigation(message.data); } Future showLocalNotification(RemoteMessage message) async { const AndroidNotificationDetails androidDetails = AndroidNotificationDetails( 'high_importance_channel', 'High Importance Notifications', channelDescription: 'This channel is used for important notifications.', importance: Importance.high, priority: Priority.high, showWhen: true, color: Color(primaryColor), icon: '@drawable/ic_stat_notification', ); const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); const NotificationDetails platformDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); await globalNotificationsPlugin.show( message.hashCode, message.notification?.title ?? 'New Notification', message.notification?.body ?? 'You have a new message', platformDetails, payload: jsonEncode(message.data), ); } // ============================================================================ // NOTIFICATION TAP HANDLER // ============================================================================ void onNotificationTapped(NotificationResponse response) async { preferences = await SharedPreferences.getInstance(); if (response.id != null) { await globalNotificationsPlugin.cancel(response.id!); } if (response.actionId != null) { await handleNotificationAction(response); return; } if (response.payload != null) { try { final data = jsonDecode(response.payload!); final notificationType = parseNotificationType( data['notificationType'], ); // Dismiss call notifications when tapped if (notificationType == ApiEnum.CallingNotification) { final callDataRaw = data['dataId']; if (callDataRaw != null) { Map? callData; try { callData = callDataRaw is String ? jsonDecode(callDataRaw) : (callDataRaw as Map); } catch (_) {} if (callData != null) { final notificationId = callData.hashCode; final channelName = extractChannelName(callData); if (channelName != null) { activeCallNotifications.remove(channelName); } await globalNotificationsPlugin.cancel(notificationId); } } globalNotificationsPlugin.cancel(response.id!); } if (SharedServices.isLoggedIn() == true) { await handleNotificationNavigation(data); } } catch (e, stackTrace) { logger.error( "Failed to handle notification tap", error: e, stackTrace: stackTrace, ); await globalNotificationsPlugin.cancelAll(); } } } Future handleNotificationAction(NotificationResponse response) async { if (response.payload == null) return; try { final callData = jsonDecode(response.payload!); switch (response.actionId) { case 'accept_call': await handleAcceptCall(callData); break; case 'reject_call': await handleRejectCall(callData); break; } } catch (e, stackTrace) { logger.error( "Failed to handle notification action: ${response.actionId}", error: e, stackTrace: stackTrace, ); } } // ============================================================================ // CALL ACTION HANDLERS // ============================================================================= //................................................................................... static Future handleAcceptCall(Map? callData) async { final logger = LoggerService(); try { final notificationNavigationController = Get.find(); notificationNavigationController.isCallingNotificationNavigation.value = true; // Parse call data ONCE Map? actualCallData; if (callData != null && callData['dataId'] != null) { try { if (callData['dataId'] is String) { actualCallData = jsonDecode(callData['dataId']); } else if (callData['dataId'] is Map) { actualCallData = Map.from(callData['dataId']); } } catch (e, stackTrace) { logger.error( "Failed to parse call data during accept", error: e, stackTrace: stackTrace, ); } } Map dataToUse = actualCallData ?? callData ?? {}; if (dataToUse.isEmpty) { return; } if (Get.currentRoute != RouteConstant.activeCallScreen) { Get.offNamed(RouteConstant.activeCallScreen); } // Extract all data at once String channelName = dataToUse['ChannelName'] ?? ''; String remoteUid = dataToUse['UserID'] ?? ''; String callerName = dataToUse['CallerName'] ?? ''; String callerImage = dataToUse['ProfilePicture'] ?? ''; String rtcType = dataToUse['CallType'] ?? 'VOICE'; if (channelName.isEmpty || remoteUid.isEmpty) { return; } // Dismiss notification immediately final notificationId = activeCallNotifications[channelName]; if (notificationId != null) { globalNotificationsPlugin.cancel(notificationId); activeCallNotifications.remove(channelName); } // Wait for app initialization if needed if (wasLaunchedFromTerminatedState) { await Future.delayed(Duration(milliseconds: 300)); } // Initialize Agora service if (!Get.isRegistered()) { Get.put(AgoraCallService()); } final agoraService = Get.find(); await agoraService.init(); final callType = rtcType == 'VIDEO' ? CallType.video : CallType.voice; agoraService.setCallDataBatch( channel: channelName, userId: remoteUid, name: callerName, profilePic: callerImage, callType: callType, isCaller: false, state: CallState.ringing, ); // Get token and start video preview in parallel final tokenFuture = getOrUseCachedToken(channelName, agoraService); final videoFuture = callType == CallType.video ? agoraService.startVideoPreview() : Future.value(); await Future.wait([tokenFuture, videoFuture]); await agoraService.acceptCall(callType: callType); } catch (e, stackTrace) { logger.error( "Failed to handle accept call action", error: e, stackTrace: stackTrace, ); } } //................................................................................... static Future handleRejectCall(Map? callData) async { final logger = LoggerService(); try { // Dismiss notification FIRST if (callData != null) { final channelName = extractChannelName(callData); if (channelName != null) { final notificationId = activeCallNotifications[channelName]; if (notificationId != null) { await globalNotificationsPlugin.cancel(notificationId); activeCallNotifications.remove(channelName); } } } // Parse & SET FULL STATE Map? actualCallData; if (callData != null && callData['dataId'] != null) { try { if (callData['dataId'] is String) { actualCallData = jsonDecode(callData['dataId']); } else if (callData['dataId'] is Map) { actualCallData = Map.from(callData['dataId']); } } catch (e, stackTrace) { logger.error( "Failed to parse call data during reject", error: e, stackTrace: stackTrace, ); } } Map dataToUse = actualCallData ?? callData ?? {}; if (dataToUse.isEmpty) { await globalNotificationsPlugin.cancelAll(); return; } if (!Get.isRegistered()) { Get.put(AgoraCallService()); } final agoraService = Get.find(); await agoraService.init(); if (dataToUse.isNotEmpty) { String channelName = dataToUse['ChannelName'] ?? ''; String remoteUid = dataToUse['UserID'] ?? ''; String callerName = dataToUse['CallerName'] ?? ''; String rtcType = dataToUse['CallType'] ?? 'VOICE'; // ✅ Set ALL required state agoraService.channelName.value = channelName; agoraService.remoteUserId.value = remoteUid; agoraService.callerName.value = callerName; agoraService.currentCallType.value = rtcType == 'VIDEO' ? CallType.video : CallType.voice; agoraService.amICaller.value = false; agoraService.callState.value = CallState.ringing; } await agoraService.rejectCall(); await globalNotificationsPlugin.cancelAll(); } catch (e, stackTrace) { logger.error( "Failed to handle reject call action", error: e, stackTrace: stackTrace, ); } } // ============================================================================ // NOTIFICATION NAVIGATION // ============================================================================ static Future handleNotificationNavigation( Map data, ) async { await Future.delayed(const Duration(milliseconds: 500)); try { // Dismiss call notification if applicable final notificationType = parseNotificationType(data['notificationType']); if (notificationType == ApiEnum.CallingNotification) { final callDataRaw = data['dataId']; if (callDataRaw != null) { Map? callData; try { callData = callDataRaw is String ? jsonDecode(callDataRaw) : Map.from(callDataRaw); } catch (_) {} if (callData != null) { final channelName = extractChannelName(callData); if (channelName != null) { final notificationId = activeCallNotifications[channelName]; if (notificationId != null) { await globalNotificationsPlugin.cancel(notificationId); activeCallNotifications.remove(channelName); } } } } } } catch (e, stackTrace) { final logger = LoggerService(); logger.error( "Failed to dismiss notification before navigation", error: e, stackTrace: stackTrace, ); await globalNotificationsPlugin.cancelAll(); } final navigationController = Get.find(); await navigationController.navigateBasedOnNotification(data); } // ============================================================================ // FCM TOKEN MANAGEMENT // ============================================================================ Future getDeviceToken() async { try { _deviceToken = await FirebaseMessaging.instance.getToken(); FirebaseMessaging.instance.onTokenRefresh.listen((newToken) { _deviceToken = newToken; }); } catch (e, stackTrace) { logger.error( "Failed to get FCM device token", error: e, stackTrace: stackTrace, ); } } Future deleteDeviceToken() async { try { await FirebaseMessaging.instance.deleteToken(); _deviceToken = null; } catch (e, stackTrace) { logger.error( "Failed to delete FCM token", error: e, stackTrace: stackTrace, ); } } RxBool sendFcmToBackendLoading = false.obs; Future sendTokenToBackend() async { sendFcmToBackendLoading(true); bool ret = false; try { if (_deviceToken != null) { final response = await ApiBase.patchRequest( extendedURL: ApiUrl.sendFcmToken, body: {"deviceFCMToken": _deviceToken}, ); if (response.statusCode == 200 || response.statusCode == 201) { ret = true; } } } catch (e, stackTrace) { logger.error( "Failed to send FCM token to backend", error: e, stackTrace: stackTrace, ); } finally { sendFcmToBackendLoading(false); } return ret; } } // =======OUTSIDE THE CLASS==================================================== // ============================================================================ // ============================================================================ // Cache for pre-fetched tokens final Map cachedRTCTokens = {}; void preFetchRTCToken(String channelName, Map callData) async { try { if (!Get.isRegistered()) { Get.put(AgoraTokenController()); } final agoraTokenController = Get.find(); bool tokenSuccess = await agoraTokenController.getRTCtoken( channelName: channelName, role: ApiEnum.SUBSCRIBER, ); if (tokenSuccess) { cachedRTCTokens[channelName] = agoraTokenController.rTCtoken.value; } } catch (e, stackTrace) { final logger = LoggerService(); logger.error( "Failed to pre-fetch RTC token for channel: $channelName", error: e, stackTrace: stackTrace, ); } } //............................................................................................. Future getOrUseCachedToken( String channelName, AgoraCallService agoraService, ) async { // Check if we have a cached token if (cachedRTCTokens.containsKey(channelName)) { agoraService.rtcToken.value = cachedRTCTokens[channelName]!; cachedRTCTokens.remove(channelName); return; } // No cached token, fetch it now if (!Get.isRegistered()) { Get.put(AgoraTokenController()); } final agoraTokenController = Get.find(); bool tokenSuccess = await agoraTokenController.getRTCtoken( channelName: channelName, role: ApiEnum.SUBSCRIBER, ); if (tokenSuccess) { agoraService.rtcToken.value = agoraTokenController.rTCtoken.value; } else { throw Exception('Failed to get RTC token'); } } // ============Above one is without Terminated Call RINGTONE================================================================ //................................................................................................................................. // In the Below code Backend should send the "category": "call_category" in the APN body to show the Accept and reject Buttons in IOS.... //................................................................................................................................. // ============ Below one is with Terminated Call RINGTONE================================================================ // import 'dart:convert'; // import 'dart:io'; // import 'package:firebase_core/firebase_core.dart'; // import 'package:firebase_messaging/firebase_messaging.dart'; // import 'package:flutter/material.dart'; // import 'package:flutter_local_notifications/flutter_local_notifications.dart'; // import 'package:get/get.dart'; // import 'package:onufitness/constants/api_endpoints.dart'; // import 'package:onufitness/constants/api_enum_constant.dart'; // import 'package:onufitness/constants/color_constant.dart'; // import 'package:onufitness/constants/constant.dart'; // import 'package:onufitness/firebase_options.dart'; // import 'package:onufitness/routes/route_constant.dart'; // import 'package:onufitness/services/api_services/base_api_services.dart'; // import 'package:onufitness/services/local_storage_services/shared_services.dart'; // import 'package:onufitness/services/notification_services/navigation_controller.dart'; // import 'package:onufitness/services/logger_service.dart'; // import 'package:onufitness/services/agora/call_services.dart'; // import 'package:onufitness/controller/get_agora_token_controller.dart'; // import 'package:permission_handler/permission_handler.dart'; // import 'dart:async'; // import 'package:shared_preferences/shared_preferences.dart'; // // ============================================================================ // // GLOBAL SHARED INSTANCES // // ============================================================================ // final FlutterLocalNotificationsPlugin globalNotificationsPlugin = // FlutterLocalNotificationsPlugin(); // final Map activeCallNotifications = {}; // // NEW: Track if there's an active call notification // // bool hasActiveCallNotification = false; // // String? activeCallChannelName; // // Global flag to track app launch state // bool wasLaunchedFromTerminatedState = false; // Map? pendingCallAction; // // ============================================================================ // // BACKGROUND MESSAGE HANDLER // // ============================================================================ // @pragma('vm:entry-point') // Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); // preferences = await SharedPreferences.getInstance(); // await initializeNotificationPlugin(); // final notificationType = parseNotificationType( // message.data['notificationType'], // ); // // Handle call cancellation - now shows missed call // if (notificationType == ApiEnum.CallingNotificationCancel) { // await handleCallCancellation(message); // return; // } // // Handle incoming call notification - with active call check // if (notificationType == ApiEnum.CallingNotification) { // await showCallNotification(message); // return; // } // } // // ============================================================================ // // UTILITY FUNCTIONS // // ============================================================================ // int? parseNotificationType(dynamic notificationType) { // if (notificationType is int) return notificationType; // if (notificationType is String) return int.tryParse(notificationType); // return null; // } // Map parseCallPayload(Map data) { // Map callPayload = {...data}; // if (data['dataId'] != null) { // try { // if (data['dataId'] is String) { // Map parsedDataId = jsonDecode(data['dataId']); // callPayload.addAll(parsedDataId); // } else if (data['dataId'] is Map) { // callPayload.addAll(Map.from(data['dataId'])); // } // } catch (e, stackTrace) { // final logger = LoggerService(); // logger.error( // "Failed to parse call payload dataId", // error: e, // stackTrace: stackTrace, // ); // } // } // return callPayload; // } // String? extractChannelName(Map data) { // try { // if (data['ChannelName'] != null) return data['ChannelName']; // if (data['dataId'] != null) { // if (data['dataId'] is String) { // final parsed = jsonDecode(data['dataId']); // return parsed['ChannelName']; // } else if (data['dataId'] is Map) { // return data['dataId']['ChannelName']; // } // } // return null; // } catch (e, stackTrace) { // final logger = LoggerService(); // logger.error( // "Failed to extract channel name from notification data", // error: e, // stackTrace: stackTrace, // ); // return null; // } // } // // ============================================================================ // // BACKGROUND NOTIFICATION INITIALIZATION // // ============================================================================ // // In initializeNotificationPlugin() function: // Future initializeNotificationPlugin() async { // const AndroidInitializationSettings androidSettings = // AndroidInitializationSettings('@mipmap/ic_launcher'); // final DarwinInitializationSettings iosSettings = DarwinInitializationSettings( // requestAlertPermission: true, // requestBadgePermission: true, // requestSoundPermission: true, // notificationCategories: [ // DarwinNotificationCategory( // 'call_category', // actions: [ // DarwinNotificationAction.plain( // 'accept_call', // 'Accept', // options: { // DarwinNotificationActionOption.foreground, // }, // ), // DarwinNotificationAction.plain( // 'reject_call', // 'Reject', // options: { // DarwinNotificationActionOption.destructive, // }, // ), // ], // options: { // DarwinNotificationCategoryOption.hiddenPreviewShowTitle, // }, // ), // ], // ); // final InitializationSettings initSettings = InitializationSettings( // android: androidSettings, // iOS: iosSettings, // ); // await globalNotificationsPlugin.initialize(initSettings); // if (Platform.isAndroid) { // // ✅ CHANNEL 1: CALL CHANNEL - WITH RINGTONE // final AndroidNotificationChannel callChannel = AndroidNotificationChannel( // 'call_channel_v1', // ⚠️ CHANGE ID if you want fresh channel // 'Incoming Calls', // description: 'Notifications for incoming voice and video calls', // importance: Importance.max, // playSound: true, // sound: RawResourceAndroidNotificationSound('ringtone'), // enableVibration: true, // enableLights: true, // ); // // ✅ CHANNEL 2: DEFAULT CHANNEL - NO RINGTONE // const AndroidNotificationChannel defaultChannel = // AndroidNotificationChannel( // 'default_channel_v1', // ⚠️ CHANGE ID if you want fresh channel // 'General Notifications', // description: 'Chat messages and general notifications', // importance: Importance.high, // playSound: true, // // NO sound parameter = default system sound // enableVibration: true, // ); // // ✅ CHANNEL 3: MISSED CALL CHANNEL - NO RINGTONE // const AndroidNotificationChannel missedCallChannel = // AndroidNotificationChannel( // 'missed_call_channel_v1', // ⚠️ CHANGE ID if you want fresh channel // 'Missed Call Notifications', // description: 'Notifications for missed calls', // importance: Importance.high, // playSound: true, // // NO sound parameter = default system sound // ); // final androidPlugin = // globalNotificationsPlugin // .resolvePlatformSpecificImplementation< // AndroidFlutterLocalNotificationsPlugin // >(); // await androidPlugin?.createNotificationChannel(callChannel); // await androidPlugin?.createNotificationChannel(defaultChannel); // await androidPlugin?.createNotificationChannel(missedCallChannel); // } // if (Platform.isIOS) { // await globalNotificationsPlugin // .resolvePlatformSpecificImplementation< // IOSFlutterLocalNotificationsPlugin // >() // ?.requestPermissions(alert: true, badge: true, sound: true); // } // } // // ============================================================================ // // CALL NOTIFICATION DISPLAY // // ============================================================================ // Future showCallNotification(RemoteMessage message) async { // final callPayload = parseCallPayload(message.data); // String title = 'Incoming Call'; // String body = 'You have an incoming call'; // if (callPayload['CallerName'] != null) { // title = 'Call from ${callPayload['CallerName']}'; // } // if (callPayload['CallType'] != null) { // final callType = callPayload['CallType'].toString().toLowerCase(); // body = callType == 'video' ? 'Incoming video call' : 'Incoming voice call'; // } // final channelName = extractChannelName(callPayload); // preferences = await SharedPreferences.getInstance(); // if (channelName != null && SharedServices.isLoggedIn()) { // preFetchRTCToken(channelName, callPayload); // } // // ✅ MUST USE: call_channel_v1 // AndroidNotificationDetails androidDetails = AndroidNotificationDetails( // 'call_channel_v1', // ✅ Use call channel // 'Incoming Calls', // channelDescription: 'Notifications for incoming calls', // importance: Importance.max, // priority: Priority.max, // showWhen: true, // ongoing: false, // autoCancel: true, // fullScreenIntent: true, // category: AndroidNotificationCategory.call, // enableVibration: true, // enableLights: true, // color: Color(primaryColor), // icon: '@drawable/ic_stat_notification', // sound: RawResourceAndroidNotificationSound('ringtone'), // playSound: true, // actions: [ // AndroidNotificationAction( // 'accept_call', // '📞 Accept', // showsUserInterface: true, // titleColor: Colors.green, // ), // AndroidNotificationAction( // 'reject_call', // '✖️ Reject', // cancelNotification: true, // titleColor: Colors.red, // showsUserInterface: true, // ), // ], // ); // const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( // presentAlert: true, // presentBadge: true, // presentSound: true, // sound: 'ringtone.caf', // categoryIdentifier: 'call_category', // interruptionLevel: InterruptionLevel.timeSensitive, // ); // NotificationDetails platformDetails = NotificationDetails( // android: androidDetails, // iOS: iosDetails, // ); // final notificationId = message.hashCode; // await globalNotificationsPlugin.show( // notificationId, // title, // body, // platformDetails, // payload: jsonEncode(callPayload), // ); // if (channelName != null) { // activeCallNotifications[channelName] = notificationId; // } // } // // ============================================================================ // // CALL CANCELLATION HANDLER - NOW SHOWS MISSED CALL // // ============================================================================ // Future handleCallCancellation(RemoteMessage message) async { // try { // final callPayload = parseCallPayload(message.data); // final channelName = extractChannelName(callPayload); // if (channelName != null) { // final notificationId = activeCallNotifications[channelName]; // if (notificationId != null) { // await globalNotificationsPlugin.cancel(notificationId); // activeCallNotifications.remove(channelName); // } // await showMissedCallNotification(callPayload); // } // } catch (e, stackTrace) { // final logger = LoggerService(); // logger.error( // "Failed to handle call cancellation", // error: e, // stackTrace: stackTrace, // ); // } // } // // ============================================================================ // // NEW: MISSED CALL NOTIFICATION // // ============================================================================ // Future showMissedCallNotification( // Map callPayload, // ) async { // String callerName = callPayload['CallerName'] ?? 'Unknown'; // String callType = // callPayload['CallType']?.toString().toLowerCase() ?? 'voice'; // String title = 'Missed Call'; // String body = 'You missed a $callType call from $callerName'; // // ✅ MUST USE: missed_call_channel_v1 // AndroidNotificationDetails androidDetails = AndroidNotificationDetails( // 'missed_call_channel_v1', // ✅ Use missed call channel // 'Missed Call Notifications', // channelDescription: 'Notifications for missed calls', // importance: Importance.high, // priority: Priority.high, // showWhen: true, // autoCancel: true, // color: Color(primaryColor), // icon: '@drawable/ic_stat_notification', // styleInformation: BigTextStyleInformation(body), // // ❌ NO sound parameter = default // ); // const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( // presentAlert: true, // presentBadge: true, // presentSound: true, // // ❌ NO sound parameter = default // ); // NotificationDetails platformDetails = NotificationDetails( // android: androidDetails, // iOS: iosDetails, // ); // final missedCallId = DateTime.now().millisecondsSinceEpoch ~/ 1000; // await globalNotificationsPlugin.show( // missedCallId, // title, // body, // platformDetails, // payload: jsonEncode(callPayload), // ); // } // // ============================================================================ // // NOTIFICATION SERVICE CLASS // // ============================================================================ // class NotificationService extends GetxService { // static NotificationService get instance => Get.find(); // final logger = LoggerService(); // String? _deviceToken; // String? get deviceToken => _deviceToken; // bool _isInitialized = false; // @override // Future onInit() async { // if (_isInitialized) { // return; // } // _isInitialized = true; // super.onInit(); // // Follow the Below Order............................................ // // await Firebase.initializeApp(); // await getDeviceToken(); // await FirebaseMessaging.instance.getAPNSToken(); // await initializeLocalNotifications(); // await processInitialNotificationLaunch(); // await processNotificationActionButtonTerminatedApp(); // await setupMessageHandlers(); // await FirebaseMessaging.instance.setAutoInitEnabled(true); // // .................................................................... // } // // ============================================================================ // // INITIAL LAUNCH HANDLING // // ============================================================================ // Future processInitialNotificationLaunch() async { // try { // final details = // await globalNotificationsPlugin.getNotificationAppLaunchDetails(); // if (details?.didNotificationLaunchApp ?? false) { // wasLaunchedFromTerminatedState = true; // final payload = details!.notificationResponse?.payload; // final actionId = details.notificationResponse?.actionId; // if (payload != null) { // try { // final data = jsonDecode(payload); // if (actionId == 'accept_call' || actionId == 'reject_call') { // pendingCallAction = {'action': actionId, 'data': data}; // } else { // await handleNotificationNavigation(data); // } // } catch (e, stackTrace) { // logger.error( // "Failed to parse initial launch notification payload", // error: e, // stackTrace: stackTrace, // ); // } // } // } else { // wasLaunchedFromTerminatedState = false; // } // } catch (e, stackTrace) { // logger.error( // "Failed to process initial notification launch", // error: e, // stackTrace: stackTrace, // ); // } // } // Future processNotificationActionButtonTerminatedApp() async { // if (pendingCallAction == null) { // return; // } // int retries = 0; // while (!Get.isRegistered() && retries < 30) { // await Future.delayed(Duration(milliseconds: 100)); // retries++; // } // if (!Get.isRegistered()) { // pendingCallAction = null; // return; // } // final agoraService = Get.find(); // await agoraService.init(); // final action = pendingCallAction!['action']; // final data = pendingCallAction!['data']; // if (action == 'accept_call') { // await handleAcceptCall(data); // } else if (action == 'reject_call') { // await handleRejectCall(data); // } // pendingCallAction = null; // } // // ============================================================================ // // FIREBASE & LOCAL NOTIFICATIONS SETUP // // ============================================================================ // Future initializeLocalNotifications() async { // const AndroidInitializationSettings androidSettings = // AndroidInitializationSettings('@mipmap/ic_launcher'); // // iOS settings with notification categories for call actions // final DarwinInitializationSettings iosSettings = // DarwinInitializationSettings( // requestAlertPermission: true, // requestBadgePermission: true, // requestSoundPermission: true, // notificationCategories: [ // DarwinNotificationCategory( // 'call_category', // actions: [ // DarwinNotificationAction.plain( // 'accept_call', // '📞 Accept', // options: { // DarwinNotificationActionOption.foreground, // }, // ), // DarwinNotificationAction.plain( // 'reject_call', // '✖️ Reject', // options: { // DarwinNotificationActionOption.destructive, // }, // ), // ], // options: { // DarwinNotificationCategoryOption.hiddenPreviewShowTitle, // }, // ), // ], // ); // final InitializationSettings initSettings = InitializationSettings( // android: androidSettings, // iOS: iosSettings, // ); // await globalNotificationsPlugin.initialize( // initSettings, // onDidReceiveNotificationResponse: onNotificationTapped, // ); // if (Platform.isAndroid) { // await createNotificationChannel(); // } else if (Platform.isIOS) { // await createNotificationChannelIOS(); // } // } // Future createNotificationChannel() async { // // ✅ CHANNEL 1: CALL CHANNEL - WITH RINGTONE // final AndroidNotificationChannel callChannel = AndroidNotificationChannel( // 'call_channel_v1', // 'Incoming Calls', // description: 'Notifications for incoming voice and video calls', // importance: Importance.max, // playSound: true, // sound: RawResourceAndroidNotificationSound('ringtone'), // enableVibration: true, // ); // // ✅ CHANNEL 2: DEFAULT CHANNEL - NO RINGTONE // const AndroidNotificationChannel defaultChannel = // AndroidNotificationChannel( // 'default_channel_v1', // 'General Notifications', // description: 'Chat messages and general notifications', // importance: Importance.high, // playSound: true, // // NO sound = default // ); // // ✅ CHANNEL 3: MISSED CALL CHANNEL - NO RINGTONE // const AndroidNotificationChannel missedCallChannel = // AndroidNotificationChannel( // 'missed_call_channel_v1', // 'Missed Call Notifications', // description: 'Notifications for missed calls', // importance: Importance.high, // playSound: true, // // NO sound = default // ); // final androidPlugin = // globalNotificationsPlugin // .resolvePlatformSpecificImplementation< // AndroidFlutterLocalNotificationsPlugin // >(); // await androidPlugin?.createNotificationChannel(callChannel); // await androidPlugin?.createNotificationChannel(defaultChannel); // await androidPlugin?.createNotificationChannel(missedCallChannel); // } // Future createNotificationChannelIOS() async { // await globalNotificationsPlugin // .resolvePlatformSpecificImplementation< // IOSFlutterLocalNotificationsPlugin // >() // ?.requestPermissions(alert: true, badge: true, sound: true); // } // // ============================================================================ // // PERMISSION HANDLING // // ============================================================================ // Future requestPermissions() async { // await FirebaseMessaging.instance.requestPermission( // alert: true, // badge: true, // sound: true, // carPlay: false, // criticalAlert: false, // provisional: false, // ); // await Future.delayed(const Duration(milliseconds: 1000)); // if (Platform.isAndroid) { // final status = await Permission.notification.status; // if (status != PermissionStatus.granted) { // final requestStatus = await Permission.notification.request(); // if (requestStatus.isPermanentlyDenied) { // await showPermissionSettingsDialog(); // } // } // } // } // Future showPermissionSettingsDialog() async { // if (Get.context != null) { // final result = await Get.dialog( // AlertDialog( // shape: RoundedRectangleBorder( // borderRadius: BorderRadius.circular(16), // ), // title: Row( // children: [ // Icon(Icons.notifications_off, color: Colors.orange, size: 24), // const SizedBox(width: 8), // const Text( // 'Notification Permission', // style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), // ), // ], // ), // content: Column( // mainAxisSize: MainAxisSize.min, // crossAxisAlignment: CrossAxisAlignment.start, // children: [ // const Text( // 'To receive important notifications, please enable notifications in your device settings.', // style: TextStyle(fontSize: 16), // ), // const SizedBox(height: 16), // Container( // padding: const EdgeInsets.all(12), // decoration: BoxDecoration( // color: Colors.grey.shade100, // borderRadius: BorderRadius.circular(8), // ), // child: Column( // crossAxisAlignment: CrossAxisAlignment.start, // children: [ // const Text( // 'How to enable:', // style: TextStyle( // fontWeight: FontWeight.w600, // fontSize: 14, // ), // ), // const SizedBox(height: 4), // Text( // Platform.isIOS // ? '1. Go to Settings\n2. Find OnuFitness\n3. Tap Notifications\n4. Enable Allow Notifications' // : '1. Go to Settings\n2. Find Apps\n3. Select OnuFitness\n4. Enable Notifications', // style: const TextStyle(fontSize: 13), // ), // ], // ), // ), // ], // ), // actions: [ // TextButton( // onPressed: () => Get.back(result: false), // child: const Text( // 'Maybe Later', // style: TextStyle(color: Colors.grey), // ), // ), // ElevatedButton( // onPressed: () => Get.back(result: true), // style: ElevatedButton.styleFrom( // backgroundColor: const Color(primaryColor), // foregroundColor: Colors.white, // shape: RoundedRectangleBorder( // borderRadius: BorderRadius.circular(8), // ), // ), // child: const Text( // 'Open Settings', // style: TextStyle(color: Colors.black), // ), // ), // ], // ), // barrierDismissible: false, // ); // if (result == true) { // await openAppSettings(); // } // } else { // await openAppSettings(); // } // } // // ============================================================================ // // MESSAGE HANDLERS // // ============================================================================ // Future setupMessageHandlers() async { // FirebaseMessaging.onMessage.listen(handleForegroundMessage); // FirebaseMessaging.onMessageOpenedApp.listen( // willOpenTerminatedAppOnNotificationTap, // ); // RemoteMessage? initialMessage = // await FirebaseMessaging.instance.getInitialMessage(); // if (initialMessage != null) { // await willOpenTerminatedAppOnNotificationTap(initialMessage); // } // } // Future handleForegroundMessage(RemoteMessage message) async { // final notificationType = parseNotificationType( // message.data['notificationType'], // ); // if (notificationType == ApiEnum.CallingNotification) { // return; // } // if ((notificationType == ApiEnum.ChatNotification || // notificationType == ApiEnum.GroupChatNotification) && // (Get.currentRoute == RouteConstant.chatListScreen || // Get.currentRoute == "/ChatDetailScreen")) { // return; // } // if (notificationType == ApiEnum.CallingNotificationCancel) { // await handleCallCancellation(message); // return; // } // await showLocalNotification(message); // } // Future willOpenTerminatedAppOnNotificationTap( // RemoteMessage message, // ) async { // await handleNotificationNavigation(message.data); // } // Future showLocalNotification(RemoteMessage message) async { // // ✅ MUST USE: default_channel_v1 // const AndroidNotificationDetails androidDetails = // AndroidNotificationDetails( // 'default_channel_v1', // ✅ Use default channel // 'General Notifications', // channelDescription: 'Chat messages and general notifications', // importance: Importance.high, // priority: Priority.high, // showWhen: true, // color: Color(primaryColor), // icon: '@drawable/ic_stat_notification', // ); // const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( // presentAlert: true, // presentBadge: true, // presentSound: true, // ); // const NotificationDetails platformDetails = NotificationDetails( // android: androidDetails, // iOS: iosDetails, // ); // await globalNotificationsPlugin.show( // message.hashCode, // message.notification?.title ?? 'New Notification', // message.notification?.body ?? 'You have a new message', // platformDetails, // payload: jsonEncode(message.data), // ); // } // // ============================================================================ // // NOTIFICATION TAP HANDLER // // ============================================================================ // void onNotificationTapped(NotificationResponse response) async { // preferences = await SharedPreferences.getInstance(); // if (response.id != null) { // await globalNotificationsPlugin.cancel(response.id!); // } // if (response.actionId != null) { // await handleNotificationAction(response); // return; // } // if (response.payload != null) { // try { // final data = jsonDecode(response.payload!); // final notificationType = parseNotificationType( // data['notificationType'], // ); // // Dismiss call notifications when tapped // if (notificationType == ApiEnum.CallingNotification) { // final callDataRaw = data['dataId']; // if (callDataRaw != null) { // Map? callData; // try { // callData = // callDataRaw is String // ? jsonDecode(callDataRaw) // : (callDataRaw as Map); // } catch (_) {} // if (callData != null) { // final notificationId = callData.hashCode; // final channelName = extractChannelName(callData); // if (channelName != null) { // activeCallNotifications.remove(channelName); // } // await globalNotificationsPlugin.cancel(notificationId); // } // } // globalNotificationsPlugin.cancel(response.id!); // } // if (SharedServices.isLoggedIn() == true) { // await handleNotificationNavigation(data); // } // } catch (e, stackTrace) { // logger.error( // "Failed to handle notification tap", // error: e, // stackTrace: stackTrace, // ); // await globalNotificationsPlugin.cancelAll(); // } // } // } // Future handleNotificationAction(NotificationResponse response) async { // if (response.payload == null) return; // try { // final callData = jsonDecode(response.payload!); // switch (response.actionId) { // case 'accept_call': // await handleAcceptCall(callData); // break; // case 'reject_call': // await handleRejectCall(callData); // break; // } // } catch (e, stackTrace) { // logger.error( // "Failed to handle notification action: ${response.actionId}", // error: e, // stackTrace: stackTrace, // ); // } // } // // ============================================================================ // // CALL ACTION HANDLERS // // ============================================================================= // //................................................................................... // static Future handleAcceptCall(Map? callData) async { // final logger = LoggerService(); // try { // final notificationNavigationController = // Get.find(); // notificationNavigationController.isCallingNotificationNavigation.value = // true; // // Parse call data ONCE // Map? actualCallData; // if (callData != null && callData['dataId'] != null) { // try { // if (callData['dataId'] is String) { // actualCallData = jsonDecode(callData['dataId']); // } else if (callData['dataId'] is Map) { // actualCallData = Map.from(callData['dataId']); // } // } catch (e, stackTrace) { // logger.error( // "Failed to parse call data during accept", // error: e, // stackTrace: stackTrace, // ); // } // } // Map dataToUse = actualCallData ?? callData ?? {}; // if (dataToUse.isEmpty) { // return; // } // if (Get.currentRoute != RouteConstant.activeCallScreen) { // Get.offNamed(RouteConstant.activeCallScreen); // } // // Extract all data at once // String channelName = dataToUse['ChannelName'] ?? ''; // String remoteUid = dataToUse['UserID'] ?? ''; // String callerName = dataToUse['CallerName'] ?? ''; // String callerImage = dataToUse['ProfilePicture'] ?? ''; // String rtcType = dataToUse['CallType'] ?? 'VOICE'; // if (channelName.isEmpty || remoteUid.isEmpty) { // return; // } // // Dismiss notification immediately // final notificationId = activeCallNotifications[channelName]; // if (notificationId != null) { // globalNotificationsPlugin.cancel(notificationId); // activeCallNotifications.remove(channelName); // } // // Wait for app initialization if needed // if (wasLaunchedFromTerminatedState) { // await Future.delayed(Duration(milliseconds: 300)); // } // // Initialize Agora service // if (!Get.isRegistered()) { // Get.put(AgoraCallService()); // } // final agoraService = Get.find(); // await agoraService.init(); // final callType = rtcType == 'VIDEO' ? CallType.video : CallType.voice; // agoraService.setCallDataBatch( // channel: channelName, // userId: remoteUid, // name: callerName, // profilePic: callerImage, // callType: callType, // isCaller: false, // state: CallState.ringing, // ); // // Get token and start video preview in parallel // final tokenFuture = getOrUseCachedToken(channelName, agoraService); // final videoFuture = // callType == CallType.video // ? agoraService.startVideoPreview() // : Future.value(); // await Future.wait([tokenFuture, videoFuture]); // await agoraService.acceptCall(callType: callType); // } catch (e, stackTrace) { // logger.error( // "Failed to handle accept call action", // error: e, // stackTrace: stackTrace, // ); // } // } //................................................................................... // static Future handleRejectCall(Map? callData) async { // final logger = LoggerService(); // try { // // Dismiss notification FIRST // if (callData != null) { // final channelName = extractChannelName(callData); // if (channelName != null) { // final notificationId = activeCallNotifications[channelName]; // if (notificationId != null) { // await globalNotificationsPlugin.cancel(notificationId); // activeCallNotifications.remove(channelName); // } // } // } // // Parse & SET FULL STATE // Map? actualCallData; // if (callData != null && callData['dataId'] != null) { // try { // if (callData['dataId'] is String) { // actualCallData = jsonDecode(callData['dataId']); // } else if (callData['dataId'] is Map) { // actualCallData = Map.from(callData['dataId']); // } // } catch (e, stackTrace) { // logger.error( // "Failed to parse call data during reject", // error: e, // stackTrace: stackTrace, // ); // } // } // Map dataToUse = actualCallData ?? callData ?? {}; // if (dataToUse.isEmpty) { // await globalNotificationsPlugin.cancelAll(); // return; // } // if (!Get.isRegistered()) { // Get.put(AgoraCallService()); // } // final agoraService = Get.find(); // await agoraService.init(); // if (dataToUse.isNotEmpty) { // String channelName = dataToUse['ChannelName'] ?? ''; // String remoteUid = dataToUse['UserID'] ?? ''; // String callerName = dataToUse['CallerName'] ?? ''; // String rtcType = dataToUse['CallType'] ?? 'VOICE'; // // ✅ Set ALL required state // agoraService.channelName.value = channelName; // agoraService.remoteUserId.value = remoteUid; // agoraService.callerName.value = callerName; // agoraService.currentCallType.value = // rtcType == 'VIDEO' ? CallType.video : CallType.voice; // agoraService.amICaller.value = false; // agoraService.callState.value = CallState.ringing; // } // await agoraService.rejectCall(); // await globalNotificationsPlugin.cancelAll(); // } catch (e, stackTrace) { // logger.error( // "Failed to handle reject call action", // error: e, // stackTrace: stackTrace, // ); // } // } // // ============================================================================ // // NOTIFICATION NAVIGATION // // ============================================================================ // static Future handleNotificationNavigation( // Map data, // ) async { // await Future.delayed(const Duration(milliseconds: 500)); // try { // // Dismiss call notification if applicable // final notificationType = parseNotificationType(data['notificationType']); // if (notificationType == ApiEnum.CallingNotification) { // final callDataRaw = data['dataId']; // if (callDataRaw != null) { // Map? callData; // try { // callData = // callDataRaw is String // ? jsonDecode(callDataRaw) // : Map.from(callDataRaw); // } catch (_) {} // if (callData != null) { // final channelName = extractChannelName(callData); // if (channelName != null) { // final notificationId = activeCallNotifications[channelName]; // if (notificationId != null) { // await globalNotificationsPlugin.cancel(notificationId); // activeCallNotifications.remove(channelName); // } // } // } // } // } // } catch (e, stackTrace) { // final logger = LoggerService(); // logger.error( // "Failed to dismiss notification before navigation", // error: e, // stackTrace: stackTrace, // ); // await globalNotificationsPlugin.cancelAll(); // } // final navigationController = Get.find(); // await navigationController.navigateBasedOnNotification(data); // } // // ============================================================================ // // FCM TOKEN MANAGEMENT // // ============================================================================ // Future getDeviceToken() async { // try { // _deviceToken = await FirebaseMessaging.instance.getToken(); // FirebaseMessaging.instance.onTokenRefresh.listen((newToken) { // _deviceToken = newToken; // }); // } catch (e, stackTrace) { // logger.error( // "Failed to get FCM device token", // error: e, // stackTrace: stackTrace, // ); // } // } // Future deleteDeviceToken() async { // try { // await FirebaseMessaging.instance.deleteToken(); // _deviceToken = null; // } catch (e, stackTrace) { // logger.error( // "Failed to delete FCM token", // error: e, // stackTrace: stackTrace, // ); // } // } // RxBool sendFcmToBackendLoading = false.obs; // Future sendTokenToBackend() async { // sendFcmToBackendLoading(true); // bool ret = false; // try { // if (_deviceToken != null) { // final response = await ApiBase.patchRequest( // extendedURL: ApiUrl.sendFcmToken, // body: {"deviceFCMToken": _deviceToken}, // ); // if (response.statusCode == 200 || response.statusCode == 201) { // ret = true; // } // } // } catch (e, stackTrace) { // logger.error( // "Failed to send FCM token to backend", // error: e, // stackTrace: stackTrace, // ); // } finally { // sendFcmToBackendLoading(false); // } // return ret; // } // } // // =======OUTSIDE THE CLASS==================================================== // // ============================================================================ // // ============================================================================ // // Cache for pre-fetched tokens // final Map cachedRTCTokens = {}; // void preFetchRTCToken(String channelName, Map callData) async { // try { // if (!Get.isRegistered()) { // Get.put(AgoraTokenController()); // } // final agoraTokenController = Get.find(); // bool tokenSuccess = await agoraTokenController.getRTCtoken( // channelName: channelName, // role: ApiEnum.SUBSCRIBER, // ); // if (tokenSuccess) { // cachedRTCTokens[channelName] = agoraTokenController.rTCtoken.value; // } // } catch (e, stackTrace) { // final logger = LoggerService(); // logger.error( // "Failed to pre-fetch RTC token for channel: $channelName", // error: e, // stackTrace: stackTrace, // ); // } // } // //............................................................................................. // Future getOrUseCachedToken( // String channelName, // AgoraCallService agoraService, // ) async { // // Check if we have a cached token // if (cachedRTCTokens.containsKey(channelName)) { // agoraService.rtcToken.value = cachedRTCTokens[channelName]!; // cachedRTCTokens.remove(channelName); // return; // } // // No cached token, fetch it now // if (!Get.isRegistered()) { // Get.put(AgoraTokenController()); // } // final agoraTokenController = Get.find(); // bool tokenSuccess = await agoraTokenController.getRTCtoken( // channelName: channelName, // role: ApiEnum.SUBSCRIBER, // ); // if (tokenSuccess) { // agoraService.rtcToken.value = agoraTokenController.rTCtoken.value; // } else { // throw Exception('Failed to get RTC token'); // } // }