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>
196 lines
5.8 KiB
Dart
196 lines
5.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:webview_flutter/webview_flutter.dart';
|
|
|
|
/// Login screen that loads wp-login.php in a WebView.
|
|
///
|
|
/// When the user successfully logs in, WordPress redirects to /twp-phone/.
|
|
/// We detect that URL change and report login success to the parent.
|
|
class LoginScreen extends StatefulWidget {
|
|
final void Function(String serverUrl) onLoginSuccess;
|
|
|
|
const LoginScreen({super.key, required this.onLoginSuccess});
|
|
|
|
@override
|
|
State<LoginScreen> createState() => _LoginScreenState();
|
|
}
|
|
|
|
class _LoginScreenState extends State<LoginScreen> {
|
|
static const _storage = FlutterSecureStorage();
|
|
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _serverController = TextEditingController();
|
|
bool _showWebView = false;
|
|
bool _webViewLoading = true;
|
|
String? _error;
|
|
late WebViewController _webViewController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadSavedServer();
|
|
}
|
|
|
|
Future<void> _loadSavedServer() async {
|
|
final saved = await _storage.read(key: 'server_url');
|
|
if (saved != null && mounted) {
|
|
_serverController.text = saved;
|
|
}
|
|
}
|
|
|
|
void _startLogin() {
|
|
if (!_formKey.currentState!.validate()) return;
|
|
|
|
var serverUrl = _serverController.text.trim();
|
|
if (!serverUrl.startsWith('http')) {
|
|
serverUrl = 'https://$serverUrl';
|
|
}
|
|
// Remove trailing slash
|
|
serverUrl = serverUrl.replaceAll(RegExp(r'/+$'), '');
|
|
|
|
setState(() {
|
|
_showWebView = true;
|
|
_webViewLoading = true;
|
|
_error = null;
|
|
});
|
|
|
|
final loginUrl =
|
|
'$serverUrl/wp-login.php?redirect_to=${Uri.encodeComponent('$serverUrl/twp-phone/')}';
|
|
|
|
_webViewController = WebViewController()
|
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
|
..setNavigationDelegate(
|
|
NavigationDelegate(
|
|
onPageStarted: (url) {
|
|
// Check if we've been redirected to the phone page (login success)
|
|
if (url.contains('/twp-phone/') || url.endsWith('/twp-phone')) {
|
|
_onLoginComplete(serverUrl);
|
|
}
|
|
},
|
|
onPageFinished: (url) {
|
|
if (mounted) {
|
|
setState(() => _webViewLoading = false);
|
|
}
|
|
// Also check on page finish in case redirect was instant
|
|
if (url.contains('/twp-phone/') || url.endsWith('/twp-phone')) {
|
|
_onLoginComplete(serverUrl);
|
|
}
|
|
},
|
|
onWebResourceError: (error) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_showWebView = false;
|
|
_error =
|
|
'Could not connect to server: ${error.description}';
|
|
});
|
|
}
|
|
},
|
|
),
|
|
)
|
|
..setUserAgent('TWPMobile/2.0 (Android; WebView)')
|
|
..loadRequest(Uri.parse(loginUrl));
|
|
}
|
|
|
|
Future<void> _onLoginComplete(String serverUrl) async {
|
|
// Save server URL for next launch
|
|
await _storage.write(key: 'server_url', value: serverUrl);
|
|
if (mounted) {
|
|
widget.onLoginSuccess(serverUrl);
|
|
}
|
|
}
|
|
|
|
void _cancelLogin() {
|
|
setState(() {
|
|
_showWebView = false;
|
|
_error = null;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_showWebView) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: _cancelLogin,
|
|
),
|
|
title: const Text('Sign In'),
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
WebViewWidget(controller: _webViewController),
|
|
if (_webViewLoading)
|
|
const Center(child: CircularProgressIndicator()),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
body: SafeArea(
|
|
child: Center(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.phone_in_talk,
|
|
size: 64,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'TWP Softphone',
|
|
style: Theme.of(context).textTheme.headlineMedium,
|
|
),
|
|
const SizedBox(height: 32),
|
|
TextFormField(
|
|
controller: _serverController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Server URL',
|
|
hintText: 'https://your-site.com',
|
|
prefixIcon: Icon(Icons.dns),
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.url,
|
|
autofillHints: const [AutofillHints.url],
|
|
validator: (v) =>
|
|
v == null || v.trim().isEmpty ? 'Required' : null,
|
|
),
|
|
if (_error != null) ...[
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
_error!,
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.error),
|
|
),
|
|
],
|
|
const SizedBox(height: 24),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 48,
|
|
child: FilledButton(
|
|
onPressed: _startLogin,
|
|
child: const Text('Connect'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_serverController.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|