2026-01-13 11:36:24 +05:30

2450 lines
82 KiB
Dart

//.................................................................................................................................
// 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<String, int> 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<String, dynamic>? pendingCallAction;
// ============================================================================
// BACKGROUND MESSAGE HANDLER
// ============================================================================
@pragma('vm:entry-point')
Future<void> 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<String, dynamic> parseCallPayload(Map<String, dynamic> data) {
Map<String, dynamic> callPayload = {...data};
if (data['dataId'] != null) {
try {
if (data['dataId'] is String) {
Map<String, dynamic> parsedDataId = jsonDecode(data['dataId']);
callPayload.addAll(parsedDataId);
} else if (data['dataId'] is Map) {
callPayload.addAll(Map<String, dynamic>.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<String, dynamic> 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<void> 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>[
DarwinNotificationAction.plain(
'accept_call',
'Accept',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.foreground,
},
),
DarwinNotificationAction.plain(
'reject_call',
'Reject',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.destructive,
},
),
],
options: <DarwinNotificationCategoryOption>{
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<void> 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>[
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<void> 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<void> showMissedCallNotification(
Map<String, dynamic> 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<void> 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<void> 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<void> processNotificationActionButtonTerminatedApp() async {
if (pendingCallAction == null) {
return;
}
int retries = 0;
while (!Get.isRegistered<AgoraCallService>() && retries < 30) {
await Future.delayed(Duration(milliseconds: 100));
retries++;
}
if (!Get.isRegistered<AgoraCallService>()) {
pendingCallAction = null;
return;
}
final agoraService = Get.find<AgoraCallService>();
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<void> 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>[
DarwinNotificationAction.plain(
'accept_call',
'📞 Accept',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.foreground,
},
),
DarwinNotificationAction.plain(
'reject_call',
'✖️ Reject',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.destructive,
},
),
],
options: <DarwinNotificationCategoryOption>{
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<void> 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<void> createNotificationChannelIOS() async {
await globalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>()
?.requestPermissions(alert: true, badge: true, sound: true);
}
// ============================================================================
// PERMISSION HANDLING
// ============================================================================
Future<void> 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<void> showPermissionSettingsDialog() async {
if (Get.context != null) {
final result = await Get.dialog<bool>(
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<void> setupMessageHandlers() async {
FirebaseMessaging.onMessage.listen(handleForegroundMessage);
FirebaseMessaging.onMessageOpenedApp.listen(
willOpenTerminatedAppOnNotificationTap,
);
RemoteMessage? initialMessage =
await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
await willOpenTerminatedAppOnNotificationTap(initialMessage);
}
}
Future<void> 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<void> willOpenTerminatedAppOnNotificationTap(
RemoteMessage message,
) async {
await handleNotificationNavigation(message.data);
}
Future<void> 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<String, dynamic>? callData;
try {
callData =
callDataRaw is String
? jsonDecode(callDataRaw)
: (callDataRaw as Map<String, dynamic>);
} 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<void> 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<void> handleAcceptCall(Map<String, dynamic>? callData) async {
final logger = LoggerService();
try {
final notificationNavigationController =
Get.find<NotificationNavigationController>();
notificationNavigationController.isCallingNotificationNavigation.value =
true;
// Parse call data ONCE
Map<String, dynamic>? 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<String, dynamic>.from(callData['dataId']);
}
} catch (e, stackTrace) {
logger.error(
"Failed to parse call data during accept",
error: e,
stackTrace: stackTrace,
);
}
}
Map<String, dynamic> 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<AgoraCallService>()) {
Get.put(AgoraCallService());
}
final agoraService = Get.find<AgoraCallService>();
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<void> handleRejectCall(Map<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>.from(callData['dataId']);
}
} catch (e, stackTrace) {
logger.error(
"Failed to parse call data during reject",
error: e,
stackTrace: stackTrace,
);
}
}
Map<String, dynamic> dataToUse = actualCallData ?? callData ?? {};
if (dataToUse.isEmpty) {
await globalNotificationsPlugin.cancelAll();
return;
}
if (!Get.isRegistered<AgoraCallService>()) {
Get.put(AgoraCallService());
}
final agoraService = Get.find<AgoraCallService>();
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<void> handleNotificationNavigation(
Map<String, dynamic> 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<String, dynamic>? callData;
try {
callData =
callDataRaw is String
? jsonDecode(callDataRaw)
: Map<String, dynamic>.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<NotificationNavigationController>();
await navigationController.navigateBasedOnNotification(data);
}
// ============================================================================
// FCM TOKEN MANAGEMENT
// ============================================================================
Future<void> 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<void> 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<bool> 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<String, String> cachedRTCTokens = {};
void preFetchRTCToken(String channelName, Map<String, dynamic> callData) async {
try {
if (!Get.isRegistered<AgoraTokenController>()) {
Get.put(AgoraTokenController());
}
final agoraTokenController = Get.find<AgoraTokenController>();
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<void> 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<AgoraTokenController>()) {
Get.put(AgoraTokenController());
}
final agoraTokenController = Get.find<AgoraTokenController>();
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<String, int> 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<String, dynamic>? pendingCallAction;
// // ============================================================================
// // BACKGROUND MESSAGE HANDLER
// // ============================================================================
// @pragma('vm:entry-point')
// Future<void> 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<String, dynamic> parseCallPayload(Map<String, dynamic> data) {
// Map<String, dynamic> callPayload = {...data};
// if (data['dataId'] != null) {
// try {
// if (data['dataId'] is String) {
// Map<String, dynamic> parsedDataId = jsonDecode(data['dataId']);
// callPayload.addAll(parsedDataId);
// } else if (data['dataId'] is Map) {
// callPayload.addAll(Map<String, dynamic>.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<String, dynamic> 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<void> 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>[
// DarwinNotificationAction.plain(
// 'accept_call',
// 'Accept',
// options: <DarwinNotificationActionOption>{
// DarwinNotificationActionOption.foreground,
// },
// ),
// DarwinNotificationAction.plain(
// 'reject_call',
// 'Reject',
// options: <DarwinNotificationActionOption>{
// DarwinNotificationActionOption.destructive,
// },
// ),
// ],
// options: <DarwinNotificationCategoryOption>{
// 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<void> 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>[
// 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<void> 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<void> showMissedCallNotification(
// Map<String, dynamic> 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<void> 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<void> 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<void> processNotificationActionButtonTerminatedApp() async {
// if (pendingCallAction == null) {
// return;
// }
// int retries = 0;
// while (!Get.isRegistered<AgoraCallService>() && retries < 30) {
// await Future.delayed(Duration(milliseconds: 100));
// retries++;
// }
// if (!Get.isRegistered<AgoraCallService>()) {
// pendingCallAction = null;
// return;
// }
// final agoraService = Get.find<AgoraCallService>();
// 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<void> 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>[
// DarwinNotificationAction.plain(
// 'accept_call',
// '📞 Accept',
// options: <DarwinNotificationActionOption>{
// DarwinNotificationActionOption.foreground,
// },
// ),
// DarwinNotificationAction.plain(
// 'reject_call',
// '✖️ Reject',
// options: <DarwinNotificationActionOption>{
// DarwinNotificationActionOption.destructive,
// },
// ),
// ],
// options: <DarwinNotificationCategoryOption>{
// 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<void> 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<void> createNotificationChannelIOS() async {
// await globalNotificationsPlugin
// .resolvePlatformSpecificImplementation<
// IOSFlutterLocalNotificationsPlugin
// >()
// ?.requestPermissions(alert: true, badge: true, sound: true);
// }
// // ============================================================================
// // PERMISSION HANDLING
// // ============================================================================
// Future<void> 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<void> showPermissionSettingsDialog() async {
// if (Get.context != null) {
// final result = await Get.dialog<bool>(
// 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<void> setupMessageHandlers() async {
// FirebaseMessaging.onMessage.listen(handleForegroundMessage);
// FirebaseMessaging.onMessageOpenedApp.listen(
// willOpenTerminatedAppOnNotificationTap,
// );
// RemoteMessage? initialMessage =
// await FirebaseMessaging.instance.getInitialMessage();
// if (initialMessage != null) {
// await willOpenTerminatedAppOnNotificationTap(initialMessage);
// }
// }
// Future<void> 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<void> willOpenTerminatedAppOnNotificationTap(
// RemoteMessage message,
// ) async {
// await handleNotificationNavigation(message.data);
// }
// Future<void> 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<String, dynamic>? callData;
// try {
// callData =
// callDataRaw is String
// ? jsonDecode(callDataRaw)
// : (callDataRaw as Map<String, dynamic>);
// } 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<void> 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<void> handleAcceptCall(Map<String, dynamic>? callData) async {
// final logger = LoggerService();
// try {
// final notificationNavigationController =
// Get.find<NotificationNavigationController>();
// notificationNavigationController.isCallingNotificationNavigation.value =
// true;
// // Parse call data ONCE
// Map<String, dynamic>? 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<String, dynamic>.from(callData['dataId']);
// }
// } catch (e, stackTrace) {
// logger.error(
// "Failed to parse call data during accept",
// error: e,
// stackTrace: stackTrace,
// );
// }
// }
// Map<String, dynamic> 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<AgoraCallService>()) {
// Get.put(AgoraCallService());
// }
// final agoraService = Get.find<AgoraCallService>();
// 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<void> handleRejectCall(Map<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>.from(callData['dataId']);
// }
// } catch (e, stackTrace) {
// logger.error(
// "Failed to parse call data during reject",
// error: e,
// stackTrace: stackTrace,
// );
// }
// }
// Map<String, dynamic> dataToUse = actualCallData ?? callData ?? {};
// if (dataToUse.isEmpty) {
// await globalNotificationsPlugin.cancelAll();
// return;
// }
// if (!Get.isRegistered<AgoraCallService>()) {
// Get.put(AgoraCallService());
// }
// final agoraService = Get.find<AgoraCallService>();
// 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<void> handleNotificationNavigation(
// Map<String, dynamic> 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<String, dynamic>? callData;
// try {
// callData =
// callDataRaw is String
// ? jsonDecode(callDataRaw)
// : Map<String, dynamic>.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<NotificationNavigationController>();
// await navigationController.navigateBasedOnNotification(data);
// }
// // ============================================================================
// // FCM TOKEN MANAGEMENT
// // ============================================================================
// Future<void> 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<void> 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<bool> 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<String, String> cachedRTCTokens = {};
// void preFetchRTCToken(String channelName, Map<String, dynamic> callData) async {
// try {
// if (!Get.isRegistered<AgoraTokenController>()) {
// Get.put(AgoraTokenController());
// }
// final agoraTokenController = Get.find<AgoraTokenController>();
// 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<void> 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<AgoraTokenController>()) {
// Get.put(AgoraTokenController());
// }
// final agoraTokenController = Get.find<AgoraTokenController>();
// 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');
// }
// }