You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
HMG_Patient_App_New/lib/widgets/otp/otp.dart

231 lines
7.6 KiB
Dart

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/presentation/authentication/register_step2.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')),
);
Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => RegisterNewStep2(null, {"nationalID": "12345678654321"})));
}
/// 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();
}
}