import 'package:e_receipt_mobile/presentation/login/login_form_providers.dart'; import 'package:e_receipt_mobile/presentation/login/login_state.dart'; import 'package:e_receipt_mobile/presentation/login/login_view_model.dart'; import 'package:e_receipt_mobile/presentation/login/widgets/login_background.dart'; import 'package:e_receipt_mobile/presentation/login/widgets/login_error_banner.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class LoginPage extends ConsumerWidget { const LoginPage({super.key}); void _submit({ required WidgetRef ref, required LoginState state, required GlobalKey formKey, required TextEditingController usernameController, required TextEditingController passwordController, }) { if (state.isLoading) { return; } ref.read(loginViewModelProvider.notifier).clearMessages(); FocusManager.instance.primaryFocus?.unfocus(); ref.read(loginAttemptedSubmitProvider.notifier).state = true; if (!(formKey.currentState?.validate() ?? false)) { return; } TextInput.finishAutofillContext(); ref.read(loginViewModelProvider.notifier).login( username: usernameController.text, password: passwordController.text, ); } @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(loginViewModelProvider); final colorScheme = Theme.of(context).colorScheme; final formKey = ref.watch(loginFormKeyProvider); final usernameController = ref.watch(loginUsernameControllerProvider); final passwordController = ref.watch(loginPasswordControllerProvider); final usernameFocusNode = ref.watch(loginUsernameFocusNodeProvider); final passwordFocusNode = ref.watch(loginPasswordFocusNodeProvider); final obscurePassword = ref.watch(loginObscurePasswordProvider); final attemptedSubmit = ref.watch(loginAttemptedSubmitProvider); return Scaffold( body: AnnotatedRegion( value: Theme.of(context).brightness == Brightness.dark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark, child: LoginBackground( child: SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 24, ), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 440), child: Card( elevation: 0, color: colorScheme.surface.withOpacity( Theme.of(context).brightness == Brightness.dark ? 0.75 : 0.92, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(28), side: BorderSide( color: colorScheme.outlineVariant.withOpacity(0.35), ), ), child: Padding( padding: const EdgeInsets.fromLTRB(20, 22, 20, 18), child: AutofillGroup( child: Form( key: formKey, autovalidateMode: attemptedSubmit ? AutovalidateMode.onUserInteraction : AutovalidateMode.disabled, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Container( height: 44, width: 44, decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ colorScheme.primary, colorScheme.tertiary, ], ), ), child: Icon( Icons.receipt_long, color: colorScheme.onPrimary, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'E-Receipt', style: Theme.of(context) .textTheme .titleLarge ?.copyWith( fontWeight: FontWeight.w700, ), ), Text( 'Sign in to continue', style: Theme.of(context) .textTheme .bodyMedium ?.copyWith( color: colorScheme .onSurfaceVariant, ), ), ], ), ), ], ), const SizedBox(height: 18), LoginErrorBanner(message: state.errorMessage), if (state.errorMessage != null) const SizedBox(height: 14), TextFormField( controller: usernameController, focusNode: usernameFocusNode, enabled: !state.isLoading, textInputAction: TextInputAction.next, autofillHints: const [AutofillHints.username], onFieldSubmitted: (_) => passwordFocusNode.requestFocus(), validator: (value) { if ((value ?? '').trim().isEmpty) { return 'Username is required'; } return null; }, decoration: InputDecoration( labelText: 'Username', hintText: 'Enter your username', prefixIcon: const Icon(Icons.person_outline), filled: true, fillColor: colorScheme.surfaceContainerHighest .withOpacity( Theme.of(context).brightness == Brightness.dark ? 0.55 : 0.9, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(18), ), ), ), const SizedBox(height: 12), TextFormField( controller: passwordController, focusNode: passwordFocusNode, enabled: !state.isLoading, textInputAction: TextInputAction.done, autofillHints: const [AutofillHints.password], obscureText: obscurePassword, onFieldSubmitted: (_) => _submit( ref: ref, state: state, formKey: formKey, usernameController: usernameController, passwordController: passwordController, ), validator: (value) { if ((value ?? '').isEmpty) { return 'Password is required'; } if (value!.length < 6) { return 'Password must be at least 6 characters'; } return null; }, decoration: InputDecoration( labelText: 'Password', hintText: 'Enter your password', prefixIcon: const Icon(Icons.lock_outline), filled: true, fillColor: colorScheme.surfaceContainerHighest .withOpacity( Theme.of(context).brightness == Brightness.dark ? 0.55 : 0.9, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(18), ), suffixIcon: IconButton( tooltip: obscurePassword ? 'Show password' : 'Hide password', onPressed: state.isLoading ? null : () => ref .read( loginObscurePasswordProvider .notifier, ) .state = !obscurePassword, icon: Icon( obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, ), ), ), ), const SizedBox(height: 16), FilledButton.icon( onPressed: state.isLoading ? null : () => _submit( ref: ref, state: state, formKey: formKey, usernameController: usernameController, passwordController: passwordController, ), icon: state.isLoading ? const SizedBox( height: 18, width: 18, child: CircularProgressIndicator( strokeWidth: 2, ), ) : const Icon(Icons.login), label: Padding( padding: const EdgeInsets.symmetric( vertical: 12, ), child: Text( state.isLoading ? 'Signing in...' : 'Sign in', style: const TextStyle( fontWeight: FontWeight.w600, ), ), ), ), const SizedBox(height: 12), Text( "By continuing, you agree to your organization's policies.", textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall ?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ], ), ), ), ), ), ), ), ), ), ), ), ); } }