import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import '../../providers/auth_provider.dart'; import '../../theme/m3_motion.dart'; import '../../utils/snackbar.dart'; class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState with SingleTickerProviderStateMixin { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); late final AnimationController _entranceController; late final Animation _fadeIn; late final Animation _slideIn; bool _isLoading = false; bool _obscurePassword = true; bool _hasValidationError = false; @override void initState() { super.initState(); _entranceController = AnimationController( vsync: this, duration: M3Motion.long, ); _fadeIn = CurvedAnimation( parent: _entranceController, curve: M3Motion.emphasizedEnter, ); _slideIn = Tween( begin: const Offset(0, 0.06), end: Offset.zero, ).animate(_fadeIn); _entranceController.forward(); } @override void dispose() { _entranceController.dispose(); _emailController.dispose(); _passwordController.dispose(); super.dispose(); } Future _handleEmailSignIn() async { if (!_formKey.currentState!.validate()) { setState(() { _hasValidationError = !_hasValidationError; }); return; } setState(() => _hasValidationError = false); setState(() => _isLoading = true); final auth = ref.read(authControllerProvider); try { final response = await auth.signInWithPassword( email: _emailController.text.trim(), password: _passwordController.text, ); if (response.session != null && mounted) { context.go('/tickets'); } else if (mounted) { showInfoSnackBar(context, 'Check your email to confirm sign-in.'); } } on Exception catch (error) { if (mounted) { showErrorSnackBar(context, 'Sign in failed: $error'); } } finally { if (mounted) { setState(() => _isLoading = false); } } } Future _handleOAuthSignIn({required bool google}) async { setState(() => _isLoading = true); final auth = ref.read(authControllerProvider); final redirectTo = kIsWeb ? Uri.base.origin : (Platform.isAndroid ? 'io.supabase.tasq://login-callback' : null); try { if (google) { await auth.signInWithGoogle(redirectTo: redirectTo); } else { await auth.signInWithMeta(redirectTo: redirectTo); } } on Exception catch (error) { if (mounted) { showErrorSnackBar(context, 'OAuth failed: $error'); } } finally { if (mounted) { setState(() => _isLoading = false); } } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final tt = Theme.of(context).textTheme; return Scaffold( body: SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), child: FadeTransition( opacity: _fadeIn, child: SlideTransition( position: _slideIn, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── Branding ── Hero( tag: 'tasq-logo', child: Image.asset( 'assets/tasq_ico.png', height: 80, width: 80, ), ), const SizedBox(height: 16), Text( 'TasQ', style: tt.headlineMedium?.copyWith( fontWeight: FontWeight.w700, color: cs.primary, ), ), const SizedBox(height: 4), Text( 'Task management, simplified', style: tt.bodyMedium?.copyWith( color: cs.onSurfaceVariant, ), ), const SizedBox(height: 32), // ── Sign-in card ── M3ErrorShake( hasError: _hasValidationError, child: Card( elevation: 0, color: cs.surfaceContainerLow, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(28), ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 28, ), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Sign in', style: tt.titleLarge?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 20), TextFormField( controller: _emailController, decoration: const InputDecoration( labelText: 'Email', prefixIcon: Icon(Icons.email_outlined), ), keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Email is required.'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _passwordController, decoration: InputDecoration( labelText: 'Password', prefixIcon: const Icon(Icons.lock_outlined), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, ), onPressed: () => setState( () => _obscurePassword = !_obscurePassword, ), ), ), obscureText: _obscurePassword, textInputAction: TextInputAction.done, onFieldSubmitted: (_) { if (!_isLoading) _handleEmailSignIn(); }, validator: (value) { if (value == null || value.isEmpty) { return 'Password is required.'; } return null; }, ), const SizedBox(height: 24), M3AnimatedSwitcher( child: _isLoading ? const SizedBox( key: ValueKey('loading'), height: 48, child: Center( child: CircularProgressIndicator(), ), ) : FilledButton( key: const ValueKey('sign-in'), onPressed: _handleEmailSignIn, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight( 48, ), ), child: const Text('Sign In'), ), ), ], ), ), ), ), ), // M3ErrorShake const SizedBox(height: 20), // ── Divider ── Row( children: [ Expanded(child: Divider(color: cs.outlineVariant)), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( 'or continue with', style: tt.labelMedium?.copyWith( color: cs.onSurfaceVariant, ), ), ), Expanded(child: Divider(color: cs.outlineVariant)), ], ), const SizedBox(height: 20), // ── OAuth buttons ── Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: _isLoading ? null : () => _handleOAuthSignIn(google: true), icon: const FaIcon( FontAwesomeIcons.google, size: 18, ), label: const Text('Google'), style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(48), ), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton.icon( onPressed: _isLoading ? null : () => _handleOAuthSignIn(google: false), icon: const FaIcon( FontAwesomeIcons.facebook, size: 18, ), label: const Text('Meta'), style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(48), ), ), ), ], ), const SizedBox(height: 24), // ── Create account link ── TextButton( onPressed: _isLoading ? null : () => context.go('/signup'), child: Text.rich( TextSpan( text: "Don't have an account? ", style: tt.bodyMedium?.copyWith( color: cs.onSurfaceVariant, ), children: [ TextSpan( text: 'Sign up', style: TextStyle( color: cs.primary, fontWeight: FontWeight.w600, ), ), ], ), ), ), ], ), ), ), ), ), ), ), ); } }