import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; class Verify2faScreen extends ConsumerStatefulWidget { const Verify2faScreen({super.key}); @override ConsumerState createState() => _Verify2faScreenState(); } class _Verify2faScreenState extends ConsumerState { final List _controllers = List.generate(6, (_) => TextEditingController()); final List _focusNodes = List.generate(6, (_) => FocusNode()); final TextEditingController _backupController = TextEditingController(); bool _useBackupCode = false; bool _isSubmitting = false; @override void dispose() { for (final c in _controllers) { c.dispose(); } for (final f in _focusNodes) { f.dispose(); } _backupController.dispose(); super.dispose(); } String get _otpCode => _controllers.map((c) => c.text).join(); void _handleOtpInput(int index, String value) { if (value.length > 1) { // Handle paste final digits = value.replaceAll(RegExp(r'[^0-9]'), ''); if (digits.length >= 6) { for (int i = 0; i < 6; i++) { _controllers[i].text = digits[i]; } _focusNodes[5].requestFocus(); _submit(); return; } _controllers[index].text = value[value.length - 1]; } if (value.isNotEmpty && index < 5) { _focusNodes[index + 1].requestFocus(); } // Auto-submit when all 6 digits are entered if (_otpCode.length == 6) { _submit(); } } void _handleKeyDown(int index, KeyEvent event) { if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.backspace) { if (_controllers[index].text.isEmpty && index > 0) { _focusNodes[index - 1].requestFocus(); _controllers[index - 1].clear(); } } } void _submit() { if (_isSubmitting) return; if (_useBackupCode) { final code = _backupController.text.trim(); if (code.isEmpty) return; _isSubmitting = true; ref.read(authProvider.notifier).verifyBackupCode(code); } else { final code = _otpCode; if (code.length != 6) return; _isSubmitting = true; ref.read(authProvider.notifier).verifyTwoFactor(code); } } void _goBackToLogin() { ref.read(authProvider.notifier).clearTwoFactor(); context.go('/login'); } @override Widget build(BuildContext context) { final authState = ref.watch(authProvider); final isLoading = authState.status == AuthStatus.loading; // Navigate when authentication succeeds, reset submit lock on error. // Agents land on /profile (Edit Profile tab — their home/dashboard view). ref.listen(authProvider, (previous, next) { if (next.status == AuthStatus.authenticated) { final isAgent = next.user?.role == 'AGENT'; context.go(isAgent ? '/profile' : '/home'); } if (next.status != AuthStatus.loading) { _isSubmitting = false; } }); return Scaffold( body: Container( width: double.infinity, height: double.infinity, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [AppColors.gradientStart, AppColors.gradientEnd], ), ), child: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( children: [ const SizedBox(height: 40), // Logo Image.asset( 'assets/icons/logo.png', width: 143, height: 41, ), const SizedBox(height: 32), // Lock icon Container( width: 64, height: 64, decoration: BoxDecoration( color: AppColors.accentOrange.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: const Icon( Icons.lock_outlined, color: AppColors.accentOrange, size: 32, ), ), const SizedBox(height: 24), // Title const Text( 'Two-Factor Authentication', style: TextStyle( fontFamily: 'Fractul', fontSize: 25, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), const SizedBox(height: 12), // Subtitle Text( _useBackupCode ? 'Enter one of your backup codes to sign in.' : 'Enter the 6-digit code from your authenticator app.', textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), const SizedBox(height: 8), // Email display if (authState.twoFactorEmail != null) Text( authState.twoFactorEmail!, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, fontWeight: FontWeight.w400, color: AppColors.primaryDark.withValues(alpha: 0.6), ), ), const SizedBox(height: 24), // Error banner if (authState.errorMessage != null) ...[ Container( width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12), decoration: BoxDecoration( color: AppColors.errorBg, border: Border.all(color: AppColors.errorBorder), borderRadius: BorderRadius.circular(7), ), child: Text( authState.errorMessage!, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w300, color: AppColors.error, ), ), ), const SizedBox(height: 16), ], // Code input if (_useBackupCode) _buildBackupCodeInput() else _buildOtpInput(), const SizedBox(height: 24), // Verify button SizedBox( width: double.infinity, child: ElevatedButton( onPressed: isLoading ? null : _submit, child: isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.primaryDark, ), ) : const Text('Verify'), ), ), const SizedBox(height: 24), // Toggle backup code / authenticator GestureDetector( onTap: () { setState(() { _useBackupCode = !_useBackupCode; // Clear inputs for (final c in _controllers) { c.clear(); } _backupController.clear(); }); }, child: Text( _useBackupCode ? 'Use authenticator app instead' : 'Use a backup code instead', style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.accentOrange, ), ), ), const SizedBox(height: 16), // Back to login GestureDetector( onTap: _goBackToLogin, child: const Text( 'Back to login', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), ), const SizedBox(height: 40), ], ), ), ), ), ); } Widget _buildOtpInput() { return Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(6, (index) { return Container( width: 48, height: 56, margin: EdgeInsets.only( right: index < 5 ? 8 : 0, left: index == 3 ? 8 : 0, // Extra gap after 3rd digit ), child: KeyboardListener( focusNode: FocusNode(), onKeyEvent: (event) => _handleKeyDown(index, event), child: TextField( controller: _controllers[index], focusNode: _focusNodes[index], textAlign: TextAlign.center, keyboardType: TextInputType.number, maxLength: 1, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (value) => _handleOtpInput(index, value), style: const TextStyle( fontFamily: 'Fractul', fontSize: 24, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), decoration: InputDecoration( counterText: '', filled: true, fillColor: AppColors.inputFill, contentPadding: const EdgeInsets.symmetric(vertical: 14), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( color: AppColors.primaryDark.withValues(alpha: 0.15), ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( color: AppColors.primaryDark.withValues(alpha: 0.15), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide( color: AppColors.accentOrange, width: 2, ), ), ), ), ), ); }), ); } Widget _buildBackupCodeInput() { return TextField( controller: _backupController, textAlign: TextAlign.center, textInputAction: TextInputAction.done, onSubmitted: (_) => _submit(), style: const TextStyle( fontFamily: 'Fractul', fontSize: 18, fontWeight: FontWeight.w500, color: AppColors.primaryDark, letterSpacing: 2, ), decoration: InputDecoration( hintText: 'Enter backup code', hintStyle: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark.withValues(alpha: 0.4), letterSpacing: 0, ), filled: true, fillColor: AppColors.inputFill, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 16, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( color: AppColors.primaryDark.withValues(alpha: 0.15), ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( color: AppColors.primaryDark.withValues(alpha: 0.15), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide( color: AppColors.accentOrange, width: 2, ), ), ), ); } }