From 15c07cb4e234c5d4a4226715207aa1e22c962f16 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Wed, 17 Sep 2025 14:42:54 +0300 Subject: [PATCH] otp screen & register Uae & resend Activation Code. --- assets/langs/ar-SA.json | 3 +- assets/langs/en-US.json | 3 +- lib/core/api_consts.dart | 2 +- lib/core/app_state.dart | 2 +- .../authentication_view_model.dart | 30 ++++++---- .../widgets/otp_verification_screen.dart | 60 ++++++++++++++++--- lib/generated/locale_keys.g.dart | 3 +- lib/presentation/authentication/login.dart | 7 +++ lib/presentation/authentication/register.dart | 44 +++++++++++--- .../authentication/saved_login_screen.dart | 29 ++++----- lib/services/navigation_service.dart | 14 +---- 11 files changed, 138 insertions(+), 59 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index fd34e55..fb6a919 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -817,5 +817,6 @@ "news": "أخبار", "ready": "جاهز", "enterValidNationalId": "الرجاء إدخال رقم الهوية الوطنية أو رقم الملف الصحيح", - "enterValidPhoneNumber": "الرجاء إدخال رقم هاتف صالح" + "enterValidPhoneNumber": "الرجاء إدخال رقم هاتف صالح", + "iAcceptThe" : "أوافق على" } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index b4cd340..81d20d4 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -813,5 +813,6 @@ "enterValidNationalId": "Please enter a valid national ID or file number", "enterValidPhoneNumber": "Please enter a valid phone number", "ready": "Ready", - "news": "News" + "news": "News", + "iAcceptThe" : "I Accept the" } \ No newline at end of file diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index bad2e4c..42f10f4 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.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index 52500bf..4973e8d 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -62,7 +62,7 @@ class AppState { SelectDeviceByImeiRespModelElement? _selectDeviceByImeiRespModelElement; - void setSelectDeviceByImeiRespModelElement(SelectDeviceByImeiRespModelElement value) { + void setSelectDeviceByImeiRespModelElement(SelectDeviceByImeiRespModelElement? value) { _selectDeviceByImeiRespModelElement = value; } diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index bb41af6..0b76716 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -48,6 +48,7 @@ class AuthenticationViewModel extends ChangeNotifier { final DialogService _dialogService; final NavigationService _navigationService; final LocalAuthService _localAuthService; + AuthenticationViewModel({ required AppState appState, required AuthenticationRepo authenticationRepo, @@ -79,6 +80,8 @@ class AuthenticationViewModel extends ChangeNotifier { CalenderEnum calenderType = CalenderEnum.gregorian; LoginTypeEnum loginTypeEnum = LoginTypeEnum.sms; + final ValueNotifier otpScreenNotifier = ValueNotifier(false); + //================== String errorMsg = ''; @@ -106,7 +109,6 @@ class AuthenticationViewModel extends ChangeNotifier { } Future clearDefaultInputValues() async { - nationalIdController.clear(); phoneNumberController.clear(); emailController.clear(); @@ -394,12 +396,8 @@ class AuthenticationViewModel extends ChangeNotifier { return isUserComingForRegister; } - Future checkActivationCode({ - required String? activationCode, - required OTPTypeEnum otpTypeEnum, - required Function(String? message) onWrongActivationCode, - Function()? onResendActivation, - }) async { + Future checkActivationCode( + {required String? activationCode, required OTPTypeEnum otpTypeEnum, required Function(String? message) onWrongActivationCode, Function()? onResendActivation}) async { bool isForRegister = (_appState.getUserRegistrationPayload.healthId != null || _appState.getUserRegistrationPayload.patientOutSa == true || _appState.getUserRegistrationPayload.patientOutSa == 1); final request = RequestUtils.getCommonRequestWelcome( @@ -422,6 +420,7 @@ class AuthenticationViewModel extends ChangeNotifier { countryCode: _appState.getSelectDeviceByImeiRespModelElement != null && _appState.getSelectDeviceByImeiRespModelElement!.outSa == true ? CountryEnum.unitedArabEmirates.countryCode : selectedCountrySignup.countryCode, + //TODO: Error Here IN Zip Code. loginType: loginTypeEnum.toInt) .toJson(); LoaderBottomSheet.showLoader(); @@ -469,6 +468,7 @@ class AuthenticationViewModel extends ChangeNotifier { failure: failure, onUnHandledFailure: (failure) async { LoaderBottomSheet.hideLoader(); + otpScreenNotifier.value = true; await _dialogService.showCommonBottomSheetWithoutH(message: failure.message, label: LocaleKeys.notice.tr(), onOkPressed: () {}); }, onMessageStatusFailure: (failure) async { @@ -595,6 +595,7 @@ class AuthenticationViewModel extends ChangeNotifier { } Future onWrongActivationCode({String? message}) async { + otpScreenNotifier.value = true; await _dialogService.showErrorBottomSheet(message: message ?? "Something went wrong. ", onOkPressed: () {}); } @@ -622,19 +623,20 @@ class AuthenticationViewModel extends ChangeNotifier { } checkLastLoginStatus(Function() onSuccess) async { - Future.delayed(Duration(seconds: 1), () async { - if(cacheService.getBool(key: CacheConst.quickLoginEnabled) == null){ + if (cacheService.getBool(key: CacheConst.quickLoginEnabled) == null) { if (_appState.getSelectDeviceByImeiRespModelElement != null && (_appState.getSelectDeviceByImeiRespModelElement!.logInType == 1 || _appState.getSelectDeviceByImeiRespModelElement!.logInType == 4)) { - phoneNumberController.text = - (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0") ? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "") : _appState.getAuthenticatedUser()!.mobileNumber)!; + phoneNumberController.text = (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0") + ? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "") + : _appState.getAuthenticatedUser()!.mobileNumber)!; nationalIdController.text = _appState.getAuthenticatedUser()!.nationalityId!; onSuccess(); } else if ((loginTypeEnum == LoginTypeEnum.sms || loginTypeEnum == LoginTypeEnum.whatsapp && _appState.getSelectDeviceByImeiRespModelElement == null) && _appState.getAuthenticatedUser() != null) { - phoneNumberController.text = - (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0") ? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "") : _appState.getAuthenticatedUser()!.mobileNumber)!; + phoneNumberController.text = (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0") + ? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "") + : _appState.getAuthenticatedUser()!.mobileNumber)!; nationalIdController.text = _appState.getAuthenticatedUser()!.nationalityId!; onSuccess(); } @@ -921,4 +923,6 @@ class AuthenticationViewModel extends ChangeNotifier { }, ); } + +// === OTP Widget Logics ===// } diff --git a/lib/features/authentication/widgets/otp_verification_screen.dart b/lib/features/authentication/widgets/otp_verification_screen.dart index db7b1eb..7755ee6 100644 --- a/lib/features/authentication/widgets/otp_verification_screen.dart +++ b/lib/features/authentication/widgets/otp_verification_screen.dart @@ -7,8 +7,10 @@ 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/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart'; +import 'package:provider/provider.dart'; typedef OnDone = void Function(String text); @@ -90,6 +92,7 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi int currentIndex = 0; List strList = []; bool hasFocus = false; + AuthenticationViewModel? authVm; @override void didUpdateWidget(OTPWidget oldWidget) { @@ -124,6 +127,7 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi @override void initState() { super.initState(); + authVm = context.read(); focusNode = widget.focusNode ?? FocusNode(); _highlightAnimationController = AnimationController(vsync: this); _initTextController(); @@ -132,12 +136,15 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi widget.controller!.addListener(_controllerListener); } focusNode.addListener(_focusListener); + authVm?.otpScreenNotifier.addListener(_onOtpScreenNotifierChanged); } void _controllerListener() { if (mounted == true) { setState(() { _initTextController(); + text = widget.controller?.text ?? ""; + currentIndex = text.length; }); var onTextChanged = widget.onTextChanged; if (onTextChanged != null) { @@ -154,6 +161,41 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi } } + void onWrongOtpClear() { + if (mounted) { + setState(() { + text = ""; + currentIndex = 0; + strList.clear(); + _calculateStrList(); + }); + + // Clear the controller if it exists + if (widget.controller != null) { + widget.controller!.clear(); + } + + // Remove focus from the input + if (focusNode.hasFocus) { + focusNode.unfocus(); + } + + // Optionally refocus after a short delay to allow user to re-enter OTP + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted && widget.autoFocus) { + FocusScope.of(context).requestFocus(focusNode); + } + }); + } + } + + void _onOtpScreenNotifierChanged() { + if (authVm?.otpScreenNotifier.value == true) { + onWrongOtpClear(); + authVm?.otpScreenNotifier.value = false; + } + } + void _initTextController() { if (widget.controller == null) { return; @@ -304,19 +346,17 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi ); } - Widget _buildPinCode(int i, BuildContext context) { 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 + pinBoxColor = AppColors.blackBgColor; } else { - pinBoxColor = widget.pinBoxColor; // Default white color + pinBoxColor = widget.pinBoxColor; } EdgeInsets insets; @@ -355,7 +395,6 @@ class OTPWidgetState extends State with SingleTickerProviderStateMixi ); } - // Widget _buildPinCode(int i, BuildContext context) { // Color pinBoxColor = widget.pinBoxColor; // @@ -455,7 +494,7 @@ class _OTPVerificationScreenState extends State { Timer? _resendTimer; int _resendTime = 120; bool _isOtpComplete = false; - bool _isVerifying = false; // Flag to prevent multiple verification calls + bool _isVerifying = false; @override void initState() { @@ -504,11 +543,18 @@ class _OTPVerificationScreenState extends State { }); _otpController.clear(); _startResendTimer(); - // autoFillOtp("1234"); widget.onResendOTPPressed(widget.phoneNumber); } } + void _clearOtpAndResetState() { + setState(() { + _isVerifying = false; + _isOtpComplete = false; + }); + _otpController.clear(); + } + String _getMaskedPhoneNumber() { final phone = widget.phoneNumber; return phone.length > 4 ? '05xxxxxx${phone.substring(phone.length - 2)}' : phone; diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 9a341a2..640c4cd 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -812,9 +812,10 @@ abstract class LocaleKeys { static const notNow = 'notNow'; static const pendingActivation = 'pendingActivation'; static const awaitingApproval = 'awaitingApproval'; + static const news = 'news'; static const ready = 'ready'; static const enterValidNationalId = 'enterValidNationalId'; static const enterValidPhoneNumber = 'enterValidPhoneNumber'; + static const iAcceptThe = 'iAcceptThe'; - static const news = 'news'; } diff --git a/lib/presentation/authentication/login.dart b/lib/presentation/authentication/login.dart index 2a8c744..f3d7664 100644 --- a/lib/presentation/authentication/login.dart +++ b/lib/presentation/authentication/login.dart @@ -4,6 +4,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.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/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -153,6 +155,7 @@ class LoginScreenState extends State { required TextEditingController? phoneNumberController, required AuthenticationViewModel authViewModel, }) { + AppState appState = getIt(); context.showBottomSheet( isScrollControlled: true, isDismissible: false, @@ -180,6 +183,8 @@ class LoginScreenState extends State { onOkPress: () { Navigator.of(context).pop(); })) { + Navigator.of(context).pop(); + appState.setSelectDeviceByImeiRespModelElement(null); await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms); } }, @@ -209,6 +214,8 @@ class LoginScreenState extends State { onOkPress: () { Navigator.of(context).pop(); })) { + Navigator.of(context).pop(); + appState.setSelectDeviceByImeiRespModelElement(null); await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp); } }, diff --git a/lib/presentation/authentication/register.dart b/lib/presentation/authentication/register.dart index 8ec988d..5150b0c 100644 --- a/lib/presentation/authentication/register.dart +++ b/lib/presentation/authentication/register.dart @@ -2,6 +2,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.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/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -152,12 +154,36 @@ class _RegisterNew extends State { }, ), SizedBox(width: 12.h), - Expanded( - child: Text( - LocaleKeys.iAcceptTermsConditions.tr(), - style: context.dynamicTextStyle(fontSize: 14.fSize, fontWeight: FontWeight.w500, color: Color(0xFF2E3039)), - ), + Row( + children: [ + Text( + LocaleKeys.iAcceptThe.tr(), + style: context.dynamicTextStyle(fontSize: 14.fSize, fontWeight: FontWeight.w500, color: Color(0xFF2E3039)), + ), + GestureDetector( + onTap: () { + // Navigate to terms and conditions page + Navigator.of(context).pushNamed('/terms'); + }, + child: Text( + LocaleKeys.termsConditoins.tr(), + style: context.dynamicTextStyle( + fontSize: 14.fSize, + fontWeight: FontWeight.w500, + color: AppColors.primaryRedColor, + decoration: TextDecoration.underline, + decorationColor: AppColors.primaryRedBorderColor, + ), + ), + ), + ], ), + // Expanded( + // child: Text( + // LocaleKeys.iAcceptTermsConditions.tr().split("the").first, + // style: context.dynamicTextStyle(fontSize: 14.fSize, fontWeight: FontWeight.w500, color: Color(0xFF2E3039)), + // ), + // ), ], ), ), @@ -224,6 +250,7 @@ class _RegisterNew extends State { } void showRegisterModel({required BuildContext context, required AuthenticationViewModel authVM}) { + AppState appState = getIt.get(); showModalBottomSheet( context: context, isScrollControlled: true, @@ -235,7 +262,7 @@ class _RegisterNew extends State { child: SingleChildScrollView( child: GenericBottomSheet( countryCode: authVM.selectedCountrySignup.countryCode, - initialPhoneNumber: "", + initialPhoneNumber: authVM.phoneNumberController.text, textController: authVM.phoneNumberController, isEnableCountryDropdown: false, onCountryChange: authVM.onCountryChange, @@ -256,6 +283,7 @@ class _RegisterNew extends State { Navigator.of(context).pop(); }, )) { + appState.setSelectDeviceByImeiRespModelElement(null); await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.sms); } }, @@ -280,15 +308,14 @@ class _RegisterNew extends State { child: CustomButton( text: LocaleKeys.sendOTPWHATSAPP.tr(), onPressed: () async { - // Dismiss keyboard before validation FocusScope.of(context).unfocus(); - if (ValidationUtils.isValidatePhone( phoneNumber: authVM.phoneNumberController.text, onOkPress: () { Navigator.of(context).pop(); }, )) { + appState.setSelectDeviceByImeiRespModelElement(null); await authVM.onRegistrationStart(otpTypeEnum: OTPTypeEnum.whatsapp); } }, @@ -296,6 +323,7 @@ class _RegisterNew extends State { borderColor: AppColors.borderOnlyColor, textColor: AppColors.textColor, icon: AppAssets.whatsapp, + iconColor: null, ), ), ], diff --git a/lib/presentation/authentication/saved_login_screen.dart b/lib/presentation/authentication/saved_login_screen.dart index 0a6138c..073ff8b 100644 --- a/lib/presentation/authentication/saved_login_screen.dart +++ b/lib/presentation/authentication/saved_login_screen.dart @@ -12,6 +12,8 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; +import 'package:hmg_patient_app_new/presentation/home/landing_page.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart'; import 'package:hmg_patient_app_new/widgets/bottomsheet/generic_bottom_sheet.dart'; @@ -164,17 +166,16 @@ class _SavedLogin extends State { Padding( padding: EdgeInsets.only(bottom: 10.h), child: CustomButton( - text: LocaleKeys.sendOTPSMS.tr(), - onPressed: () { - Navigator.of(context).pop(); - loginType = LoginTypeEnum.sms; - authVm.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms); - }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedBorderColor, - textColor: AppColors.whiteColor, - icon: AppAssets.sms, - ), + text: LocaleKeys.sendOTPSMS.tr(), + onPressed: () { + Navigator.of(context).pop(); + loginType = LoginTypeEnum.sms; + authVm.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + icon: AppAssets.sms), ), Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -247,13 +248,13 @@ class _SavedLogin extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: Container( + child: SizedBox( height: 56, child: CustomButton( text: LocaleKeys.guest.tr(), onPressed: () { Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (BuildContext context) => LoginScreen()), + MaterialPageRoute(builder: (BuildContext context) => LandingNavigation()), ); }, backgroundColor: Color(0xffFEE9EA), @@ -277,7 +278,7 @@ class _SavedLogin extends State { text: LocaleKeys.switchAccount.tr(), onPressed: () async { await authVm.clearDefaultInputValues(); - Navigator.of(context).pushReplacement( + Navigator.of(context).push( MaterialPageRoute(builder: (BuildContext context) => LoginScreen()), ); }, diff --git a/lib/services/navigation_service.dart b/lib/services/navigation_service.dart index 9835bba..420c4cf 100644 --- a/lib/services/navigation_service.dart +++ b/lib/services/navigation_service.dart @@ -22,19 +22,9 @@ class NavigationService { navigatorKey.currentState?.pushReplacementNamed(routeName); } - Future pushToOtpScreen({ - required String phoneNumber, - required Function(int code) checkActivationCode, - required Function(String phoneNumber) onResendOTPPressed, - }) { + Future pushToOtpScreen({required String phoneNumber, required Function(int code) checkActivationCode, required Function(String phoneNumber) onResendOTPPressed}) { return navigatorKey.currentState!.push( - MaterialPageRoute( - builder: (_) => OTPVerificationScreen( - phoneNumber: phoneNumber, - checkActivationCode: checkActivationCode, - onResendOTPPressed: onResendOTPPressed, - ), - ), + MaterialPageRoute(builder: (_) => OTPVerificationScreen(phoneNumber: phoneNumber, checkActivationCode: checkActivationCode, onResendOTPPressed: onResendOTPPressed)), ); }