|
|
|
|
@ -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,7 +51,7 @@ class OTPWidget extends StatefulWidget {
|
|
|
|
|
final TextInputType keyboardType;
|
|
|
|
|
final EdgeInsets pinBoxOuterPadding;
|
|
|
|
|
|
|
|
|
|
OTPWidget({
|
|
|
|
|
const OTPWidget({
|
|
|
|
|
Key? key,
|
|
|
|
|
this.maxLength = 4,
|
|
|
|
|
this.controller,
|
|
|
|
|
@ -101,7 +102,7 @@ class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixi
|
|
|
|
|
});
|
|
|
|
|
widget.controller?.text = text;
|
|
|
|
|
widget.controller?.selection = TextSelection.collapsed(offset: text.length);
|
|
|
|
|
} else if (oldWidget.maxLength > widget.maxLength && widget.maxLength > 0 && text.length > 0 && text.length > widget.maxLength) {
|
|
|
|
|
} else if (oldWidget.maxLength > widget.maxLength && widget.maxLength > 0 && text.isNotEmpty && text.length > widget.maxLength) {
|
|
|
|
|
setState(() {
|
|
|
|
|
text = text.substring(0, widget.maxLength);
|
|
|
|
|
currentIndex = text.length;
|
|
|
|
|
@ -127,7 +128,9 @@ class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixi
|
|
|
|
|
_highlightAnimationController = AnimationController(vsync: this);
|
|
|
|
|
_initTextController();
|
|
|
|
|
_calculateStrList();
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
@ -241,12 +246,7 @@ 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,
|
|
|
|
|
),
|
|
|
|
|
style: TextStyle(height: 0.1, color: Colors.transparent),
|
|
|
|
|
decoration: InputDecoration(
|
|
|
|
|
contentPadding: EdgeInsets.all(0),
|
|
|
|
|
focusedErrorBorder: transparentBorder,
|
|
|
|
|
@ -256,10 +256,7 @@ class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixi
|
|
|
|
|
focusedBorder: transparentBorder,
|
|
|
|
|
counterText: null,
|
|
|
|
|
counterStyle: null,
|
|
|
|
|
helperStyle: TextStyle(
|
|
|
|
|
height: 0.0,
|
|
|
|
|
color: Colors.transparent,
|
|
|
|
|
),
|
|
|
|
|
helperStyle: TextStyle(height: 0.0, color: Colors.transparent),
|
|
|
|
|
labelStyle: TextStyle(height: 0.1),
|
|
|
|
|
fillColor: Colors.transparent,
|
|
|
|
|
border: InputBorder.none,
|
|
|
|
|
@ -307,25 +304,19 @@ class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixi
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildPinCode(int i, BuildContext context) {
|
|
|
|
|
Color borderColor;
|
|
|
|
|
Color pinBoxColor;
|
|
|
|
|
|
|
|
|
|
// Determine if OTP is complete
|
|
|
|
|
bool isComplete = text.length == widget.maxLength;
|
|
|
|
|
|
|
|
|
|
if (widget.hasError) {
|
|
|
|
|
borderColor = widget.errorBorderColor;
|
|
|
|
|
pinBoxColor = widget.pinBoxColor;
|
|
|
|
|
} else if (isComplete) {
|
|
|
|
|
borderColor = Colors.transparent;
|
|
|
|
|
pinBoxColor = widget.errorBorderColor;
|
|
|
|
|
} else if (text.length == widget.maxLength) {
|
|
|
|
|
// Check for completion first, before individual box logic
|
|
|
|
|
pinBoxColor = AppColors.successColor;
|
|
|
|
|
} 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;
|
|
|
|
|
pinBoxColor = widget.pinBoxColor; // Default white color
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EdgeInsets insets;
|
|
|
|
|
@ -346,18 +337,17 @@ 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,
|
|
|
|
|
),
|
|
|
|
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
|
|
|
|
color: pinBoxColor,
|
|
|
|
|
borderRadius: BorderRadius.circular(widget.pinBoxRadius),
|
|
|
|
|
borderRadius: widget.pinBoxRadius,
|
|
|
|
|
),
|
|
|
|
|
width: widget.pinBoxWidth,
|
|
|
|
|
height: widget.pinBoxHeight,
|
|
|
|
|
@ -365,6 +355,59 @@ class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixi
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Widget _buildPinCode(int i, BuildContext context) {
|
|
|
|
|
// Color pinBoxColor = widget.pinBoxColor;
|
|
|
|
|
//
|
|
|
|
|
// if (widget.hasError) {
|
|
|
|
|
// pinBoxColor = widget.errorBorderColor;
|
|
|
|
|
// } else if (i < text.length) {
|
|
|
|
|
// pinBoxColor = AppColors.blackBgColor; // Custom color for filled boxes
|
|
|
|
|
// } else {
|
|
|
|
|
// 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(
|
|
|
|
|
// left: 0,
|
|
|
|
|
// top: widget.pinBoxOuterPadding.top,
|
|
|
|
|
// right: widget.pinBoxOuterPadding.right,
|
|
|
|
|
// bottom: widget.pinBoxOuterPadding.bottom,
|
|
|
|
|
// );
|
|
|
|
|
// } else if (i == strList.length - 1) {
|
|
|
|
|
// insets = EdgeInsets.only(
|
|
|
|
|
// left: widget.pinBoxOuterPadding.left,
|
|
|
|
|
// top: widget.pinBoxOuterPadding.top,
|
|
|
|
|
// right: 0,
|
|
|
|
|
// bottom: widget.pinBoxOuterPadding.bottom,
|
|
|
|
|
// );
|
|
|
|
|
// } else {
|
|
|
|
|
// insets = widget.pinBoxOuterPadding;
|
|
|
|
|
// }
|
|
|
|
|
//
|
|
|
|
|
// 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: RoundedRectangleBorder().toSmoothCornerDecoration(
|
|
|
|
|
// color: pinBoxColor,
|
|
|
|
|
// borderRadius: widget.pinBoxRadius,
|
|
|
|
|
// ),
|
|
|
|
|
// width: widget.pinBoxWidth,
|
|
|
|
|
// height: widget.pinBoxHeight,
|
|
|
|
|
// child: _animatedTextBox(strList[i], i),
|
|
|
|
|
// );
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
Widget _animatedTextBox(String text, int i) {
|
|
|
|
|
if (widget.pinTextAnimatedSwitcherTransition != null) {
|
|
|
|
|
return AnimatedSwitcher(
|
|
|
|
|
@ -407,10 +450,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,30 +482,29 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _onOtpChanged(String value) {
|
|
|
|
|
// Handle clipboard paste or programmatic input
|
|
|
|
|
if (value.length >= 4) {
|
|
|
|
|
_onOtpCompleted(value);
|
|
|
|
|
// String? otp = _extractOtpFromText(value);
|
|
|
|
|
// if (otp != null) {
|
|
|
|
|
// autoFillOtp(otp);
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
setState(() {
|
|
|
|
|
_isOtpComplete = value.length == _otpLength;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// The OTPWidget will automatically call onDone when complete
|
|
|
|
|
// This method can be used for any additional logic on text change
|
|
|
|
|
if (_isOtpComplete && !_isVerifying) {
|
|
|
|
|
_isVerifying = true;
|
|
|
|
|
_verifyOtp(value);
|
|
|
|
|
} else if (!_isOtpComplete) {
|
|
|
|
|
// Reset the flag when OTP is incomplete (user is editing)
|
|
|
|
|
_isVerifying = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _onOtpCompleted(String otp) {
|
|
|
|
|
debugPrint('OTP Completed: $otp');
|
|
|
|
|
widget.checkActivationCode(int.parse(otp));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _resendOtp() {
|
|
|
|
|
if (_resendTime == 0) {
|
|
|
|
|
setState(() => _resendTime = 60);
|
|
|
|
|
setState(() {
|
|
|
|
|
_resendTime = 120;
|
|
|
|
|
_isVerifying = false;
|
|
|
|
|
_isOtpComplete = false;
|
|
|
|
|
});
|
|
|
|
|
_otpController.clear();
|
|
|
|
|
_startResendTimer();
|
|
|
|
|
autoFillOtp("1234");
|
|
|
|
|
// autoFillOtp("1234");
|
|
|
|
|
widget.onResendOTPPressed(widget.phoneNumber);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -470,68 +514,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(
|
|
|
|
|
@ -563,33 +545,28 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
|
|
|
|
|
|
|
|
|
|
// OTP Input Fields using new OTP Widget
|
|
|
|
|
Center(
|
|
|
|
|
child: AutofillGroup(
|
|
|
|
|
child: OTPWidget(
|
|
|
|
|
maxLength: _otpLength,
|
|
|
|
|
controller: _otpController,
|
|
|
|
|
pinBoxWidth: 75.h,
|
|
|
|
|
pinBoxHeight: 100.h,
|
|
|
|
|
autoFocus: true,
|
|
|
|
|
pinBoxWidth: 70.h,
|
|
|
|
|
pinBoxHeight: 100,
|
|
|
|
|
pinBoxRadius: 16,
|
|
|
|
|
pinBoxBorderWidth: 0,
|
|
|
|
|
pinBoxOuterPadding: EdgeInsets.symmetric(horizontal: 4.h),
|
|
|
|
|
defaultBorderColor: Colors.transparent,
|
|
|
|
|
textBorderColor: Colors.transparent,
|
|
|
|
|
errorBorderColor: AppColors.primaryRedColor,
|
|
|
|
|
pinBoxColor: AppColors.whiteColor,
|
|
|
|
|
autoFocus: true,
|
|
|
|
|
onTextChanged: _onOtpChanged,
|
|
|
|
|
pinTextStyle: TextStyle(
|
|
|
|
|
fontSize: 50.fSize,
|
|
|
|
|
fontSize: 40.fSize,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: AppColors.whiteColor,
|
|
|
|
|
),
|
|
|
|
|
onTextChanged: _onOtpChanged,
|
|
|
|
|
onDone: _onOtpCompleted,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
|
|
|
|
|
|
// Resend OTP
|
|
|
|
|
Row(
|
|
|
|
|
@ -597,9 +574,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). ',
|
|
|
|
|
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(
|
|
|
|
|
@ -621,84 +607,16 @@ 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 (!mounted) return;
|
|
|
|
|
if (otp.length != _otpLength) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Clear any existing text first
|
|
|
|
|
_otpController.clear();
|
|
|
|
|
|
|
|
|
|
// Use WidgetsBinding to ensure the widget tree is ready
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Set the text first
|
|
|
|
|
_isVerifying = false;
|
|
|
|
|
_otpController.text = otp;
|
|
|
|
|
|
|
|
|
|
// Use a longer delay for iOS and add validation
|
|
|
|
|
Future.delayed(const Duration(milliseconds: 300), () {
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Only attempt to set selection if conditions are met
|
|
|
|
|
if (_otpController.text == otp &&
|
|
|
|
|
_otpController.text.length == _otpLength &&
|
|
|
|
|
_otpController.text.length <= _otpController.text.length) {
|
|
|
|
|
|
|
|
|
|
final newSelection = TextSelection.fromPosition(
|
|
|
|
|
TextPosition(offset: _otpController.text.length),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Validate selection before setting
|
|
|
|
|
if (newSelection.baseOffset <= _otpController.text.length &&
|
|
|
|
|
newSelection.extentOffset <= _otpController.text.length) {
|
|
|
|
|
_otpController.selection = newSelection;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (selectionError) {
|
|
|
|
|
// Silently fail on selection - text is already set correctly
|
|
|
|
|
debugPrint('Selection error (non-critical): $selectionError');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} catch (textError) {
|
|
|
|
|
debugPrint('Error setting OTP text: $textError');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
debugPrint('Error in autoFillOtp: $e');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|