|
|
|
|
@ -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<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;
|
|
|
|
|
bool _isOtpComplete = false;
|
|
|
|
|
bool _isVerifying = false; // Flag to prevent multiple verification calls
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
@ -437,27 +436,25 @@ 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 = 60;
|
|
|
|
|
_isVerifying = false; // Reset verification flag
|
|
|
|
|
});
|
|
|
|
|
_startResendTimer();
|
|
|
|
|
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 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<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; // Reset flag before setting new OTP
|
|
|
|
|
_otpController.text = otp;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|