@ -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 = 10 0.0,
this . pinBoxHeight = 7 0.0,
this . pinTextStyle ,
this . onDone ,
this . defaultBorderColor = Colors . black ,
@ -127,7 +128,9 @@ class OTPWidgetState extends State<OTPWidget> 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<OTPWidget> 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<OTPWidget> with SingleTickerProviderStateMixi
width: 0.0 ,
) ,
) ;
return SizedBox (
return Container (
width: _width ,
height: widget . pinBoxHeight ,
child: TextField (
@ -241,8 +246,6 @@ class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixi
controller: widget . controller ,
keyboardType: widget . keyboardType ,
inputFormatters: widget . keyboardType = = TextInputType . number ? < TextInputFormatter > [ 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<OTPWidget> 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<OTPWidget> with SingleTickerProviderStateMixi
} else {
insets = widget . pinBoxOuterPadding ;
}
return Container (
return AnimatedContainer (
duration: const Duration ( milliseconds: 200 ) ,
curve: Curves . easeInOut ,
key: ValueKey < String > ( " 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 < OTPVerificationScreen > {
final int _otpLength = 4 ;
late TextEditingController _otpController ;
late final TextEditingController _otpController ;
Timer ? _resendTimer ;
int _resendTime = 60 ;
int _resendTime = 120 ;
bool _isOtpComplete = false ;
bool _isVerifying = false ; / / Flag to prevent multiple verification calls
@ override
void initState ( ) {
@ -437,29 +436,27 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
}
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 = 120 ;
_isVerifying = false ; / / Reset verification flag
} ) ;
_startResendTimer ( ) ;
autoFillOtp ( " 1234 " ) ;
/ / autoFillOtp ( " 1234 " ) ;
widget . onResendOTPPressed ( widget . phoneNumber ) ;
}
}
@ -469,68 +466,6 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
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 < void > _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<OTPVerificationScreen> {
) ,
SizedBox ( height: 40. h ) ,
/ / OTP Input Fields using new OTP Widget
/ / 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 (
@ -596,9 +526,18 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
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 ) ,
Builder (
/ / Use a Builder to easily calculate minutes and seconds inline
builder: ( context ) {
final minutes = ( _resendTime ~ / 60 )
. toString ( )
. padLeft ( 2 , ' 0 ' ) ; / / Integer division for minutes final seconds = ( _resendTime % 60 ) . toString ( ) . padLeft ( 2 , ' 0 ' ) ; / / Modulo for remaining seconds
final seconds = ( _resendTime % 60 ) . toString ( ) . padLeft ( 2 , ' 0 ' ) ; / / Modulo for remaining seconds / / < - - - HERE IT IS
return Text (
' resend in ( $ minutes : $ seconds ). ' ,
style: const TextStyle ( color: Colors . grey ) ,
) ;
} ,
)
else
GestureDetector (
@ -620,50 +559,15 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
) ;
}
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 ;
_otpController . text = otp ;
}
}