Files
twilio-wp-plugin/mobile/lib/services/push_notification_service.dart
Claude 4af4be94a4
All checks were successful
Create Release / build (push) Successful in 6s
Add FCM push notifications, queue alerts, caller ID fixes, and auto-revert agent status
Server-side:
- Add push credential auto-creation for FCM incoming call notifications
- Add queue alert FCM notifications (data-only for background delivery)
- Add queue alert cancellation on call accept/disconnect
- Fix caller ID to show caller's number instead of Twilio number
- Fix FCM token storage when refresh_token is null
- Add pre_call_status tracking to revert agent status 30s after call ends
- Add SSE fallback polling for mobile app connectivity

Mobile app:
- Add Android telecom permissions and phone account registration
- Add VoiceFirebaseMessagingService for incoming call push handling
- Add insistent queue alert notifications with custom sound
- Fix caller number display on active call screen
- Add caller ID selection dropdown on dashboard
- Add phone numbers endpoint and provider support
- Add unit tests for CallInfo, QueueState, and CallProvider
- Remove local.properties from tracking, add .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:11:02 -08:00

150 lines
4.6 KiB
Dart

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<void> _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<void> _showQueueAlertNotification(Map<String, dynamic> 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<void> 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<void> _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);
}
}