2450 lines
82 KiB
Dart
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');
|
|
// }
|
|
// }
|