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

@@ -1,11 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'services/api_client.dart';
import 'providers/auth_provider.dart';
import 'providers/agent_provider.dart';
import 'providers/call_provider.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'screens/login_screen.dart';
import 'screens/dashboard_screen.dart';
import 'screens/phone_screen.dart';
class TwpSoftphoneApp extends StatefulWidget {
const TwpSoftphoneApp({super.key});
@@ -15,51 +11,77 @@ class TwpSoftphoneApp extends StatefulWidget {
}
class _TwpSoftphoneAppState extends State<TwpSoftphoneApp> {
final _apiClient = ApiClient();
static const _storage = FlutterSecureStorage();
String? _serverUrl;
bool _loading = true;
@override
void initState() {
super.initState();
_checkSavedSession();
}
Future<void> _checkSavedSession() async {
final url = await _storage.read(key: 'server_url');
if (mounted) {
setState(() {
_serverUrl = url;
_loading = false;
});
}
}
void _onLoginSuccess(String serverUrl) {
setState(() {
_serverUrl = serverUrl;
});
}
void _onLogout() async {
await _storage.delete(key: 'server_url');
if (mounted) {
setState(() {
_serverUrl = null;
});
}
}
void _onSessionExpired() {
// Server URL is still saved, but session cookie is gone.
// Show login screen but keep the server URL pre-filled.
if (mounted) {
setState(() {
_serverUrl = null;
});
}
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) {
final auth = AuthProvider(_apiClient);
auth.tryRestoreSession();
return auth;
},
child: MaterialApp(
title: 'TWP Softphone',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
brightness: Brightness.light,
),
darkTheme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
brightness: Brightness.dark,
),
home: Consumer<AuthProvider>(
builder: (context, auth, _) {
if (auth.state == AuthState.authenticated) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => AgentProvider(
auth.apiClient,
auth.sseService,
)..refresh(),
),
ChangeNotifierProvider(
create: (_) => CallProvider(auth.voiceService),
),
],
child: const DashboardScreen(),
);
}
return const LoginScreen();
},
),
return MaterialApp(
title: 'TWP Softphone',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
brightness: Brightness.light,
),
darkTheme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
brightness: Brightness.dark,
),
home: _loading
? const Scaffold(
body: Center(child: CircularProgressIndicator()),
)
: _serverUrl != null
? PhoneScreen(
serverUrl: _serverUrl!,
onLogout: _onLogout,
onSessionExpired: _onSessionExpired,
)
: LoginScreen(onLoginSuccess: _onLoginSuccess),
);
}
}