import 'package:flutter/material.dart'; import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:logging/logging.dart'; import 'package:sheetless/core/services/api_client.dart'; import 'package:sheetless/core/services/storage_service.dart'; import '../home/home_page.dart'; /// Login page for authenticating with the Sheetless server. class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State createState() => _LoginPageState(); } class _LoginPageState extends State { final _log = Logger('LoginPage'); final _formKey = GlobalKey(); final _storageService = StorageService(); final _urlController = TextEditingController( text: 'https://sheetless.julian-mutter.de', ); final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); String? _errorMessage; bool _isLoggingIn = false; @override void initState() { super.initState(); _tryAutoLogin(); } @override void dispose() { _urlController.dispose(); _usernameController.dispose(); _passwordController.dispose(); super.dispose(); } // --------------------------------------------------------------------------- // Auto-Login // --------------------------------------------------------------------------- /// Attempts to auto-login using a stored JWT token. Future _tryAutoLogin() async { final jwt = await _storageService.readSecure(SecureStorageKey.jwt); if (jwt != null && _isTokenValid(jwt)) { await _navigateToHome(); return; } // Restore saved credentials for convenience await _restoreCredentials(); } /// Checks if a JWT token is still valid (not expired). bool _isTokenValid(String jwt) { try { return !JwtDecoder.isExpired(jwt); } on FormatException { return false; } } /// Restores previously saved URL and username for convenience. Future _restoreCredentials() async { final url = await _storageService.readSecure(SecureStorageKey.url); final username = await _storageService.readSecure(SecureStorageKey.email); if (url != null) _urlController.text = url; if (username != null) _usernameController.text = username; } // --------------------------------------------------------------------------- // Login // --------------------------------------------------------------------------- /// Handles the login button press. Future _handleLogin() async { if (_isLoggingIn) return; if (!_formKey.currentState!.validate()) return; setState(() { _isLoggingIn = true; _errorMessage = null; }); try { final apiClient = ApiClient(baseUrl: _urlController.text); await apiClient.login(_usernameController.text, _passwordController.text); // Save credentials for next time await _saveCredentials(apiClient.token!); await _navigateToHome(); } catch (e) { _log.warning('Login failed', e); setState(() { _errorMessage = 'Login failed.\n$e'; }); } finally { if (mounted) { setState(() => _isLoggingIn = false); } } } /// Saves credentials after successful login. Future _saveCredentials(String token) async { await _storageService.writeSecure( SecureStorageKey.url, _urlController.text, ); await _storageService.writeSecure(SecureStorageKey.jwt, token); await _storageService.writeSecure( SecureStorageKey.email, _usernameController.text, ); } /// Navigates to the home page after successful authentication. Future _navigateToHome() async { final config = await _storageService.readConfig(); if (!mounted) return; Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => HomePage(config: config)), ); } // --------------------------------------------------------------------------- // Validation // --------------------------------------------------------------------------- String? _validateNotEmpty(String? value) { if (value == null || value.isEmpty) { return 'This field is required'; } return null; } // --------------------------------------------------------------------------- // UI // --------------------------------------------------------------------------- @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Login')), body: SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildUrlField(), const SizedBox(height: 16), _buildUsernameField(), const SizedBox(height: 16), _buildPasswordField(), const SizedBox(height: 24), _buildLoginButton(), if (_errorMessage != null) _buildErrorMessage(), ], ), ), ), ), ); } Widget _buildUrlField() { return TextFormField( controller: _urlController, validator: _validateNotEmpty, decoration: const InputDecoration( labelText: 'Server URL', prefixIcon: Icon(Icons.dns), ), keyboardType: TextInputType.url, textInputAction: TextInputAction.next, ); } Widget _buildUsernameField() { return TextFormField( controller: _usernameController, validator: _validateNotEmpty, decoration: const InputDecoration( labelText: 'Username', prefixIcon: Icon(Icons.person), ), textInputAction: TextInputAction.next, ); } Widget _buildPasswordField() { return TextFormField( controller: _passwordController, validator: _validateNotEmpty, decoration: const InputDecoration( labelText: 'Password', prefixIcon: Icon(Icons.lock), ), obscureText: true, textInputAction: TextInputAction.done, onFieldSubmitted: (_) => _handleLogin(), ); } Widget _buildLoginButton() { return ElevatedButton( onPressed: _isLoggingIn ? null : _handleLogin, child: _isLoggingIn ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Login'), ); } Widget _buildErrorMessage() { return Padding( padding: const EdgeInsets.only(top: 16.0), child: Text( _errorMessage!, style: const TextStyle(color: Colors.red), textAlign: TextAlign.center, ), ); } }