import 'dart:typed_data'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'api_client.dart'; /// Notification ID for queue alerts (fixed so we can cancel it). const int _queueAlertNotificationId = 9001; /// Background handler — must be top-level function. @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(); final data = message.data; final type = data['type']; if (type == 'queue_alert') { await _showQueueAlertNotification(data); } else if (type == 'queue_alert_cancel') { final plugin = FlutterLocalNotificationsPlugin(); await plugin.cancel(_queueAlertNotificationId); } // VoIP pushes handled natively by twilio_voice plugin. } /// Show an insistent queue alert notification (works from background handler too). Future _showQueueAlertNotification(Map data) async { final plugin = FlutterLocalNotificationsPlugin(); final title = data['title'] ?? 'Call Waiting'; final body = data['body'] ?? 'New call in queue'; final androidDetails = AndroidNotificationDetails( 'twp_queue_alerts', 'Queue Alerts', channelDescription: 'Alerts when calls are waiting in queue', importance: Importance.max, priority: Priority.max, playSound: true, sound: const RawResourceAndroidNotificationSound('queue_alert'), enableVibration: true, vibrationPattern: Int64List.fromList([0, 500, 200, 500, 200, 500]), ongoing: true, autoCancel: false, category: AndroidNotificationCategory.alarm, additionalFlags: Int32List.fromList([4]), // FLAG_INSISTENT = 4 fullScreenIntent: true, visibility: NotificationVisibility.public, ); await plugin.show( _queueAlertNotificationId, title, body, NotificationDetails(android: androidDetails), ); } class PushNotificationService { final ApiClient _api; final FirebaseMessaging _messaging = FirebaseMessaging.instance; final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin(); String? _fcmToken; String? get fcmToken => _fcmToken; PushNotificationService(this._api); Future initialize() async { FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); await _messaging.requestPermission( alert: true, badge: true, sound: true, criticalAlert: true, ); // Initialize local notifications const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); const initSettings = InitializationSettings(android: androidSettings); await _localNotifications.initialize(initSettings); // Get and register FCM token final token = await _messaging.getToken(); debugPrint('FCM token: ${token != null ? "${token.substring(0, 20)}..." : "NULL"}'); if (token != null) { _fcmToken = token; await _registerToken(token); } else { debugPrint('FCM: Failed to get token - Firebase may not be configured correctly'); } // Listen for token refresh _messaging.onTokenRefresh.listen(_registerToken); // Handle foreground messages (non-VoIP) FirebaseMessaging.onMessage.listen(_handleForegroundMessage); } Future _registerToken(String token) async { try { await _api.dio.post('/fcm/register', data: {'fcm_token': token}); } catch (_) {} } void _handleForegroundMessage(RemoteMessage message) { final data = message.data; final type = data['type']; // VoIP incoming_call is handled by twilio_voice natively if (type == 'incoming_call') return; // Queue alert — show insistent notification if (type == 'queue_alert') { _showQueueAlertNotification(data); return; } // Queue alert cancel — dismiss notification if (type == 'queue_alert_cancel') { _localNotifications.cancel(_queueAlertNotificationId); return; } // Show local notification for other types (missed call, etc.) _localNotifications.show( message.hashCode, data['title'] ?? 'TWP Softphone', data['body'] ?? '', const NotificationDetails( android: AndroidNotificationDetails( 'twp_general', 'General Notifications', importance: Importance.high, priority: Priority.high, ), ), ); } /// Cancel any active queue alert (called when agent accepts a call in-app). void cancelQueueAlert() { _localNotifications.cancel(_queueAlertNotificationId); } }