Replace native Twilio Voice SDK with WebView-based softphone

Rewrites the mobile app from a native Twilio Voice SDK integration
(Android Telecom/ConnectionService) to a thin WebView shell that loads
a standalone browser phone page from WordPress. This eliminates the
buggy Android phone account registration, fixes frequent logouts by
using 7-day WP session cookies instead of JWT tokens, and maintains
all existing call features (dialpad, queues, hold, transfer, requeue,
recording, caller ID, agent status).

Server-side:
- Add class-twp-mobile-phone-page.php: standalone /twp-phone/ endpoint
  with mobile-optimized UI, dark mode, tab navigation, and Flutter
  WebView JS bridge
- Extend auth cookie to 7 days for phone agents
- Add WP AJAX handler for FCM token registration (cookie auth)

Flutter app (v2.0.0):
- Replace 18 native files with 5-file WebView shell
- Login via wp-login.php in WebView (auto-detect redirect on success)
- Full-screen WebView with auto microphone grant for WebRTC
- FCM push notifications preserved for queue alerts
- Remove: twilio_voice, dio, provider, JWT auth, SSE, native call UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-10 09:11:25 -07:00
parent 4af4be94a4
commit 621b0890a9
37 changed files with 2744 additions and 2663 deletions

View File

@@ -3,12 +3,11 @@ 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.
/// Background handler -- must be top-level function.
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
@@ -21,7 +20,6 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
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).
@@ -57,8 +55,12 @@ Future<void> _showQueueAlertNotification(Map<String, dynamic> data) async {
);
}
/// 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 ApiClient _api;
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
@@ -66,8 +68,6 @@ class PushNotificationService {
String? get fcmToken => _fcmToken;
PushNotificationService(this._api);
Future<void> initialize() async {
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
@@ -84,43 +84,37 @@ class PushNotificationService {
const initSettings = InitializationSettings(android: androidSettings);
await _localNotifications.initialize(initSettings);
// Get and register FCM token
// Get FCM token
final token = await _messaging.getToken();
debugPrint('FCM token: ${token != null ? "${token.substring(0, 20)}..." : "NULL"}');
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');
debugPrint(
'FCM: Failed to get token - Firebase may not be configured correctly');
}
// Listen for token refresh
_messaging.onTokenRefresh.listen(_registerToken);
_messaging.onTokenRefresh.listen((token) {
_fcmToken = token;
});
// Handle foreground messages (non-VoIP)
// Handle foreground messages
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
// Queue alert -- show insistent notification
if (type == 'queue_alert') {
_showQueueAlertNotification(data);
return;
}
// Queue alert cancel dismiss notification
// Queue alert cancel -- dismiss notification
if (type == 'queue_alert_cancel') {
_localNotifications.cancel(_queueAlertNotificationId);
return;
@@ -142,7 +136,7 @@ class PushNotificationService {
);
}
/// Cancel any active queue alert (called when agent accepts a call in-app).
/// Cancel any active queue alert.
void cancelQueueAlert() {
_localNotifications.cancel(_queueAlertNotificationId);
}