/// Email/password sign-in against PocketBase. library; import 'package:flutter/material.dart'; import 'package:flutter/services.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, this.asRoot = false}); /// When true (cold start / auth gate), stay put on success so [AuthGate] /// rebuilds into the shell. When false, pop with `true` for modal sign-in. final bool asRoot; @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, ); // Lets iOS/Android password managers offer to save / confirm the login. TextInput.finishAutofillContext(); if (!mounted) return; if (!widget.asRoot) { 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'), automaticallyImplyLeading: !widget.asRoot, ), body: ResponsiveBody( child: ListView( padding: const EdgeInsets.fromLTRB(16, 24, 16, 32), children: [ Text( widget.asRoot ? 'Welcome to $kAppName' : 'Sync account', style: theme.textTheme.headlineSmall, ), const SizedBox(height: 8), Text( widget.asRoot ? 'Sign in to sync labels, notes, and transactions across ' 'your devices. You can connect your bank afterward.' : 'Sign in to link this device with PocketBase. Labels, notes, ' 'and bank transactions sync across your devices.', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 24), AutofillGroup( child: Form( key: _formKey, child: Column( children: [ AppTextField( controller: _emailController, label: 'Email', keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, autofocus: true, autofillHints: const [ AutofillHints.email, AutofillHints.username, ], 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, autofillHints: const [AutofillHints.password], enableSuggestions: false, autocorrect: false, 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'), ), ], ), ), ), ], ), ), ); } }