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'; /// 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); } } /// 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), ); } /// Push notification service for queue alerts and general notifications. /// /// FCM token registration is handled via the WebView JavaScript bridge /// instead of a REST API call. The token is exposed via [fcmToken] and /// injected into the web page by [PhoneScreen]. class PushNotificationService { final FirebaseMessaging _messaging = FirebaseMessaging.instance; final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin(); String? _fcmToken; String? get fcmToken => _fcmToken; 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 FCM token final token = await _messaging.getToken(); debugPrint( 'FCM token: ${token != null ? "${token.substring(0, 20)}..." : "NULL"}'); if (token != null) { _fcmToken = token; } else { debugPrint( 'FCM: Failed to get token - Firebase may not be configured correctly'); } // Listen for token refresh _messaging.onTokenRefresh.listen((token) { _fcmToken = token; }); // Handle foreground messages FirebaseMessaging.onMessage.listen(_handleForegroundMessage); } void _handleForegroundMessage(RemoteMessage message) { final data = message.data; final type = data['type']; // 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. void cancelQueueAlert() { _localNotifications.cancel(_queueAlertNotificationId); } }