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