diff --git a/lib/features/authentication/widgets/otp_verification_screen.dart b/lib/features/authentication/widgets/otp_verification_screen.dart index 1ea9be5..b70d05c 100644 --- a/lib/features/authentication/widgets/otp_verification_screen.dart +++ b/lib/features/authentication/widgets/otp_verification_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.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'; @@ -50,12 +51,12 @@ class OTPWidget extends StatefulWidget { final TextInputType keyboardType; final EdgeInsets pinBoxOuterPadding; - OTPWidget({ + const OTPWidget({ Key? key, this.maxLength = 4, this.controller, this.pinBoxWidth = 70.0, - this.pinBoxHeight = 100.0, + this.pinBoxHeight = 70.0, this.pinTextStyle, this.onDone, this.defaultBorderColor = Colors.black, @@ -127,7 +128,9 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi _highlightAnimationController = AnimationController(vsync: this); _initTextController(); _calculateStrList(); - widget.controller!.addListener(_controllerListener); + if (widget.controller != null) { + widget.controller!.addListener(_controllerListener); + } focusNode.addListener(_focusListener); } @@ -190,7 +193,9 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi focusNode.removeListener(_focusListener); } _highlightAnimationController.dispose(); - widget.controller?.removeListener(_controllerListener); + if (widget.controller != null) { + widget.controller!.removeListener(_controllerListener); + } super.dispose(); } @@ -231,7 +236,7 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi width: 0.0, ), ); - return SizedBox( + return Container( width: _width, height: widget.pinBoxHeight, child: TextField( @@ -241,8 +246,6 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi controller: widget.controller, keyboardType: widget.keyboardType, inputFormatters: widget.keyboardType == TextInputType.number ? [FilteringTextInputFormatter.digitsOnly] : null, - // Enable SMS autofill - autofillHints: const [AutofillHints.oneTimeCode], style: TextStyle( height: 0.1, color: Colors.transparent, @@ -301,33 +304,28 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi return _buildPinCode(i, context); }); return Row( + children: pinCodes, mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: pinCodes, ); } Widget _buildPinCode(int i, BuildContext context) { - Color borderColor; - Color pinBoxColor; - - // Determine if OTP is complete - bool isComplete = text.length == widget.maxLength; + Color pinBoxColor = widget.pinBoxColor; if (widget.hasError) { - borderColor = widget.errorBorderColor; - pinBoxColor = widget.pinBoxColor; - } else if (isComplete) { - borderColor = Colors.transparent; - pinBoxColor = AppColors.successColor; + pinBoxColor = widget.errorBorderColor; } else if (i < text.length) { - borderColor = Colors.transparent; - pinBoxColor = AppColors.blackBgColor; + pinBoxColor = AppColors.blackBgColor; // Custom color for filled boxes } else { - borderColor = Colors.transparent; pinBoxColor = widget.pinBoxColor; } + // Change color to success when all fields are complete + if (text.length == widget.maxLength) { + pinBoxColor = AppColors.successColor; + } + EdgeInsets insets; if (i == 0) { insets = EdgeInsets.only( @@ -346,22 +344,21 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi } else { insets = widget.pinBoxOuterPadding; } - return Container( + + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, key: ValueKey("container$i"), alignment: Alignment.center, padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 1.0), margin: insets, - decoration: BoxDecoration( - border: Border.all( - color: borderColor, - width: widget.pinBoxBorderWidth, - ), + child: _animatedTextBox(strList[i], i), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: pinBoxColor, - borderRadius: BorderRadius.circular(widget.pinBoxRadius), + borderRadius: widget.pinBoxRadius, ), width: widget.pinBoxWidth, height: widget.pinBoxHeight, - child: _animatedTextBox(strList[i], i), ); } @@ -407,10 +404,12 @@ class OTPVerificationScreen extends StatefulWidget { class _OTPVerificationScreenState extends State { final int _otpLength = 4; - late TextEditingController _otpController; + late final TextEditingController _otpController; Timer? _resendTimer; int _resendTime = 60; + bool _isOtpComplete = false; + bool _isVerifying = false; // Flag to prevent multiple verification calls @override void initState() { @@ -437,27 +436,25 @@ class _OTPVerificationScreenState extends State { } void _onOtpChanged(String value) { - // Handle clipboard paste or programmatic input - if (value.length > 1) { - String? otp = _extractOtpFromText(value); - if (otp != null) { - autoFillOtp(otp); - return; - } - } - - // The OTPWidget will automatically call onDone when complete - // This method can be used for any additional logic on text change - } + setState(() { + _isOtpComplete = value.length == _otpLength; + }); - void _onOtpCompleted(String otp) { - debugPrint('OTP Completed: $otp'); - widget.checkActivationCode(int.parse(otp)); + if (_isOtpComplete && !_isVerifying) { + _isVerifying = true; + _verifyOtp(value); + } else if (!_isOtpComplete) { + // Reset the flag when OTP is incomplete (user is editing) + _isVerifying = false; + } } void _resendOtp() { if (_resendTime == 0) { - setState(() => _resendTime = 60); + setState(() { + _resendTime = 60; + _isVerifying = false; // Reset verification flag + }); _startResendTimer(); autoFillOtp("1234"); widget.onResendOTPPressed(widget.phoneNumber); @@ -469,68 +466,6 @@ class _OTPVerificationScreenState extends State { return phone.length > 4 ? '05xxxxxx${phone.substring(phone.length - 2)}' : phone; } - /// Extract OTP from text using multiple patterns - String? _extractOtpFromText(String text) { - // Pattern 1: Find 4-6 consecutive digits - RegExp digitPattern = RegExp(r'\b\d{4,6}\b'); - Match? match = digitPattern.firstMatch(text); - - if (match != null) { - String digits = match.group(0)!; - if (digits.length >= _otpLength) { - return digits.substring(0, _otpLength); - } - } - - // Pattern 2: Find digits separated by spaces or special characters - String cleanedText = text.replaceAll(RegExp(r'[^\d]'), ''); - if (cleanedText.length >= _otpLength) { - return cleanedText.substring(0, _otpLength); - } - - return null; - } - - /// Paste OTP from clipboard - Future _pasteFromClipboard() async { - try { - ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null && data.text != null) { - String clipboardText = data.text!; - String? otp = _extractOtpFromText(clipboardText); - - if (otp != null) { - autoFillOtp(otp); - - // Show feedback to user - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('OTP pasted: $otp'), - duration: const Duration(seconds: 2), - backgroundColor: AppColors.successColor, - ), - ); - } else { - // Show error if no valid OTP found - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No valid OTP found in clipboard'), - duration: Duration(seconds: 2), - ), - ); - } - } - } catch (e) { - debugPrint('Error pasting from clipboard: $e'); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to paste from clipboard'), - duration: Duration(seconds: 2), - ), - ); - } - } - @override Widget build(BuildContext context) { return Scaffold( @@ -560,35 +495,30 @@ class _OTPVerificationScreenState extends State { ), SizedBox(height: 40.h), - // OTP Input Fields using new OTPWidget + // OTP Input Fields using new OTP Widget Center( - child: AutofillGroup( - child: OTPWidget( - maxLength: _otpLength, - controller: _otpController, - pinBoxWidth: 75.h, - pinBoxHeight: 100.h, - autoFocus: true, - pinBoxRadius: 16, - pinBoxBorderWidth: 0, - pinBoxOuterPadding: EdgeInsets.symmetric(horizontal: 4.h), - defaultBorderColor: Colors.transparent, - textBorderColor: Colors.transparent, - errorBorderColor: AppColors.primaryRedColor, - pinBoxColor: AppColors.whiteColor, - pinTextStyle: TextStyle( - fontSize: 50.fSize, - fontWeight: FontWeight.bold, - color: AppColors.whiteColor, - ), - onTextChanged: _onOtpChanged, - onDone: _onOtpCompleted, + child: OTPWidget( + maxLength: _otpLength, + controller: _otpController, + pinBoxWidth: 70.h, + pinBoxHeight: 100, + pinBoxRadius: 16, + pinBoxBorderWidth: 0, + pinBoxOuterPadding: EdgeInsets.symmetric(horizontal: 4.h), + defaultBorderColor: Colors.transparent, + textBorderColor: Colors.transparent, + pinBoxColor: AppColors.whiteColor, + autoFocus: true, + onTextChanged: _onOtpChanged, + pinTextStyle: TextStyle( + fontSize: 40.fSize, + fontWeight: FontWeight.bold, + color: AppColors.whiteColor, ), ), ), - const SizedBox(height: 16), - + const SizedBox(height: 32), // Resend OTP Row( @@ -620,50 +550,15 @@ class _OTPVerificationScreenState extends State { ); } + void _verifyOtp(String otp) { + debugPrint('Verifying OTP: $otp'); + widget.checkActivationCode(int.parse(otp)); + } + /// Auto fill OTP into text fields void autoFillOtp(String otp) { if (otp.length != _otpLength) return; - - // Clear any existing text first - _otpController.clear(); - - // Add a small delay to ensure the UI is updated - Future.delayed(const Duration(milliseconds: 50), () { - _otpController.text = otp; - // Move cursor to the end - _otpController.selection = TextSelection.fromPosition( - TextPosition(offset: otp.length), - ); - }); - } - - /// Clear OTP fields - void clearOtp() { - _otpController.clear(); - } - - /// Get current OTP value - String getCurrentOtp() { - return _otpController.text; - } - - /// Check if OTP is complete - bool isOtpComplete() { - return _otpController.text.length == _otpLength; - } - - /// Simulate SMS received with OTP (for testing purposes) - void simulateSMSReceived(String otp) { - if (otp.length == _otpLength && RegExp(r'^\d+$').hasMatch(otp)) { - autoFillOtp(otp); - // Show a brief indicator that SMS was detected - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('OTP detected from SMS: $otp'), - duration: const Duration(seconds: 2), - backgroundColor: AppColors.successColor, - ), - ); - } + _isVerifying = false; // Reset flag before setting new OTP + _otpController.text = otp; } }