changes
							parent
							
								
									8f1e4500cc
								
							
						
					
					
						commit
						67e580447e
					
				| @ -0,0 +1,227 @@ | ||||
| import 'dart:async'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; | ||||
| import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; | ||||
| import 'package:hmg_patient_app_new/theme/colors.dart'; | ||||
| import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart'; | ||||
| 
 | ||||
| class OTPVerificationPage extends StatefulWidget { | ||||
|   final String phoneNumber; | ||||
| 
 | ||||
|   const OTPVerificationPage({Key? key, required this.phoneNumber}) : super(key: key); | ||||
| 
 | ||||
|   @override | ||||
|   State<OTPVerificationPage> createState() => _OTPVerificationPageState(); | ||||
| } | ||||
| 
 | ||||
| class _OTPVerificationPageState extends State<OTPVerificationPage> { | ||||
|   final int _otpLength = 4; | ||||
|   late final List<TextEditingController> _controllers; | ||||
|   late final List<FocusNode> _focusNodes; | ||||
| 
 | ||||
|   Timer? _resendTimer; | ||||
|   int _resendTime = 60; | ||||
|   bool _isOtpComplete = false; | ||||
| 
 | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _controllers = List.generate(_otpLength, (_) => TextEditingController()); | ||||
|     _focusNodes = List.generate(_otpLength, (_) => FocusNode()); | ||||
|     _startResendTimer(); | ||||
| 
 | ||||
|     // Focus the first field once the screen is built | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|       if (_focusNodes.isNotEmpty) { | ||||
|         FocusScope.of(context).requestFocus(_focusNodes[0]); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void dispose() { | ||||
|     for (final c in _controllers) c.dispose(); | ||||
|     for (final f in _focusNodes) f.dispose(); | ||||
|     _resendTimer?.cancel(); | ||||
|     super.dispose(); | ||||
|   } | ||||
| 
 | ||||
|   void _startResendTimer() { | ||||
|     _resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) { | ||||
|       if (_resendTime > 0) { | ||||
|         setState(() => _resendTime--); | ||||
|       } else { | ||||
|         timer.cancel(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   void _onOtpChanged(int index, String value) { | ||||
|     if (value.length == 1 && index < _otpLength - 1) { | ||||
|       _focusNodes[index + 1].requestFocus(); | ||||
|     } else if (value.isEmpty && index > 0) { | ||||
|       _focusNodes[index - 1].requestFocus(); | ||||
|     } | ||||
|     _checkOtpCompletion(); | ||||
|   } | ||||
| 
 | ||||
|   void _checkOtpCompletion() { | ||||
|     final isComplete = _controllers.every((c) => c.text.isNotEmpty); | ||||
| 
 | ||||
|     if (isComplete != _isOtpComplete) { | ||||
|       setState(() => _isOtpComplete = isComplete); | ||||
| 
 | ||||
|       if (isComplete) { | ||||
|         _verifyOtp(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void _resendOtp() { | ||||
|     if (_resendTime == 0) { | ||||
|       setState(() => _resendTime = 60); | ||||
|       _startResendTimer(); | ||||
|       autoFillOtp("1234"); | ||||
| 
 | ||||
|       // call resend API here | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   String _getMaskedPhoneNumber() { | ||||
|     final phone = widget.phoneNumber; | ||||
|     return phone.length > 4 ? '05xxxxxx${phone.substring(phone.length - 2)}' : phone; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       appBar: CustomAppBar( | ||||
|         hideLogoAndLang: true, | ||||
|         onBackPressed: () { | ||||
|           Navigator.of(context).pop(); | ||||
|         }, | ||||
|         onLanguageChanged: (lang) {}, | ||||
|       ), | ||||
|       body: SingleChildScrollView( | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.symmetric(horizontal: 24.h), | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               SizedBox(height: 40.h), | ||||
|               Text( | ||||
|                 'OTP Verification', | ||||
|                 style: TextStyle(fontSize: 24.fSize, fontWeight: FontWeight.bold), | ||||
|               ), | ||||
|               SizedBox(height: 16.h), | ||||
|               Text( | ||||
|                 'We have sent you the OTP code on ${_getMaskedPhoneNumber()} via SMS for registration verification', | ||||
|                 style: TextStyle(fontSize: 16.fSize, color: Colors.grey), | ||||
|               ), | ||||
|               SizedBox(height: 40.h), | ||||
| 
 | ||||
|               // OTP Input Fields | ||||
|               SizedBox( | ||||
|                 height: 100, | ||||
|                 child: Row( | ||||
|                   mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||||
|                   children: List.generate(_otpLength, (index) { | ||||
|                     return ValueListenableBuilder<TextEditingValue>( | ||||
|                       valueListenable: _controllers[index], | ||||
|                       builder: (context, value, _) { | ||||
|                         final hasText = value.text.isNotEmpty; | ||||
| 
 | ||||
|                         return AnimatedContainer( | ||||
|                           duration: const Duration(milliseconds: 200), | ||||
|                           curve: Curves.easeInOut, | ||||
|                           width: 70.h, | ||||
|                           margin: EdgeInsets.symmetric(horizontal: 4.h), | ||||
|                           decoration: RoundedRectangleBorder() | ||||
|                               .toSmoothCornerDecoration(color: _isOtpComplete ? AppColors.successColor : (hasText ? AppColors.blackBgColor : AppColors.whiteColor), borderRadius: 16), | ||||
|                           child: Center( | ||||
|                             child: TextField( | ||||
|                               controller: _controllers[index], | ||||
|                               focusNode: _focusNodes[index], | ||||
|                               textAlign: TextAlign.center, | ||||
|                               keyboardType: TextInputType.number, | ||||
|                               maxLength: 1, | ||||
|                               style: TextStyle( | ||||
|                                 fontSize: 40.fSize, | ||||
|                                 fontWeight: FontWeight.bold, | ||||
|                                 color: AppColors.whiteColor, | ||||
|                               ), | ||||
|                               decoration: InputDecoration( | ||||
|                                 counterText: '', | ||||
|                                 filled: true, | ||||
|                                 fillColor: Colors.transparent, | ||||
|                                 border: OutlineInputBorder( | ||||
|                                   borderRadius: BorderRadius.circular(18), | ||||
|                                   borderSide: BorderSide.none, | ||||
|                                 ), | ||||
|                               ), | ||||
|                               onChanged: (v) => _onOtpChanged(index, v), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ); | ||||
|                       }, | ||||
|                     ); | ||||
|                   }), | ||||
|                 ), | ||||
|               ), | ||||
| 
 | ||||
|               const SizedBox(height: 32), | ||||
| 
 | ||||
|               // Resend OTP | ||||
|               Row( | ||||
|                 mainAxisAlignment: MainAxisAlignment.center, | ||||
|                 children: [ | ||||
|                   const Text("Didn't receive it? "), | ||||
|                   if (_resendTime > 0) | ||||
|                     Text( | ||||
|                       'resend in (${_resendTime.toString().padLeft(2, '0')}:00). ', | ||||
|                       style: const TextStyle(color: Colors.grey), | ||||
|                     ) | ||||
|                   else | ||||
|                     GestureDetector( | ||||
|                       onTap: _resendOtp, | ||||
|                       child: const Text( | ||||
|                         'Resend', | ||||
|                         style: TextStyle( | ||||
|                           color: AppColors.primaryRedColor, | ||||
|                           fontWeight: FontWeight.bold, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                 ], | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   void _verifyOtp() { | ||||
|     final otp = _controllers.map((c) => c.text).join(); | ||||
|     debugPrint('Verifying OTP: $otp'); | ||||
| 
 | ||||
|     ScaffoldMessenger.of(context).showSnackBar( | ||||
|       SnackBar(content: Text('Verifying OTP: $otp')), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Auto fill OTP into text fields | ||||
|   void autoFillOtp(String otp) { | ||||
|     if (otp.length != _otpLength) return; | ||||
| 
 | ||||
|     for (int i = 0; i < _otpLength; i++) { | ||||
|       _controllers[i].text = otp[i]; | ||||
|     } | ||||
| 
 | ||||
|     // Move focus to the last field | ||||
|     _focusNodes[_otpLength - 1].requestFocus(); | ||||
| 
 | ||||
|     // Trigger completion check and color update | ||||
|     _checkOtpCompletion(); | ||||
|   } | ||||
| } | ||||
					Loading…
					
					
				
		Reference in New Issue