import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import '../cubit/auth_cubit.dart'; import '../cubit/auth_state.dart'; import '../../../core/design/tokens.dart'; import '../../../core/ui/tripz_scaffold.dart'; import '../../../core/ui/tripz_button.dart'; /// شاشة تحقق OTP — تطابق stitch/otp_verification.html: /// شريط علوي بلون primary + 4 خانات منفصلة (ترتيب LTR ثابت) + عدّاد /// إعادة إرسال + تأكيد تلقائي عند اكتمال الخانات. class OtpScreen extends StatefulWidget { const OtpScreen({super.key}); @override State createState() => _OtpScreenState(); } class _OtpScreenState extends State { static const _otpLength = 4; final List _controllers = List.generate(_otpLength, (_) => TextEditingController()); final List _focusNodes = List.generate(_otpLength, (_) => FocusNode()); int _resendSeconds = 60; Timer? _resendTimer; @override void initState() { super.initState(); _startResendTimer(); WidgetsBinding.instance.addPostFrameCallback((_) { _focusNodes[0].requestFocus(); }); } @override void dispose() { _resendTimer?.cancel(); for (final c in _controllers) { c.dispose(); } for (final f in _focusNodes) { f.dispose(); } super.dispose(); } void _startResendTimer() { _resendSeconds = 60; _resendTimer?.cancel(); _resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_resendSeconds <= 0) { timer.cancel(); } else { setState(() => _resendSeconds--); } }); } String get _otpCode => _controllers.map((c) => c.text).join(); bool get _isComplete => _otpCode.length == _otpLength; void _onDigitChanged(int index, String value) { if (value.length > 1) { // لصق تلقائي: يوزّع الأرقام الملصوقة على الخانات بالترتيب final digits = value.replaceAll(RegExp(r'[^0-9]'), ''); for (int i = 0; i < _otpLength && i < digits.length; i++) { _controllers[i].text = digits[i]; } final nextIdx = digits.length.clamp(0, _otpLength - 1); _focusNodes[nextIdx].requestFocus(); } else if (value.isNotEmpty) { if (index < _otpLength - 1) { _focusNodes[index + 1].requestFocus(); } else { _focusNodes[index].unfocus(); } } setState(() {}); if (_isComplete) { _submit(); } } void _onKeyPress(int index, KeyEvent event) { if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.backspace && _controllers[index].text.isEmpty && index > 0) { _controllers[index - 1].clear(); _focusNodes[index - 1].requestFocus(); setState(() {}); } } void _submit() { if (!_isComplete) return; context.read().verify(_otpCode); } void _resend() { final phone = context.read().state.phone; context.read().sendOtp(phone); _startResendTimer(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final theme = Theme.of(context); final cs = theme.colorScheme; final phone = context.watch().state.phone; return BlocConsumer( listener: (context, state) { if (state.status == AuthStatus.error && state.error != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(state.error!)), ); } }, builder: (context, state) { final isLoading = state.status == AuthStatus.authenticating; return TripzScaffold( padding: false, safeAreaTop: false, bottomAction: TripzButton( onPressed: (_isComplete && !isLoading) ? _submit : null, label: l10n.confirm, loading: isLoading, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── شريط علوي بلون primary (يطابق TopAppBar في stitch) ── SafeArea( bottom: false, child: SizedBox( height: kToolbarHeight, child: ColoredBox( color: cs.primary, child: Padding( padding: const EdgeInsets.symmetric(horizontal: T.s8), child: Row( children: [ IconButton( icon: const BackButtonIcon(), color: cs.onPrimary, onPressed: () => context.go('/phone'), ), const SizedBox(width: T.s8), Text( l10n.confirmPhoneNumber, style: theme.textTheme.titleLarge?.copyWith( color: cs.onPrimary, fontWeight: FontWeight.w700, ), ), ], ), ), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: T.pagePadding), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: T.s24), // نص الشرح + تعديل الرقم Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( l10n.otpSentTo(phone), style: theme.textTheme.bodyLarge?.copyWith( color: cs.onSurfaceVariant, height: 1.5, ), ), ), TextButton( onPressed: () => context.go('/phone'), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: T.s8), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text( l10n.edit, style: theme.textTheme.labelLarge?.copyWith( color: cs.primary, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: T.s32), // ── خانات الرمز — ترتيب LTR ثابت (يطابق dir="ltr" في stitch) ── Directionality( textDirection: TextDirection.ltr, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(_otpLength, (i) { final hasFocus = _focusNodes[i].hasFocus; final hasValue = _controllers[i].text.isNotEmpty; return Padding( padding: EdgeInsetsDirectional.only( end: i < _otpLength - 1 ? T.s16 : 0, ), child: Container( width: 60, height: 64, decoration: BoxDecoration( borderRadius: BorderRadius.circular(T.r12), boxShadow: (hasFocus || hasValue) ? const [T.shadowSm] : null, ), child: KeyboardListener( focusNode: FocusNode(), onKeyEvent: (event) => _onKeyPress(i, event), child: TextField( controller: _controllers[i], focusNode: _focusNodes[i], textAlign: TextAlign.center, keyboardType: TextInputType.number, maxLength: 1, style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, color: cs.onSurface, fontFeatures: const [FontFeature.tabularFigures()], ), inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], decoration: InputDecoration( counterText: '', filled: true, fillColor: cs.surfaceContainerLowest, contentPadding: const EdgeInsets.symmetric(vertical: 14), border: OutlineInputBorder( borderRadius: BorderRadius.circular(T.r12), borderSide: BorderSide(color: cs.outlineVariant), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(T.r12), borderSide: BorderSide( color: hasValue ? cs.primary : cs.outlineVariant, width: hasValue ? 1.5 : 1, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(T.r12), borderSide: BorderSide(color: cs.primary, width: 2), ), ), onChanged: (v) => _onDigitChanged(i, v), ), ), ), ); }), ), ), const SizedBox(height: T.s24), // عدّاد إعادة الإرسال Center( child: _resendSeconds > 0 ? Text( l10n.resendIn(_resendSeconds), style: theme.textTheme.bodyMedium?.copyWith( color: cs.onSurfaceVariant, fontFeatures: const [FontFeature.tabularFigures()], ), ) : TripzButton( onPressed: _resend, style: TripzButtonStyle.text, label: l10n.resendCode, fullWidth: false, ), ), ], ), ), ), ], ), ); }, ); } }