From 21a9cc8c89350c4d29dddaaa1f8177b62b786563 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Tue, 16 Sep 2025 11:00:19 +0300 Subject: [PATCH] otp screen & register Uae & resend Activation Code. --- lib/core/utils/validation_utils.dart | 43 +++++++++- .../authentication_view_model.dart | 43 +++++----- .../widgets/otp_verification_screen.dart | 23 ++++-- .../authentication/register_step2.dart | 27 ++++++- .../bottomsheet/generic_bottom_sheet.dart | 1 + .../dropdown/country_dropdown_widget.dart | 79 ++++++++++--------- lib/widgets/input_widget.dart | 23 ++---- 7 files changed, 156 insertions(+), 83 deletions(-) diff --git a/lib/core/utils/validation_utils.dart b/lib/core/utils/validation_utils.dart index 098059c..89a8e42 100644 --- a/lib/core/utils/validation_utils.dart +++ b/lib/core/utils/validation_utils.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:hmg_patient_app_new/core/common_models/nationality_country_model.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -33,7 +34,7 @@ class ValidationUtils { if (nationalId != null && nationalId.isNotEmpty && selectedCountry != null) { if (selectedCountry == CountryEnum.saudiArabia) { if (!validateIqama(nationalId)) { - _dialogService.showExceptionBottomSheet(message: "Please enter a valid national ID", onOkPressed: onOkPress); + _dialogService.showExceptionBottomSheet(message: "Please enter a valid Iqama ID", onOkPressed: onOkPress); return false; } } @@ -98,4 +99,44 @@ class ValidationUtils { final regex = RegExp(r'^784\d{4}\d{7}\d{1}$'); return regex.hasMatch(id); } + + static bool validateUaeRegistration({String? name, GenderTypeEnum? gender, NationalityCountries? country, MaritalStatusTypeEnum? maritalStatus, required Function() onOkPress}) { + if (name == null || name.isEmpty) { + _dialogService.showExceptionBottomSheet(message: "Please enter a valid name", onOkPressed: onOkPress); + return false; + } + + if (gender == null) { + _dialogService.showExceptionBottomSheet(message: "Please select a gender", onOkPressed: onOkPress); + return false; + } + + + if (maritalStatus == null) { + _dialogService.showExceptionBottomSheet(message: "Please select a marital status", onOkPressed: onOkPress); + return false; + } + + if (country == null) { + _dialogService.showExceptionBottomSheet(message: "Please select a country", onOkPressed: onOkPress); + return false; + } + + return true; + } + + static bool isValidateEmail({String? email, required Function() onOkPress}) { + if (email == null || email.isEmpty) { + _dialogService.showExceptionBottomSheet(message: "Please enter email", onOkPressed: onOkPress); + return false; + } + final bool emailIsValid = RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}$").hasMatch(email); + + if (!emailIsValid) { + _dialogService.showExceptionBottomSheet(message: "Please enter a valid email format", onOkPressed: onOkPress); + return false; + } + + return true; + } } diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index e17d576..f37dfc6 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -108,6 +108,7 @@ class AuthenticationViewModel extends ChangeNotifier { Future clearDefaultInputValues() async { nationalIdController.clear(); phoneNumberController.clear(); + emailController.clear(); dobController.clear(); maritalStatus = null; genderType = null; @@ -324,7 +325,8 @@ class AuthenticationViewModel extends ChangeNotifier { ); } - Future sendActivationCode({required OTPTypeEnum otpTypeEnum, required String nationalIdOrFileNumber, required String phoneNumber, required bool isForRegister, dynamic payload}) async { + Future sendActivationCode( + {required OTPTypeEnum otpTypeEnum, required String nationalIdOrFileNumber, required String phoneNumber, required bool isForRegister, dynamic payload, bool isComingFromResendOTP = false}) async { var request = RequestUtils.getCommonRequestSendActivationCode( otpTypeEnum: otpTypeEnum, mobileNumber: phoneNumber, @@ -364,7 +366,7 @@ class AuthenticationViewModel extends ChangeNotifier { } else { if (apiResponse.data != null && apiResponse.data['isSMSSent'] == true) { LoaderBottomSheet.hideLoader(); - navigateToOTPScreen(otpTypeEnum: otpTypeEnum, phoneNumber: phoneNumber); + if (!isComingFromResendOTP) navigateToOTPScreen(otpTypeEnum: otpTypeEnum, phoneNumber: phoneNumber, isComingFromRegister: checkIsUserComingForRegister(request: payload), payload: payload); } else { // TODO: Handle isSMSSent false // navigateToOTPScreen(otpTypeEnum: otpTypeEnum, phoneNumber: phoneNumber); @@ -386,8 +388,9 @@ class AuthenticationViewModel extends ChangeNotifier { required String? activationCode, required OTPTypeEnum otpTypeEnum, required Function(String? message) onWrongActivationCode, + Function()? onResendActivation, }) async { - bool isForRegister = (_appState.getUserRegistrationPayload.healthId != null || _appState.getUserRegistrationPayload.patientOutSa == true); + bool isForRegister = (_appState.getUserRegistrationPayload.healthId != null || _appState.getUserRegistrationPayload.patientOutSa == true || _appState.getUserRegistrationPayload.patientOutSa == 1); final request = RequestUtils.getCommonRequestWelcome( phoneNumber: phoneNumberController.text, @@ -553,20 +556,27 @@ class AuthenticationViewModel extends ChangeNotifier { _navigationService.pushAndReplace(AppRoutes.landingScreen); } - Future navigateToOTPScreen({required OTPTypeEnum otpTypeEnum, required String phoneNumber}) async { + Future navigateToOTPScreen({required OTPTypeEnum otpTypeEnum, required String phoneNumber, required bool isComingFromRegister, dynamic payload}) async { _navigationService.pushToOtpScreen( phoneNumber: phoneNumber, checkActivationCode: (int activationCode) async { await checkActivationCode( - activationCode: activationCode.toString(), + activationCode: activationCode.toString(), + otpTypeEnum: otpTypeEnum, + onWrongActivationCode: (String? value) { + onWrongActivationCode(message: value); + }, + ); + }, + onResendOTPPressed: (String phoneNumber) async { + await sendActivationCode( otpTypeEnum: otpTypeEnum, - onWrongActivationCode: (String? value) { - onWrongActivationCode(message: value); - }); - - // Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => RegisterNewStep2(null, {"nationalID": "12345678654321"}))); + phoneNumber: phoneNumberController.text, + nationalIdOrFileNumber: nationalIdController.text, + isForRegister: isComingFromRegister, + isComingFromResendOTP: true, + payload: payload); }, - onResendOTPPressed: (String phoneNumber) {}, ); } @@ -649,19 +659,18 @@ class AuthenticationViewModel extends ChangeNotifier { } Future onRegistrationComplete() async { + LoaderBottomSheet.showLoader(); var request = RequestUtils.getUserSignupCompletionRequest(fullName: nameController.text, emailAddress: emailController.text, gender: genderType, maritalStatus: maritalStatus); - // - print("============= Final Payload ==============="); - print(request); - print("=================== ===================="); - final resultEither = await _authenticationRepo.registerUser(registrationPayloadDataModelRequest: request); resultEither.fold((failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) async { if (apiResponse.data is String) { + //TODO: This Section Need to Be Testing. + _dialogService.showExceptionBottomSheet(message: apiResponse.data, onOkPressed: () {}, onCancelPressed: () {}); //TODO: Here We Need to Show a Dialog Of Something in the case of Fail With OK and Cancel and the Display Variable WIll be result. } else { print(apiResponse.data as Map); if (apiResponse.data["MessageStatus"] == 1) { + LoaderBottomSheet.hideLoader(); //TODO: Here We Need to Show a Dialog Of Something in the case of Success. await clearDefaultInputValues(); // This will Clear All Default Values Of User. _navigationService.pushAndReplace(AppRoutes.loginScreen); @@ -722,8 +731,6 @@ class AuthenticationViewModel extends ChangeNotifier { } else { isOutSideSa = false; } - - print(isOutSideSa); return isOutSideSa; } diff --git a/lib/features/authentication/widgets/otp_verification_screen.dart b/lib/features/authentication/widgets/otp_verification_screen.dart index b70d05c..372598b 100644 --- a/lib/features/authentication/widgets/otp_verification_screen.dart +++ b/lib/features/authentication/widgets/otp_verification_screen.dart @@ -407,7 +407,7 @@ class _OTPVerificationScreenState extends State { late final TextEditingController _otpController; Timer? _resendTimer; - int _resendTime = 60; + int _resendTime = 120; bool _isOtpComplete = false; bool _isVerifying = false; // Flag to prevent multiple verification calls @@ -452,11 +452,11 @@ class _OTPVerificationScreenState extends State { void _resendOtp() { if (_resendTime == 0) { setState(() { - _resendTime = 60; + _resendTime = 120; _isVerifying = false; // Reset verification flag }); _startResendTimer(); - autoFillOtp("1234"); + // autoFillOtp("1234"); widget.onResendOTPPressed(widget.phoneNumber); } } @@ -526,9 +526,18 @@ class _OTPVerificationScreenState extends State { 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( @@ -558,7 +567,7 @@ class _OTPVerificationScreenState extends State { /// Auto fill OTP into text fields void autoFillOtp(String otp) { if (otp.length != _otpLength) return; - _isVerifying = false; // Reset flag before setting new OTP + _isVerifying = false; _otpController.text = otp; } } diff --git a/lib/presentation/authentication/register_step2.dart b/lib/presentation/authentication/register_step2.dart index d7071a4..de61552 100644 --- a/lib/presentation/authentication/register_step2.dart +++ b/lib/presentation/authentication/register_step2.dart @@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/core/common_models/nationality_country_model import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; @@ -66,7 +67,7 @@ class _RegisterNew extends State { children: [ TextInputWidget( labelText: authVM!.isUserFromUAE() ? LocaleKeys.fullName.tr() : LocaleKeys.name.tr(), - hintText: authVM!.isUserFromUAE() ? "" : ("${appState.getNHICUserData.firstNameEn!.toUpperCase()} ${appState.getNHICUserData.lastNameEn!.toUpperCase()}"), + hintText: authVM!.isUserFromUAE() ? "Enter your full name" : ("${appState.getNHICUserData.firstNameEn!.toUpperCase()} ${appState.getNHICUserData.lastNameEn!.toUpperCase()}"), controller: authVM!.isUserFromUAE() ? authVM!.nameController : null, isEnable: true, prefix: null, @@ -275,6 +276,20 @@ class _RegisterNew extends State { icon: AppAssets.confirm, iconColor: AppColors.textGreenColor, onPressed: () { + if (appState.getUserRegistrationPayload.zipCode != CountryEnum.saudiArabia.countryCode) { + if (ValidationUtils.validateUaeRegistration( + name: authVM!.nameController.text, + gender: authVM!.genderType, + country: authVM!.pickedCountryByUAEUser, + maritalStatus: authVM!.maritalStatus, + onOkPress: () { + Navigator.of(context).pop(); + })) { + showModel(context: context); + } + } else { + showModel(context: context); + } // if (isFromDubai) { // if (name == null) { // AppToast.showErrorToast(message: LocaleKeys.enterFullName); @@ -293,8 +308,6 @@ class _RegisterNew extends State { // return; // } // } - - showModel(context: context); }, ), ) @@ -324,7 +337,13 @@ class _RegisterNew extends State { child: CustomButton( text: LocaleKeys.submit, onPressed: () { - authVM!.onRegistrationComplete(); + if (ValidationUtils.isValidateEmail( + email: authVM!.emailController.text, + onOkPress: () { + Navigator.of(context).pop(); + })) { + authVM!.onRegistrationComplete(); + } }, backgroundColor: AppColors.bgGreenColor, borderColor: AppColors.bgGreenColor, diff --git a/lib/widgets/bottomsheet/generic_bottom_sheet.dart b/lib/widgets/bottomsheet/generic_bottom_sheet.dart index 4c7f5af..03145c8 100644 --- a/lib/widgets/bottomsheet/generic_bottom_sheet.dart +++ b/lib/widgets/bottomsheet/generic_bottom_sheet.dart @@ -152,6 +152,7 @@ class _GenericBottomSheetState extends State { prefix: widget.isForEmail ? null : widget.countryCode, isBorderAllowed: false, isAllowLeadingIcon: true, + fontSize: 12.h, isCountryDropDown: widget.isEnableCountryDropdown, leadingIcon: widget.isForEmail ? AppAssets.email : AppAssets.smart_phone, ) diff --git a/lib/widgets/dropdown/country_dropdown_widget.dart b/lib/widgets/dropdown/country_dropdown_widget.dart index 4507423..dd45df4 100644 --- a/lib/widgets/dropdown/country_dropdown_widget.dart +++ b/lib/widgets/dropdown/country_dropdown_widget.dart @@ -69,16 +69,21 @@ class _CustomCountryDropdownState extends State { child: Row( children: [ GestureDetector( - onTap: () { - if (_isDropdownOpen) { - _closeDropdown(); - } else { - _openDropdown(); - } - }, - child: Utils.buildSvgWithAssets(icon: selectedCountry != null ? selectedCountry!.iconPath : AppAssets.ksa, width: 40.h, height: 40.h)), - SizedBox(width: 8.h), - Utils.buildSvgWithAssets(icon: _isDropdownOpen ? AppAssets.dropdow_icon : AppAssets.dropdow_icon), + onTap: () { + if (_isDropdownOpen) { + _closeDropdown(); + } else { + _openDropdown(); + } + }, + child: Row( + children: [ + Utils.buildSvgWithAssets(icon: selectedCountry != null ? selectedCountry!.iconPath : AppAssets.ksa, width: 40.h, height: 40.h), + SizedBox(width: 8.h), + Utils.buildSvgWithAssets(icon: _isDropdownOpen ? AppAssets.dropdow_icon : AppAssets.dropdow_icon), + ], + ), + ), SizedBox(width: 4.h), if (widget.isFromBottomSheet) GestureDetector( @@ -100,19 +105,23 @@ class _CustomCountryDropdownState extends State { ], ), Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( selectedCountry!.countryCode, - style: TextStyle(fontSize: 12.fSize, height: 21 / 18, fontWeight: FontWeight.w600, letterSpacing: -0.2), + style: TextStyle(fontSize: 12.fSize, height: 23 / 18, fontWeight: FontWeight.w600, letterSpacing: -1), ), SizedBox(width: 4.h), if (widget.isEnableTextField) SizedBox( - height: 18, + height: 20, width: 200, + // color: Colors.red, child: TextField( focusNode: textFocusNode, - decoration: InputDecoration(hintText: "", isDense: true, border: InputBorder.none), + style: TextStyle(fontSize: 12.fSize, height: 23 / 18, fontWeight: FontWeight.w600, letterSpacing: -1), + decoration: InputDecoration(hintText: "", isDense: false, border: InputBorder.none), keyboardType: TextInputType.phone, onChanged: widget.onPhoneNumberChanged), ), @@ -146,7 +155,6 @@ class _CustomCountryDropdownState extends State { _overlayEntry = OverlayEntry( builder: (context) => Stack( children: [ - // Dismiss dropdown when tapping outside Positioned.fill( child: GestureDetector( onTap: _closeDropdown, @@ -157,7 +165,7 @@ class _CustomCountryDropdownState extends State { Positioned( top: offset.dy + renderBox.size.height, left: widget.isRtl ? offset.dx + 6.h : offset.dx - 6.h, - width: renderBox.size.width, + width: !widget.isFromBottomSheet ? renderBox.size.width : 60.h, child: Material( child: Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 12), @@ -165,27 +173,26 @@ class _CustomCountryDropdownState extends State { children: widget.countryList .map( (country) => GestureDetector( - onTap: () { - setState(() { - selectedCountry = country; - }); - widget.onCountryChange?.call(country); - _closeDropdown(); - }, - child: Container( - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets(icon: country.iconPath, width: 38.h, height: 38.h), - if (!widget.isFromBottomSheet) SizedBox(width: 12.h), - if (!widget.isFromBottomSheet) - Text(appState.getLanguageCode() == "ar" ? country.nameArabic : country.displayName, - style: TextStyle(fontSize: 14.fSize, height: 21 / 14, fontWeight: FontWeight.w500, letterSpacing: -0.2)), - ], - ), - ), - ), + onTap: () { + setState(() { + selectedCountry = country; + }); + widget.onCountryChange?.call(country); + _closeDropdown(); + }, + child: Container( + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets(icon: country.iconPath, width: 38.h, height: 38.h), + if (!widget.isFromBottomSheet) SizedBox(width: 12.h), + if (!widget.isFromBottomSheet) + Text(appState.getLanguageCode() == "ar" ? country.nameArabic : country.displayName, + style: TextStyle(fontSize: 14.fSize, height: 21 / 14, fontWeight: FontWeight.w500, letterSpacing: -0.2)), + ], + ), + )), ) .toList(), ), diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 66c3877..ab89cc3 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -223,32 +223,21 @@ class TextInputWidget extends StatelessWidget { keyboardType: keyboardType, controller: controller, readOnly: isReadOnly, - textAlignVertical: TextAlignVertical.top, + textAlignVertical: TextAlignVertical.center, textAlign: TextAlign.left, textDirection: TextDirection.ltr, onChanged: onChange, focusNode: focusNode ?? _focusNode, autofocus: autoFocus, textInputAction: TextInputAction.done, - cursorHeight: isWalletAmountInput! ? 40.h : 18.h, - style: TextStyle(fontSize: fontSize!.fSize, height: isWalletAmountInput! ? 1 / 4 : 21 / 14, fontWeight: FontWeight.w500, color: AppColors.textColor, letterSpacing: -0.2), + cursorHeight: isWalletAmountInput! ? 40.h : 20.h, + style: TextStyle(fontSize: fontSize!.fSize, height: isWalletAmountInput! ? 1 / 4 : 16 / 14, fontWeight: FontWeight.w500, color: AppColors.textColor, letterSpacing: -1), decoration: InputDecoration( isDense: true, hintText: hintText, - hintStyle: TextStyle(fontSize: 14.fSize, height: 21 / 16, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -0.2), - prefixIconConstraints: BoxConstraints(minWidth: 45.h), - prefixIcon: prefix == null - ? null - : Text( - "+" + prefix!, - style: TextStyle( - fontSize: 14.fSize, - height: 21 / 14, - fontWeight: FontWeight.w500, - color: Color(0xff2E303A), - letterSpacing: -0.2, - ), - ), + hintStyle: TextStyle(fontSize: 14.fSize, height: 21 / 16, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -1), + prefixIconConstraints: BoxConstraints(minWidth: 30.h), + prefixIcon: prefix == null ? null : "+${prefix!}".toText14(letterSpacing: -1, color: AppColors.textColor, weight: FontWeight.w500), contentPadding: EdgeInsets.zero, border: InputBorder.none, focusedBorder: InputBorder.none,