From 87830e17ab98a34d0f2c3c17d82b8bcf0f970479 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Mon, 15 Sep 2025 16:07:31 +0300 Subject: [PATCH 1/3] otp screen --- .../widgets/otp_verification_screen.dart | 243 +++++------------- 1 file changed, 69 insertions(+), 174 deletions(-) diff --git a/lib/features/authentication/widgets/otp_verification_screen.dart b/lib/features/authentication/widgets/otp_verification_screen.dart index 1ea9be5..b70d05c 100644 --- a/lib/features/authentication/widgets/otp_verification_screen.dart +++ b/lib/features/authentication/widgets/otp_verification_screen.dart @@ -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 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 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 with SingleTickerProviderStateMixi width: 0.0, ), ); - return SizedBox( + return Container( width: _width, height: widget.pinBoxHeight, child: TextField( @@ -241,8 +246,6 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi controller: widget.controller, keyboardType: widget.keyboardType, inputFormatters: widget.keyboardType == TextInputType.number ? [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 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 with SingleTickerProviderStateMixi } else { insets = widget.pinBoxOuterPadding; } - return Container( + + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, key: ValueKey("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 { 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 { } 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 { 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 _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 { ), 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 { ); } + 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; } } From 21a9cc8c89350c4d29dddaaa1f8177b62b786563 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Tue, 16 Sep 2025 11:00:19 +0300 Subject: [PATCH 2/3] 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, From 4e17ff9c82bdae62396c12897b0e686c32d452a5 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Tue, 16 Sep 2025 16:19:30 +0300 Subject: [PATCH 3/3] otp screen & register Uae & resend Activation Code. --- assets/langs/ar-SA.json | 4 +- assets/langs/en-US.json | 4 +- lib/core/api/api_client.dart | 7 +- lib/core/api_consts.dart | 2 +- lib/core/enums.dart | 14 +-- lib/core/utils/validation_utils.dart | 7 +- .../authentication/authentication_repo.dart | 4 +- .../authentication_view_model.dart | 59 ++++++++---- .../widgets/otp_verification_screen.dart | 90 ++++++++++++++----- lib/generated/locale_keys.g.dart | 2 + lib/main.dart | 1 + .../authentication/register_step2.dart | 20 +---- .../authentication/saved_login_screen.dart | 10 +-- lib/services/error_handler_service.dart | 10 ++- 14 files changed, 156 insertions(+), 78 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index bbfa68c..a822898 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -814,5 +814,7 @@ "notNow": "ليس الآن", "pendingActivation": "في انتظار التنشيط", "awaitingApproval": "انتظر القبول", - "ready": "جاهز" + "ready": "جاهز", + "enterValidNationalId": "الرجاء إدخال رقم الهوية الوطنية أو رقم الملف الصحيح", + "enterValidPhoneNumber": "الرجاء إدخال رقم هاتف صالح" } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index e054238..411aa37 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -810,5 +810,7 @@ "notNow": "Not Now", "pendingActivation": "Pending Activation", "awaitingApproval": "Awaiting Approval", - "ready": "Ready" + "ready": "Ready", + "enterValidNationalId": "Please enter a valid national ID or file number", + "enterValidPhoneNumber": "Please enter a valid phone number" } \ No newline at end of file diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 5e9dbb7..b0a5c78 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -222,7 +222,12 @@ class ApiClientImp implements ApiClient { } if (parsed['ErrorType'] == 2) { // todo: handle Logout - logApiEndpointError(endPoint, "session logged out", statusCode); + onFailure( + parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'], + statusCode, + failureType: MessageStatusFailure(parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']), + ); + // logApiEndpointError(endPoint, "session logged out", statusCode); } if (isAllowAny) { onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 42f10f4..bad2e4c 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -726,7 +726,7 @@ const DEACTIVATE_ACCOUNT = 'Services/Patients.svc/REST/PatientAppleActivation_In class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 7ed275a..2f0b078 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -5,6 +5,9 @@ // unverified, // } +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; + enum AuthMethodTypesEnum { sms, whatsApp, @@ -90,15 +93,17 @@ extension LoginTypeExtension on LoginTypeEnum { } String get displayName { + AppState appState = getIt.get(); + bool isArabic = appState.getLanguageID() == "ar"; switch (this) { case LoginTypeEnum.sms: - return 'SMS'; + return isArabic ? 'رسالة نصية' : 'SMS'; case LoginTypeEnum.whatsapp: - return 'WhatsApp'; + return isArabic ? 'واتساب' : 'WhatsApp'; case LoginTypeEnum.face: - return 'Face Recognition'; + return isArabic ? 'القياسات الحيوية' : 'Biometric'; // Or 'بصمة الوجه' case LoginTypeEnum.fingerprint: - return 'Fingerprint'; + return isArabic ? 'بصمة الإصبع' : 'Fingerprint'; } } @@ -190,4 +195,3 @@ extension ServiceTypeEnumExt on ServiceTypeEnum { } } } - diff --git a/lib/core/utils/validation_utils.dart b/lib/core/utils/validation_utils.dart index 89a8e42..e180eaa 100644 --- a/lib/core/utils/validation_utils.dart +++ b/lib/core/utils/validation_utils.dart @@ -1,9 +1,11 @@ import 'dart:developer'; +import 'package:easy_localization/easy_localization.dart'; 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'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; class ValidationUtils { @@ -13,12 +15,12 @@ class ValidationUtils { log("phoneNumber: $phoneNumber"); log("nationalId: $nationalId"); if (nationalId == null || nationalId.isEmpty) { - _dialogService.showErrorBottomSheet(message: "Please enter a valid national ID or file number", onOkPressed: () {}); + _dialogService.showErrorBottomSheet(message: LocaleKeys.enterValidNationalId.tr(), onOkPressed: () {}); return false; } if (phoneNumber == null || phoneNumber.isEmpty) { - _dialogService.showErrorBottomSheet(message: "Please enter a valid phone number", onOkPressed: () {}); + _dialogService.showErrorBottomSheet(message: LocaleKeys.enterValidPhoneNumber.tr(), onOkPressed: () {}); return false; } return true; @@ -111,7 +113,6 @@ class ValidationUtils { return false; } - if (maritalStatus == null) { _dialogService.showExceptionBottomSheet(message: "Please select a marital status", onOkPressed: onOkPress); return false; diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index f3f4119..2eaeb80 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -516,7 +516,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { failure = DataParsingFailure(e.toString()); } }, - ).then( (_) { + ).then((_) { if (failure != null) return Left(failure!); if (apiResponse == null) return Left(ServerFailure("Unknown error")); return Right(apiResponse!); @@ -524,7 +524,5 @@ class AuthenticationRepoImp implements AuthenticationRepo { } catch (e) { return Future.value(Left(UnknownFailure(e.toString()))); } - - } } diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index f37dfc6..8694884 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -283,7 +283,9 @@ class AuthenticationViewModel extends ChangeNotifier { patientOutSA: false, otpTypeEnum: otpTypeEnum, patientId: 0, - zipCode: selectedCountrySignup.countryCode, + zipCode: _appState.getSelectDeviceByImeiRespModelElement != null && _appState.getSelectDeviceByImeiRespModelElement!.outSa == true + ? CountryEnum.unitedArabEmirates.countryCode + : selectedCountrySignup.countryCode, calenderType: calenderType); final result = await _authenticationRepo.checkPatientAuthentication(checkPatientAuthenticationReq: checkPatientAuthenticationReq); @@ -315,7 +317,14 @@ class AuthenticationViewModel extends ChangeNotifier { if (apiResponse.data['IsAuthenticated']) { await checkActivationCode( otpTypeEnum: otpTypeEnum, - onWrongActivationCode: (String? message) {}, + onWrongActivationCode: (String? message) async { + await _dialogService.showCommonBottomSheetWithoutH( + message: message ?? "", + label: LocaleKeys.notice.tr(), + onOkPressed: () { + _navigationService.pop(); + }); + }, activationCode: null, //todo silent login case halded on the repo itself.. ); } @@ -398,7 +407,7 @@ class AuthenticationViewModel extends ChangeNotifier { deviceToken: _appState.deviceToken, // patientOutSA: _appState.getUserRegistrationPayload.projectOutSa == 1 ? true : false, patientOutSA: isForRegister - ? _appState.getUserRegistrationPayload.projectOutSa == 1 + ? _appState.getUserRegistrationPayload.projectOutSa == true ? true : false : _appState.getSelectDeviceByImeiRespModelElement != null @@ -409,7 +418,9 @@ class AuthenticationViewModel extends ChangeNotifier { loginTokenID: _appState.appAuthToken, registeredData: isForRegister ? _appState.getUserRegistrationPayload : null, nationIdText: nationalIdController.text, - countryCode: selectedCountrySignup.countryCode, + countryCode: _appState.getSelectDeviceByImeiRespModelElement != null && _appState.getSelectDeviceByImeiRespModelElement!.outSa == true + ? CountryEnum.unitedArabEmirates.countryCode + : selectedCountrySignup.countryCode, loginType: loginTypeEnum.toInt) .toJson(); LoaderBottomSheet.showLoader(); @@ -454,19 +465,21 @@ class AuthenticationViewModel extends ChangeNotifier { resultEither.fold( (failure) async => await _errorHandlerService.handleError( - failure: failure, - onUnHandledFailure: (failure) async { - LoaderBottomSheet.hideLoader(); - await _dialogService.showCommonBottomSheetWithoutH(message: failure.message, label: LocaleKeys.notice.tr(), onOkPressed: () {}); - }, - ), (apiResponse) async { + failure: failure, + onUnHandledFailure: (failure) async { + LoaderBottomSheet.hideLoader(); + await _dialogService.showCommonBottomSheetWithoutH(message: failure.message, label: LocaleKeys.notice.tr(), onOkPressed: () {}); + }, + onMessageStatusFailure: (failure) async { + LoaderBottomSheet.hideLoader(); + await _dialogService.showCommonBottomSheetWithoutH(message: failure.message, label: LocaleKeys.notice.tr(), onOkPressed: () {}); + }), (apiResponse) async { final activation = CheckActivationCode.fromJson(apiResponse.data as Map); if (activation.errorCode == '699') { // Todo: Hide Loader LoaderBottomSheet.hideLoader(); onWrongActivationCode(activation.errorEndUserMessage); - return; } else if (activation.messageStatus == 2) { LoaderBottomSheet.hideLoader(); @@ -592,9 +605,7 @@ class AuthenticationViewModel extends ChangeNotifier { loginTypeEnum = (_appState.deviceTypeID == 1 ? LoginTypeEnum.face : LoginTypeEnum.fingerprint); if (!_appState.isAuthenticated) { //commenting this api to check either the same flow working or not because this api does not needed further if work fine we will remove this - // await getPatientDeviceData(loginTypeEnum.toInt); - await checkActivationCode(otpTypeEnum: OTPTypeEnum.faceIDFingerprint, activationCode: null, onWrongActivationCode: (String? message) {}); - await insertPatientIMEIData(loginTypeEnum.toInt); + await getPatientDeviceData(loginTypeEnum.toInt); } else { // authenticated = true; await insertPatientIMEIData(loginTypeEnum.toInt); @@ -839,7 +850,25 @@ class AuthenticationViewModel extends ChangeNotifier { resultEither.fold((failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) async { if (apiResponse.messageStatus == 1) { dynamic deviceInfo = apiResponse.data['List_MobileLoginInfo']; - getDeviceLastLogin = deviceInfo['LoginType']; + getDeviceLastLogin = deviceInfo.first['LoginType']; + await checkActivationCode(otpTypeEnum: OTPTypeEnum.faceIDFingerprint, activationCode: null, onWrongActivationCode: (String? message) {}); + await insertPatientIMEIData(loginTypeEnum.toInt); + } + if (apiResponse.messageStatus == 2) { + LoaderBottomSheet.hideLoader(); + await _dialogService.showCommonBottomSheetWithoutH( + message: apiResponse.errorMessage ?? "", + label: LocaleKeys.notice.tr(), + onOkPressed: () { + _dialogService.showPhoneNumberPickerSheet(onSMSPress: () { + checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms); + }, onWhatsappPress: () { + checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp); + }); + }, + onCancelPressed: () { + _navigationService.pop(); + }); } }); } diff --git a/lib/features/authentication/widgets/otp_verification_screen.dart b/lib/features/authentication/widgets/otp_verification_screen.dart index 372598b..e54b08b 100644 --- a/lib/features/authentication/widgets/otp_verification_screen.dart +++ b/lib/features/authentication/widgets/otp_verification_screen.dart @@ -56,7 +56,7 @@ class OTPWidget extends StatefulWidget { this.maxLength = 4, this.controller, this.pinBoxWidth = 70.0, - this.pinBoxHeight = 70.0, + this.pinBoxHeight = 100.0, this.pinTextStyle, this.onDone, this.defaultBorderColor = Colors.black, @@ -102,7 +102,7 @@ class OTPWidgetState extends State 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; @@ -236,7 +236,7 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi width: 0.0, ), ); - return Container( + return SizedBox( width: _width, height: widget.pinBoxHeight, child: TextField( @@ -246,10 +246,7 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi controller: widget.controller, keyboardType: widget.keyboardType, inputFormatters: widget.keyboardType == TextInputType.number ? [FilteringTextInputFormatter.digitsOnly] : null, - style: TextStyle( - height: 0.1, - color: Colors.transparent, - ), + style: TextStyle(height: 0.1, color: Colors.transparent), decoration: InputDecoration( contentPadding: EdgeInsets.all(0), focusedErrorBorder: transparentBorder, @@ -259,10 +256,7 @@ class OTPWidgetState extends State 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, @@ -304,26 +298,25 @@ class OTPWidgetState extends State 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 pinBoxColor = widget.pinBoxColor; + Color pinBoxColor; if (widget.hasError) { 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) { 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; + pinBoxColor = widget.pinBoxColor; // Default white color } EdgeInsets insets; @@ -352,16 +345,69 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi alignment: Alignment.center, padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 1.0), margin: insets, - child: _animatedTextBox(strList[i], i), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: pinBoxColor, borderRadius: widget.pinBoxRadius, ), width: widget.pinBoxWidth, height: widget.pinBoxHeight, + child: _animatedTextBox(strList[i], i), ); } + + // 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("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( @@ -453,8 +499,10 @@ class _OTPVerificationScreenState extends State { if (_resendTime == 0) { setState(() { _resendTime = 120; - _isVerifying = false; // Reset verification flag + _isVerifying = false; + _isOtpComplete = false; }); + _otpController.clear(); _startResendTimer(); // autoFillOtp("1234"); widget.onResendOTPPressed(widget.phoneNumber); diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 8b7f021..a91447a 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -813,5 +813,7 @@ abstract class LocaleKeys { static const pendingActivation = 'pendingActivation'; static const awaitingApproval = 'awaitingApproval'; static const ready = 'ready'; + static const enterValidNationalId = 'enterValidNationalId'; + static const enterValidPhoneNumber = 'enterValidPhoneNumber'; } diff --git a/lib/main.dart b/lib/main.dart index 24f5c36..f791679 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -185,3 +185,4 @@ class MyApp extends StatelessWidget { ); } } +// flutter pub run easy_localization:generate -S assets/langs -f keys -o locale_keys.g.dart \ No newline at end of file diff --git a/lib/presentation/authentication/register_step2.dart b/lib/presentation/authentication/register_step2.dart index de61552..f31909f 100644 --- a/lib/presentation/authentication/register_step2.dart +++ b/lib/presentation/authentication/register_step2.dart @@ -67,7 +67,7 @@ class _RegisterNew extends State { children: [ TextInputWidget( labelText: authVM!.isUserFromUAE() ? LocaleKeys.fullName.tr() : LocaleKeys.name.tr(), - hintText: authVM!.isUserFromUAE() ? "Enter your full name" : ("${appState.getNHICUserData.firstNameEn!.toUpperCase()} ${appState.getNHICUserData.lastNameEn!.toUpperCase()}"), + hintText: authVM!.isUserFromUAE() ? LocaleKeys.enterNameHere.tr() : ("${appState.getNHICUserData.firstNameEn!.toUpperCase()} ${appState.getNHICUserData.lastNameEn!.toUpperCase()}"), controller: authVM!.isUserFromUAE() ? authVM!.nameController : null, isEnable: true, prefix: null, @@ -290,24 +290,6 @@ class _RegisterNew extends State { } else { showModel(context: context); } - // if (isFromDubai) { - // if (name == null) { - // AppToast.showErrorToast(message: LocaleKeys.enterFullName); - // return; - // } - // if (!name!.contains(" ")) if (selectedGenderType == null) { - // AppToast.showErrorToast(message: TranslationBase.of(context).enterFullName); - // return; - // } - // if (selectedMaritalStatusType == null) { - // AppToast.showErrorToast(message: TranslationBase.of(context).chooseMaritalStatus); - // return; - // } - // if (selectedCountry == null) { - // AppToast.showErrorToast(message: TranslationBase.of(context).chooseCountry); - // return; - // } - // } }, ), ) diff --git a/lib/presentation/authentication/saved_login_screen.dart b/lib/presentation/authentication/saved_login_screen.dart index 9f55c85..e0a8885 100644 --- a/lib/presentation/authentication/saved_login_screen.dart +++ b/lib/presentation/authentication/saved_login_screen.dart @@ -56,7 +56,9 @@ class _SavedLogin extends State { onBackPressed: () { Navigator.of(context).pop(); }, - onLanguageChanged: (lang) {}, + onLanguageChanged: (value) { + context.setLocale(value == 'en' ? Locale('en', 'US') : Locale('ar', 'SA')); + }, ), body: SafeArea( child: Padding( @@ -84,7 +86,7 @@ class _SavedLogin extends State { children: [ // Last login info - ("${LocaleKeys.lastLoginBy.tr()} ${loginType.displayName}").toText14(isBold: true, color: AppColors.greyTextColor), + ("${LocaleKeys.lastLoginBy.tr()} ${loginType.displayName}").toText14(isBold: true, color: AppColors.greyTextColor, letterSpacing: -1), (appState.getSelectDeviceByImeiRespModelElement!.createdOn != null ? DateUtil.getFormattedDate(DateUtil.convertStringToDate(appState.getSelectDeviceByImeiRespModelElement!.createdOn!), "d MMMM, y 'at' HH:mm") : '--') @@ -103,7 +105,6 @@ class _SavedLogin extends State { if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) { authVm.loginWithFingerPrintFace(() {}); } else { - // int? val = loginType.toInt; authVm.checkUserAuthentication(otpTypeEnum: loginType == LoginTypeEnum.sms ? OTPTypeEnum.sms : OTPTypeEnum.whatsapp); } }, @@ -167,7 +168,6 @@ class _SavedLogin extends State { onPressed: () { Navigator.of(context).pop(); loginType = LoginTypeEnum.sms; - int? val = loginType.toInt; authVm.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms); }, backgroundColor: AppColors.primaryRedColor, @@ -190,7 +190,6 @@ class _SavedLogin extends State { onPressed: () { Navigator.of(context).pop(); loginType = LoginTypeEnum.whatsapp; - int? val = loginType.toInt; authVm.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp); }, backgroundColor: AppColors.transparent, @@ -230,7 +229,6 @@ class _SavedLogin extends State { authVm.loginWithFingerPrintFace(() {}); } else { loginType = LoginTypeEnum.whatsapp; - int? val = loginType.toInt; authVm.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp); } }, diff --git a/lib/services/error_handler_service.dart b/lib/services/error_handler_service.dart index d4c15f2..c29dc17 100644 --- a/lib/services/error_handler_service.dart +++ b/lib/services/error_handler_service.dart @@ -9,7 +9,7 @@ import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; abstract class ErrorHandlerService { - Future handleError({required Failure failure, Function() onOkPressed, Function(Failure)? onUnHandledFailure}); + Future handleError({required Failure failure, Function() onOkPressed, Function(Failure)? onUnHandledFailure, Function(Failure)? onMessageStatusFailure}); } class ErrorHandlerServiceImp implements ErrorHandlerService { @@ -24,7 +24,7 @@ class ErrorHandlerServiceImp implements ErrorHandlerService { }); @override - Future handleError({required Failure failure, Function()? onOkPressed, Function(Failure)? onUnHandledFailure}) async { + Future handleError({required Failure failure, Function()? onOkPressed, Function(Failure)? onUnHandledFailure, Function(Failure)? onMessageStatusFailure}) async { if (failure is APIException) { loggerService.errorLogs("API Exception: ${failure.message}"); } else if (failure is ServerFailure) { @@ -51,6 +51,12 @@ class ErrorHandlerServiceImp implements ErrorHandlerService { } else { await _showDialog(failure, title: "Error", onOkPressed: onOkPressed); } + } else if (failure is MessageStatusFailure) { + if (onMessageStatusFailure != null) { + onMessageStatusFailure(failure); + } else { + await _showDialog(failure, title: "Error", onOkPressed: onOkPressed); + } } else { loggerService.errorLogs("Unhandled failure type: $failure"); await _showDialog(failure, title: "Error", onOkPressed: onOkPressed);