import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../app/providers.dart'; import '../../../core/router/app_router.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../core/utils/validators.dart'; import '../../../core/widgets/primary_button.dart'; import '../providers/auth_controller.dart'; /// Store sign-in. The terminal shows this until a valid account is entered. class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState { final _formKey = GlobalKey(); final _email = TextEditingController(text: DemoCredentials.email); final _password = TextEditingController(text: DemoCredentials.password); bool _obscure = true; bool _rememberTerminal = true; @override void dispose() { _email.dispose(); _password.dispose(); super.dispose(); } Future _submit() async { FocusScope.of(context).unfocus(); if (!(_formKey.currentState?.validate() ?? false)) return; final ok = await ref.read(authControllerProvider.notifier).signIn( email: _email.text, password: _password.text, ); if (ok && mounted) context.go(AppRoutes.pos); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.background, body: LayoutBuilder( builder: (context, constraints) { // Below this there isn't room for the brand panel beside the form. final showBrandPanel = constraints.maxWidth >= 1000; return Row( children: [ if (showBrandPanel) const Expanded(flex: 5, child: _BrandPanel()), Expanded( flex: 4, child: _FormPanel( formKey: _formKey, email: _email, password: _password, obscure: _obscure, rememberTerminal: _rememberTerminal, showCompactLogo: !showBrandPanel, onToggleObscure: () => setState(() => _obscure = !_obscure), onToggleRemember: (v) => setState(() => _rememberTerminal = v ?? true), onSubmit: _submit, ), ), ], ); }, ), ); } } class _BrandPanel extends StatelessWidget { const _BrandPanel(); @override Widget build(BuildContext context) { return Container( decoration: const BoxDecoration(gradient: AppColors.primaryGradient), child: SafeArea( child: Padding( padding: const EdgeInsets.all(AppSpacing.giant), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(11), ), alignment: Alignment.center, child: const Text( 'N', style: TextStyle( color: AppColors.primary, fontSize: 23, fontWeight: FontWeight.w800, ), ), ), const SizedBox(width: AppSpacing.md), const Flexible( child: Text( 'Nearle POS', overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.w700, letterSpacing: -0.4, ), ), ), ], ), const SizedBox(height: AppSpacing.giant), const Text( 'Billing that keeps up\nwith your counter.', style: TextStyle( color: Colors.white, fontSize: 34, height: 1.25, fontWeight: FontWeight.w700, letterSpacing: -1, ), ), const SizedBox(height: AppSpacing.lg), Text( 'Scanner-first billing, GST-ready invoices and loyalty ' 'built in — for supermarkets, pharmacies and retail.', style: TextStyle( color: Colors.white.withValues(alpha: 0.78), fontSize: 15, height: 1.6, ), ), const SizedBox(height: AppSpacing.giant), const _Feature( icon: Icons.qr_code_scanner_rounded, title: 'Scan and go', body: 'No dialogs between items. Barcode to bill instantly.', ), const _Feature( icon: Icons.receipt_long_rounded, title: 'GST compliant', body: 'Per-slab tax split into CGST and SGST on every bill.', ), const _Feature( icon: Icons.stars_rounded, title: 'Loyalty that runs itself', body: 'Tiers and points applied without cashier input.', ), ], ), ), ), ), ).animate().fadeIn(duration: 300.ms); } } class _Feature extends StatelessWidget { const _Feature({ required this.icon, required this.title, required this.body, }); final IconData icon; final String title; final String body; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: AppSpacing.xl), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.16), borderRadius: AppRadius.brSm, ), child: Icon(icon, color: Colors.white, size: 19), ), const SizedBox(width: AppSpacing.lg), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( body, style: TextStyle( color: Colors.white.withValues(alpha: 0.72), fontSize: 13, height: 1.5, ), ), ], ), ), ], ), ); } } class _FormPanel extends ConsumerWidget { const _FormPanel({ required this.formKey, required this.email, required this.password, required this.obscure, required this.rememberTerminal, required this.showCompactLogo, required this.onToggleObscure, required this.onToggleRemember, required this.onSubmit, }); final GlobalKey formKey; final TextEditingController email; final TextEditingController password; final bool obscure; final bool rememberTerminal; final bool showCompactLogo; final VoidCallback onToggleObscure; final ValueChanged onToggleRemember; final VoidCallback onSubmit; @override Widget build(BuildContext context, WidgetRef ref) { final auth = ref.watch(authControllerProvider); final session = ref.watch(cashierSessionProvider); final busy = auth is Authenticating; return SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(AppSpacing.xxl), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: Form( key: formKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ if (showCompactLogo) ...[ Center( child: Container( width: 52, height: 52, decoration: BoxDecoration( gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(14), ), alignment: Alignment.center, child: const Text( 'N', style: TextStyle( color: Colors.white, fontSize: 26, fontWeight: FontWeight.w800, ), ), ), ), const SizedBox(height: AppSpacing.xxl), ], Text( 'Sign in to your store', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: AppSpacing.xs), const Text( 'Use the credentials issued when your outlet was ' 'registered.', style: TextStyle( fontSize: 13.5, color: AppColors.textSecondary, height: 1.5, ), ), const SizedBox(height: AppSpacing.xxxl), const _Label('Store email'), TextFormField( controller: email, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, enabled: !busy, validator: (v) => (v ?? '').trim().isEmpty ? 'Store email is required' : Validators.emailOptional(v), decoration: const InputDecoration( hintText: 'store@example.in', prefixIcon: Icon(Icons.storefront_outlined), ), ), const SizedBox(height: AppSpacing.lg), const _Label('Password'), TextFormField( controller: password, obscureText: obscure, enabled: !busy, textInputAction: TextInputAction.done, onFieldSubmitted: (_) => onSubmit(), validator: (v) => (v ?? '').isEmpty ? 'Password is required' : ((v ?? '').length < 6 ? 'Password looks too short' : null), decoration: InputDecoration( hintText: 'Enter your password', prefixIcon: const Icon(Icons.lock_outline_rounded), suffixIcon: IconButton( onPressed: onToggleObscure, icon: Icon( obscure ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 20, ), tooltip: obscure ? 'Show password' : 'Hide password', ), ), ), const SizedBox(height: AppSpacing.sm), // Wrap, not Row — on a narrow tablet these would collide. Wrap( alignment: WrapAlignment.spaceBetween, crossAxisAlignment: WrapCrossAlignment.center, children: [ InkWell( onTap: busy ? null : () => onToggleRemember(!rememberTerminal), borderRadius: AppRadius.brXs, child: Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 22, height: 22, child: Checkbox( value: rememberTerminal, onChanged: busy ? null : onToggleRemember, visualDensity: VisualDensity.compact, ), ), const SizedBox(width: AppSpacing.sm), const Text( 'Remember this terminal', style: TextStyle( fontSize: 13, color: AppColors.textSecondary, ), ), ], ), ), ), TextButton( onPressed: busy ? null : () {}, child: const Text('Forgot password?'), ), ], ), if (auth is AuthFailure) ...[ const SizedBox(height: AppSpacing.sm), Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: const BoxDecoration( color: AppColors.dangerSurface, borderRadius: AppRadius.brSm, ), child: Row( children: [ const Icon(Icons.error_outline_rounded, color: AppColors.danger, size: 18,), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( auth.message, style: const TextStyle( color: AppColors.danger, fontSize: 13, ), ), ), ], ), ).animate().shake(duration: 320.ms, hz: 3), ], const SizedBox(height: AppSpacing.xl), PrimaryButton( label: 'Sign in', icon: Icons.login_rounded, large: true, busy: busy, onPressed: onSubmit, ), const SizedBox(height: AppSpacing.xl), _DemoHint( onFill: busy ? null : () { email.text = DemoCredentials.email; password.text = DemoCredentials.password; }, ), const SizedBox(height: AppSpacing.xxl), Center( child: Text( 'Terminal ${session.terminalId}', style: const TextStyle( fontSize: 11.5, color: AppColors.textTertiary, ), ), ), ], ), ), ), ), ), ); } } class _Label extends StatelessWidget { const _Label(this.text); final String text; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: AppSpacing.sm), child: Text( text, style: const TextStyle( fontSize: 12.5, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ), ), ); } } class _DemoHint extends StatelessWidget { const _DemoHint({this.onFill}); final VoidCallback? onFill; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: AppColors.primarySurface, borderRadius: AppRadius.brSm, border: Border.all(color: AppColors.primaryBorder), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(Icons.info_outline_rounded, size: 17, color: AppColors.primary,), const SizedBox(width: AppSpacing.sm), const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Demo account', style: TextStyle( fontSize: 12.5, fontWeight: FontWeight.w600, color: AppColors.primary, ), ), SizedBox(height: 2), SelectableText( '${DemoCredentials.email} · ${DemoCredentials.password}', style: TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], ), ), TextButton( onPressed: onFill, style: TextButton.styleFrom( minimumSize: const Size(0, 32), padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm), ), child: const Text('Fill', style: TextStyle(fontSize: 12.5)), ), ], ), ); } }