import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get/get.dart'; import 'package:taxglide/consts/app_asstes.dart'; import 'package:taxglide/consts/app_colors.dart'; import 'package:taxglide/consts/comman_button.dart'; import 'package:taxglide/consts/comman_container_auth.dart'; import 'package:taxglide/consts/responsive_helper.dart'; import 'package:taxglide/consts/validation_popup.dart'; import 'package:taxglide/controller/api_contoller.dart'; import 'package:taxglide/router/consts_routers.dart'; import 'package:taxglide/view/Main_controller/main_controller.dart'; class OtpScreen extends ConsumerStatefulWidget { const OtpScreen({super.key}); @override ConsumerState createState() => _OtpScreenState(); } class _OtpScreenState extends ConsumerState { final ValidationPopup _validationPopup = ValidationPopup(); final int otpLength = 4; final List _controllers = []; final List _focusNodes = []; String otpValue = ''; Timer? _timer; int _remainingSeconds = 600; bool _canResend = false; late String mobile; @override void initState() { super.initState(); final args = Get.arguments as Map; mobile = args['mobile'] ?? ''; for (int i = 0; i < otpLength; i++) { _controllers.add(TextEditingController()); _focusNodes.add(FocusNode()); } _startTimer(); } @override void dispose() { _timer?.cancel(); for (var c in _controllers) c.dispose(); for (var f in _focusNodes) f.dispose(); super.dispose(); } void _startTimer() { _canResend = false; _remainingSeconds = 30; _timer?.cancel(); _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_remainingSeconds > 0) { setState(() => _remainingSeconds--); } else { setState(() => _canResend = true); timer.cancel(); } }); } String _formatTime(int seconds) { int minutes = seconds ~/ 60; int remainingSeconds = seconds % 60; return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; } void _resendOtp() async { if (!_canResend) return; for (var controller in _controllers) controller.clear(); setState(() => otpValue = ''); _focusNodes[0].requestFocus(); try { await ref.read(loginProvider.notifier).login(mobile); final loginState = ref.read(loginProvider); loginState.when( data: (result) { if (result['success'] == true) { _validationPopup.showSuccessMessage( context, "OTP has been resent successfully!", ); _startTimer(); for (var controller in _controllers) controller.clear(); setState(() => otpValue = ''); } else { _validationPopup.showErrorMessage( context, result['error'] ?? "Failed to resend OTP", ); } }, loading: () {}, error: (error, stack) { _validationPopup.showErrorMessage( context, "Failed to resend OTP. Please try again.", ); }, ); } catch (e) { _validationPopup.showErrorMessage( context, "An error occurred. Please try again.", ); } } void _verifyOtp() async { if (otpValue.length != otpLength) { _validationPopup.showErrorMessage(context, "Please enter all OTP digits"); return; } await ref.read(loginProvider.notifier).verifyOtp(mobile, otpValue); final loginState = ref.read(loginProvider); loginState.when( data: (result) { if (result['success'] == true) { _validationPopup.showSuccessMessage( context, "OTP Verified Successfully!", ); Future.delayed(const Duration(seconds: 1), () { Get.offAll(() => MainController()); }); } else { _validationPopup.showErrorMessage( context, result['error'] ?? "Invalid OTP. Please try again.", ); } }, loading: () {}, error: (error, stack) { _validationPopup.showErrorMessage( context, "Failed to verify OTP. Please try again.", ); }, ); } void _onOtpChange(int index, String value) { if (value.isNotEmpty && index < otpLength - 1) { _focusNodes[index + 1].requestFocus(); } if (value.isEmpty && index > 0) { _focusNodes[index - 1].requestFocus(); } setState(() { otpValue = _controllers.map((c) => c.text).join(); }); } @override Widget build(BuildContext context) { final loginState = ref.watch(loginProvider); final isLoading = loginState is AsyncLoading; // Initialize responsive utils final r = ResponsiveUtils(context); // Responsive values final logoWidth = r.getValue( mobile: 120, tablet: 141, desktop: 160, ); final logoHeight = r.getValue( mobile: 85, tablet: 100, desktop: 115, ); final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36); final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); final mobileFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14); final resendFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); final timerFontSize = r.fontSize(mobile: 14, tablet: 16, desktop: 18); final otpBoxWidth = r.getValue(mobile: 52, tablet: 60, desktop: 68); final otpBoxHeight = r.getValue( mobile: 48, tablet: 56, desktop: 64, ); final otpFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32); final otpBoxMargin = r.getValue(mobile: 5, tablet: 6, desktop: 8); final otpBoxRadius = r.getValue(mobile: 5, tablet: 6, desktop: 8); final spacingXS = r.spacing(mobile: 10, tablet: 10, desktop: 12); final spacingSM = r.spacing(mobile: 20, tablet: 20, desktop: 24); final spacingMD = r.spacing(mobile: 22, tablet: 22, desktop: 26); final spacingLG = r.spacing(mobile: 30, tablet: 30, desktop: 36); return Scaffold( body: Stack( children: [ // Background gradient Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Color(0xFFFFF8F0), Color(0xFFEBC894), Color(0xFFE8DAF2), Color(0xFFB49EF4), ], stops: [0.0, 0.3, 0.6, 1.0], ), ), ), // Scrollable content SingleChildScrollView( child: SizedBox( height: r.screenHeight, child: Center( child: CommonContainerAuth( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ // Logo Image.asset( AppAssets.taxgildelogoauth, width: logoWidth, height: logoHeight, fit: BoxFit.contain, ), SizedBox(height: spacingSM), // Title Text( "Enter OTP", textAlign: TextAlign.center, style: TextStyle( fontFamily: 'Gilroy-SemiBold', fontSize: titleFontSize, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: 0.01 * titleFontSize, color: AppColors.authheading, ), ), SizedBox(height: spacingSM), // Subtitle Text( "OTP has been sent to your registered mobile number", textAlign: TextAlign.center, style: TextStyle( fontFamily: 'Gilroy-SemiBold', fontSize: subtitleFontSize, fontWeight: FontWeight.w600, height: 1.4, letterSpacing: 0.03 * subtitleFontSize, color: AppColors.authleading, ), ), SizedBox(height: spacingMD), // Mobile number + Change Text.rich( TextSpan( text: mobile, style: TextStyle( fontFamily: 'Gilroy-Medium', fontWeight: FontWeight.w500, fontSize: mobileFontSize, height: 1.8, letterSpacing: 0.01, color: AppColors.authleading, ), children: [ TextSpan( text: " ( Change Number )", style: TextStyle( fontFamily: 'Gilroy-ExtraBold', fontWeight: FontWeight.w800, fontSize: mobileFontSize, height: 1.8, letterSpacing: 0.04, color: AppColors.authchange, ), recognizer: TapGestureRecognizer() ..onTap = () { Get.offNamed( ConstRouters.login, arguments: {'mobile': mobile}, ); }, ), ], ), textAlign: TextAlign.center, ), SizedBox(height: spacingLG), // OTP Boxes Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(otpLength, (index) { bool isFilled = _controllers[index].text.isNotEmpty; return Container( margin: EdgeInsets.symmetric( horizontal: otpBoxMargin, ), width: otpBoxWidth, height: otpBoxHeight, decoration: BoxDecoration( color: isFilled ? AppColors.commanbutton : Colors.white, borderRadius: BorderRadius.circular(otpBoxRadius), border: Border.all( color: const Color(0xFFDFDFDF), ), boxShadow: [ BoxShadow( color: const Color( 0xFFBDBDBD, ).withOpacity(0.25), blurRadius: 7, offset: const Offset(0, 1), ), ], ), child: Center( child: TextField( controller: _controllers[index], focusNode: _focusNodes[index], textAlign: TextAlign.center, keyboardType: TextInputType.number, maxLength: 1, enabled: !isLoading, style: TextStyle( fontFamily: 'Gilroy-Medium', fontWeight: FontWeight.w400, fontSize: otpFontSize, letterSpacing: 0.03 * otpFontSize, color: isFilled ? Colors.white : Colors.black, ), decoration: const InputDecoration( counterText: '', border: InputBorder.none, ), onChanged: (value) => _onOtpChange(index, value), ), ), ); }), ), SizedBox(height: spacingXS), // Resend Align( alignment: Alignment.centerRight, child: GestureDetector( onTap: _canResend && !isLoading ? _resendOtp : null, child: Text( "Resend", textAlign: TextAlign.center, style: TextStyle( fontFamily: 'Gilroy-Medium', fontWeight: FontWeight.w500, fontSize: resendFontSize, height: 1.4, letterSpacing: 0.02 * resendFontSize, color: _canResend && !isLoading ? AppColors.authchange : Colors.grey, ), ), ), ), SizedBox(height: spacingSM), // Timer Text( _formatTime(_remainingSeconds), style: TextStyle( fontFamily: 'Gilroy-Medium', fontWeight: FontWeight.w600, fontSize: timerFontSize, color: _remainingSeconds > 0 ? AppColors.authheading : Colors.red, ), ), SizedBox(height: spacingLG), // Verify Button isLoading ? const CircularProgressIndicator() : CommanButton(text: "Verify", onPressed: _verifyOtp), ], ), ), ), ), ), ], ), ); } }