Files
twilio-wp-plugin/mobile/lib/screens/login_screen.dart

196 lines
5.8 KiB
Dart
Raw Normal View History

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();
}
}