/// Email/password sign-in against PocketBase. library; import 'package:flutter/material.dart'; import 'package:pocketbase/pocketbase.dart'; import 'package:provider/provider.dart'; import '../providers/auth_provider.dart'; import '../utils/constants.dart'; import '../widgets/app_text_field.dart'; import '../widgets/responsive_body.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @override State createState() => _LoginScreenState(); } class _LoginScreenState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); bool _busy = false; bool _obscurePassword = true; @override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); } Future _submit() async { if (_busy) return; if (!(_formKey.currentState?.validate() ?? false)) return; setState(() => _busy = true); final auth = context.read(); try { await auth.login( email: _emailController.text, password: _passwordController.text, ); if (!mounted) return; Navigator.of(context).pop(true); } on ClientException catch (error) { if (!mounted) return; final message = error.response['message'] as String? ?? 'Sign-in failed. Check email and password.'; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message)), ); } catch (error) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Could not reach $kPocketBaseUrl')), ); } finally { if (mounted) setState(() => _busy = false); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar(title: const Text('Sign in')), body: ResponsiveBody( child: ListView( padding: const EdgeInsets.fromLTRB(16, 24, 16, 32), children: [ Text( 'Sync account', style: theme.textTheme.headlineSmall, ), const SizedBox(height: 8), Text( 'Sign in to link this device with PocketBase. Your labels, ' 'notes, and bank cache stay on this phone until sync is added.', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 24), Form( key: _formKey, child: Column( children: [ AppTextField( controller: _emailController, label: 'Email', keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, autofocus: true, validator: (value) { final text = value?.trim() ?? ''; if (text.isEmpty) return 'Enter your email'; if (!text.contains('@')) return 'Enter a valid email'; return null; }, ), const SizedBox(height: 16), TextFormField( controller: _passwordController, obscureText: _obscurePassword, textInputAction: TextInputAction.done, onFieldSubmitted: (_) => _submit(), validator: (value) { if (value == null || value.isEmpty) { return 'Enter your password'; } return null; }, decoration: InputDecoration( labelText: 'Password', suffixIcon: IconButton( tooltip: _obscurePassword ? 'Show password' : 'Hide password', onPressed: () => setState( () => _obscurePassword = !_obscurePassword, ), icon: Icon( _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, ), ), ), ), const SizedBox(height: 24), FilledButton.icon( onPressed: _busy ? null : _submit, icon: _busy ? SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: theme.colorScheme.onPrimary, ), ) : const Icon(Icons.login), label: Text(_busy ? 'Signing in…' : 'Sign in'), ), ], ), ), ], ), ), ); } }