diff --git a/.gitignore b/.gitignore index 79c113f..3ada50c 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +/android/ diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 0a7ad1b..64477a3 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -776,6 +776,19 @@ "validPassportNumber": "يرجى إدخال رقم جواز سفر صالح", "continuePlan": "متابعة خطة العلاج؟", "aboutApp": "حول التطبيق", + "loginOrRegister": "تسجيل الدخول أو التسجيل", "dontHaveAccount": "ليس لديك حساب؟", - "loginOrRegister": "تسجيل الدخول أو التسجيل" + "receiveOtpToast": "أين تود تلقي رمز التحقق OTP؟", + "enterPhoneNumber": "أدخل رقم الهاتف", + "enterEmailDesc": "أدخل عنوان بريدك الإلكتروني لإكمال عملية إنشاء ملف طبي", + "enterPhoneDesc": "أدخل رقم هاتفك لتلقي رمز التحقق ", + "pleaseChooseOption": "الرجاء اختيار من الخيارات أدناه لتلقي رمز التحقق OTP", + "prepareToElevate": "هل أنت مستعد لتحسين صحتك ورفاهتك؟", + "iAcceptTermsConditions": "أوافق على الشروط والأحكام", + "alreadyHaveAccount": "هل لديك حساب بالفعل؟", + "loginNow": "تسجيل الدخول الآن", + "notice": "إشعار", + "oR": "أو", + "sendOTPWHATSAPP": "أرسل لي OTP عبر واتساب", + "sendOTPSMS": "أرسل لي OTP عبر الرسائل القصيرة" } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 4c2c5c0..cd15a5a 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -772,6 +772,19 @@ "aboutApp": "About the app", "aboutPoints": "Online Appointment Booking & rescheduling, Insurance approval status, Find A doctor, Ask your doctor, Medical prescriptions, Lab results, Hospitals contact numbers, Doctor profiles, Hospitals locations, Pharmacies Locations, Hospital's Virtual Tour, Official Social Media, Vaccines Schedule, Health Calculators, Other Services", "termsConditions": "These Online Services Terms of Use (Service Terms) govern certain online services provided by Dr Sulaiman Al Habib Medical Services Group Company (HMG, we, us, our)...", + "receiveOtpToast": "Where would you like to receive OTP?", + "enterPhoneNumber": "Enter Phone Number", + "enterEmailDesc": "Enter your email address to complete the process of creating a medical file", + "enterPhoneDesc": "Enter your phone number to receive OTP verification code", + "pleaseChooseOption": "Please select from the below options to receive OTP", "dontHaveAccount": "Don't have an account?", - "loginOrRegister": "Login or Register" + "loginOrRegister": "Login or Register", + "prepareToElevate": "Prepared to elevate your health and well-being?", + "iAcceptTermsConditions": "I Accept the Terms and Conditions", + "alreadyHaveAccount": "Already have an account?", + "loginNow": "Login Now", + "notice": "Notice", + "oR": "OR", + "sendOTPWHATSAPP": "Send me OTP on Whatsapp", + "sendOTPSMS": "Send me OTP on SMS" } \ No newline at end of file diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index bdca5ff..ed4770d 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -13,6 +13,7 @@ import 'package:hmg_patient_app_new/core/dependencies.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/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/dialogs/confirm_dialog.dart'; @@ -59,14 +60,13 @@ class Utils { // )); return !isAddHours ? DateFormat('hh:mm a', appState.isArabic() ? "ar_SA" : "en_US") - .format(DateTime.tryParse(startTime.contains("T") ? startTime : convertStringToDateTime(startTime))!.toLocal()) + .format(DateTime.tryParse(startTime.contains("T") ? startTime : convertStringToDateTime(startTime))!.toLocal()) : DateFormat('hh:mm a', appState.isArabic() ? "ar_SA" : "en_US") - .format(DateTime.tryParse(startTime.contains("T") ? startTime : convertStringToDateTime(startTime))!.add( - Duration( - hours: isAddHours ? 3 : 0, - ), - )); - ; + .format(DateTime.tryParse(startTime.contains("T") ? startTime : convertStringToDateTime(startTime))!.add( + Duration( + hours: isAddHours ? 3 : 0, + ), + )); } static String convertStringToDateTime(String dateTimeString) { @@ -209,12 +209,12 @@ class Utils { builder: (BuildContext context) => LoadingDialog(), ) .then((value) { - _isLoadingVisible = false; - }) + _isLoadingVisible = false; + }) .catchError((e) {}) .onError( (error, stackTrace) {}, - ); + ); } static void hideLoading() { @@ -236,11 +236,12 @@ class Utils { showDialog( barrierDismissible: false, context: context, - builder: (cxt) => ConfirmDialog( - title: title!, - message: message!, - onTap: onTap, - ), + builder: (cxt) => + ConfirmDialog( + title: title!, + message: message!, + onTap: onTap, + ), ); } @@ -371,7 +372,9 @@ class Utils { static String formatHijriDateToDisplay(String hijriDateString) { try { // Assuming hijriDateString is in the format yyyy-MM-dd - final datePart = hijriDateString.split("T").first; + final datePart = hijriDateString + .split("T") + .first; final parts = datePart.split('-'); if (parts.length != 3) return ""; @@ -429,8 +432,14 @@ class Utils { void Function(LottieComposition)? onLoaded, }) { return Lottie.asset(assetPath, - height: height ?? MediaQuery.of(context).size.height * 0.26, - width: width ?? MediaQuery.of(context).size.width, + height: height ?? MediaQuery + .of(context) + .size + .height * 0.26, + width: width ?? MediaQuery + .of(context) + .size + .width, fit: fit, alignment: alignment, repeat: repeat, @@ -487,9 +496,13 @@ class Utils { ], ); } + static Future isGoogleServicesAvailable() async { GooglePlayServicesAvailability availability = await GoogleApiAvailability.instance.checkGooglePlayServicesAvailability(); - String status = availability.toString().split('.').last; + String status = availability + .toString() + .split('.') + .last; if (status == "success") { return true; } @@ -510,3 +523,26 @@ class Utils { return crypto.md5.convert(utf8.encode(input)).toString(); } } + +class ValidationUtils { + static DialogService dialogService = getIt.get(); + + + static bool isValidatePhoneAndId({ + String? nationalId, + String? phoneNumber + }) { + if (nationalId == null || nationalId.isEmpty) { + dialogService.showErrorDialog(message: "Please enter a valid national ID or file number", onOkPressed: () {}); + return false; + } + + if (phoneNumber == null || phoneNumber.isEmpty) { + dialogService.showErrorDialog(message: "Please enter a valid phone number", onOkPressed: () {}); + return false; + } + return true; + } +} + + diff --git a/lib/extensions/context_extensions.dart b/lib/extensions/context_extensions.dart index 5783ce3..4e9da0d 100644 --- a/lib/extensions/context_extensions.dart +++ b/lib/extensions/context_extensions.dart @@ -2,15 +2,32 @@ import 'package:flutter/material.dart'; extension ContextUtils on BuildContext { double get screenHeight => MediaQuery.of(this).size.height; + double get screenWidth => MediaQuery.of(this).size.width; + ThemeData get theme => Theme.of(this); + TextTheme get textTheme => theme.textTheme; - // TextStyle get headline1 => textTheme.headline1!; - // TextStyle get headline2 => textTheme.headline2!; - // TextStyle get headline3 => textTheme.headline3!; - // TextStyle get headline4 => textTheme.headline4!; - // TextStyle get headline5 => textTheme.headline5!; - // TextStyle get headline6 => textTheme.headline6!; - // TextStyle get bodyText1 => textTheme.bodyText1!; - // TextStyle get bodyText2 => textTheme.bodyText2!; +// TextStyle get headline1 => textTheme.headline1!; +// TextStyle get headline2 => textTheme.headline2!; +// TextStyle get headline3 => textTheme.headline3!; +// TextStyle get headline4 => textTheme.headline4!; +// TextStyle get headline5 => textTheme.headline5!; +// TextStyle get headline6 => textTheme.headline6!; +// TextStyle get bodyText1 => textTheme.bodyText1!; +// TextStyle get bodyText2 => textTheme.bodyText2!; +} + +extension ShowBottomSheet on BuildContext { + Future showBottomSheet({isScrollControlled = true, isDismissible = false, required Widget child, Color? backgroundColor, enableDra = false, useSafeArea = false}) { + return showModalBottomSheet( + context: this, + isScrollControlled: isScrollControlled, + isDismissible: isDismissible, + enableDrag: enableDra, + useSafeArea: useSafeArea, + backgroundColor: backgroundColor ?? Colors.transparent, + builder: (_) => child, + ); + } } diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index b9d4ba8..cbfac11 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -1,3 +1,5 @@ +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/cupertino.dart'; @@ -219,12 +221,27 @@ extension EmailValidator on String { style: TextStyle(height: 23 / 24, color: color ?? AppColors.blackColor, fontSize: 24.fSize, letterSpacing: -0.4, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); + Widget toText28({Color? color, bool isBold = false, bool isCenter = false, TextScaler? textScaler}) => Text( + this, + textAlign: isCenter ? TextAlign.center : null, + textScaler: textScaler, + style: TextStyle(height: 40 / 28, color: color ?? AppColors.blackColor, fontSize: 28.fSize, letterSpacing: -1, fontWeight: isBold ? FontWeight.w600 : FontWeight.normal), + ); + Widget toText32({Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle(height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.fSize, letterSpacing: -0.4, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); + Widget toText36({Color? color, bool isBold = false, bool isCenter = false}) => Text( + this, + textAlign: isCenter ? TextAlign.center : null, + style: TextStyle(height: 47 / 36, color: color ?? AppColors.blackColor, fontSize: 36.fSize, letterSpacing: -1, fontWeight: isBold ? FontWeight.w600 : FontWeight.normal), + ); + + + Widget toText44({Color? color, bool isBold = false}) => Text( this, style: TextStyle(height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 44.fSize, letterSpacing: -0.4, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), @@ -365,3 +382,56 @@ class FontUtils { return isArabic ? 'Cairo' : 'Poppins'; } } + + +extension CountryExtension on CountryEnum { + String get displayName { + switch (this) { + case CountryEnum.saudiArabia: + return "Kingdom Of Saudi Arabia"; + case CountryEnum.unitedArabEmirates: + return "United Arab Emirates"; + } + } + + String get nameArabic { + switch (this) { + case CountryEnum.saudiArabia: + return "المملكة العربية السعودية"; + case CountryEnum.unitedArabEmirates: + return "الإمارات العربية المتحدة"; + } + } + + String get iconPath { + switch (this) { + case CountryEnum.saudiArabia: + return AppAssets.ksa; + case CountryEnum.unitedArabEmirates: + return AppAssets.uae; + } + } + + String get countryCode { + switch (this) { + case CountryEnum.saudiArabia: + return "966"; + case CountryEnum.unitedArabEmirates: + return "971"; + } + } + + static CountryEnum fromDisplayName(String name) { + switch (name) { + case "Kingdom Of Saudi Arabia": + case "المملكة العربية السعودية": + return CountryEnum.saudiArabia; + case "United Arab Emirates": + case "الإمارات العربية المتحدة": + return CountryEnum.unitedArabEmirates; + default: + throw Exception("Invalid country name"); + } + } +} + diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index 7307013..0be6eaf 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -20,8 +20,7 @@ abstract class AuthenticationRepo { required CheckPatientAuthenticationReq checkPatientAuthenticationReq, }); - Future>> sendActivationCodeRegister( - {required CheckPatientAuthenticationReq checkPatientAuthenticationReq, String? languageID}); + Future>> sendActivationCodeRegister({required CheckPatientAuthenticationReq checkPatientAuthenticationReq, String? languageID}); } class AuthenticationRepoImp implements AuthenticationRepo { @@ -114,8 +113,7 @@ class AuthenticationRepoImp implements AuthenticationRepo { } @override - Future>> sendActivationCodeRegister( - {required CheckPatientAuthenticationReq checkPatientAuthenticationReq, String? languageID}) async { + Future>> sendActivationCodeRegister({required CheckPatientAuthenticationReq checkPatientAuthenticationReq, String? languageID}) async { int isOutKsa = (checkPatientAuthenticationReq.zipCode == '966' || checkPatientAuthenticationReq.zipCode == '+966') ? 0 : 1; //TODO : We will use all these from AppState directly in the ApiClient diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index c519311..10038ba 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/request_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_repo.dart'; import 'package:hmg_patient_app_new/features/authentication/models/request_models/check_patient_authentication_request_model.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; @@ -23,6 +24,13 @@ class AuthenticationViewModel extends ChangeNotifier { final TextEditingController nationalIdController = TextEditingController(); final TextEditingController phoneNumberController = TextEditingController(); + void login() { + if (ValidationUtils.isValidatePhoneAndId(nationalId: nationalIdController.text, phoneNumber: phoneNumberController.text)) { + } else { + + } + } + Future selectDeviceImei({Function(dynamic)? onSuccess, Function(String)? onError}) async { String firebaseToken = "dOGRRszQQMGe_9wA5Hx3kO:APA91bFV5IcIJXvcCXXk0tc2ddtZgWwCPq7sGSuPr-YW7iiJpQZKgFGN9GAzCVOWL8MfheaP1slE8MdxB7lczdPBGdONQ7WbMmhgHcsUCUktq-hsapGXXqc"; final result = await authenticationRepo.selectDeviceByImei(firebaseToken: firebaseToken); @@ -52,6 +60,37 @@ class AuthenticationViewModel extends ChangeNotifier { nationIdText: '1234567890', countryCode: 'SA', ); +// Future checkUserAuthentication({Function(dynamic)? onSuccess, Function(String)? onError}) async { +// CheckPatientAuthenticationReq checkPatientAuthenticationReq = RequestUtils.getCommonRequestWelcome( +// phoneNumber: '0567184134', +// otpTypeEnum: OTPTypeEnum.sms, +// deviceToken: 'dummyDeviceToken123', +// patientOutSA: true, +// loginTokenID: 'dummyLoginToken456', +// registeredData: null, +// patientId: 12345, +// nationIdText: '1234567890', +// countryCode: 'SA', +// ); +// +// final result = await authenticationRepo.checkPatientAuthentication(checkPatientAuthenticationReq: checkPatientAuthenticationReq); +// result.fold( +// (failure) async => await errorHandlerService.handleError(failure: failure), +// (apiResponse) { +// if (apiResponse.data['isSMSSent']) { +// // TODO: set this in AppState +// // sharedPref.setString(LOGIN_TOKEN_ID, value['LogInTokenID']); +// // loginTokenID = value['LogInTokenID'], +// // sharedPref.setObject(REGISTER_DATA_FOR_LOGIIN, request), +// sendActivationCode(type); +// } else { +// if (apiResponse.data['IsAuthenticated']) { +// checkActivationCode(onWrongActivationCode: (String? message) {}); +// } +// } +// }, +// ); +// } final result = await authenticationRepo.checkPatientAuthentication(checkPatientAuthenticationReq: checkPatientAuthenticationReq); result.fold( @@ -72,4 +111,64 @@ class AuthenticationViewModel extends ChangeNotifier { ); } +// Future sendActivationCode({required OTPTypeEnum otpTypeEnum}) async { +// var request = RequestUtils.getCommonRequestAuthProvider( +// otpTypeEnum: otpTypeEnum, +// registeredData: null, +// deviceToken: "dummyLoginToken456", +// mobileNumber: "0567184134", +// zipCode: "SA", +// patientOutSA: true, +// loginTokenID: "dummyLoginToken456", +// selectedOption: selectedOption, +// patientId: 12345, +// ); +// +// request.sMSSignature = await SMSOTP.getSignature(); +// selectedOption = type; +// // GifLoaderDialogUtils.showMyDialog(context); +// if (healthId != null || isDubai) { +// if (!isDubai) { +// request.dob = dob; //isHijri == 1 ? dob : dateFormat2.format(dateFormat.parse(dob)); +// } +// request.healthId = healthId; +// request.isHijri = isHijri; +// await this.apiClient.sendActivationCodeRegister(request).then((result) { +// // GifLoaderDialogUtils.hideDialog(context); +// if (result != null && result['isSMSSent'] == true) { +// this.startSMSService(type); +// } +// }).catchError((r) { +// GifLoaderDialogUtils.hideDialog(context); +// context.showBottomSheet( +// child: ExceptionBottomSheet( +// message: r.toString(), +// onOkPressed: () { +// Navigator.of(context).pop(); +// }, +// )); +// // AppToast.showErrorToast(message: r); +// }); +// } else { +// request.dob = ""; +// request.healthId = ""; +// request.isHijri = 0; +// await this.authService.sendActivationCode(request).then((result) { +// GifLoaderDialogUtils.hideDialog(context); +// if (result != null && result['isSMSSent'] == true) { +// this.startSMSService(type); +// } +// }).catchError((r) { +// GifLoaderDialogUtils.hideDialog(context); +// context.showBottomSheet( +// child: ExceptionBottomSheet( +// message: r.toString(), +// onOkPressed: () { +// Navigator.of(context).pop(); +// }, +// )); +// // AppToast.showErrorToast(message: r.toString()); +// }); +// } +// } } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 6ef6804..c5f2592 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -774,7 +774,20 @@ abstract class LocaleKeys { static const validPassportNumber = 'validPassportNumber'; static const continuePlan = 'continuePlan'; static const aboutApp = 'aboutApp'; - static const dontHaveAccount = 'dontHaveAccount'; static const loginOrRegister = 'loginOrRegister'; + static const dontHaveAccount = 'dontHaveAccount'; + static const receiveOtpToast = 'receiveOtpToast'; + static const enterPhoneNumber = 'enterPhoneNumber'; + static const enterEmailDesc = 'enterEmailDesc'; + static const enterPhoneDesc = 'enterPhoneDesc'; + static const pleaseChooseOption = 'pleaseChooseOption'; + static const prepareToElevate = 'prepareToElevate'; + static const iAcceptTermsConditions = 'iAcceptTermsConditions'; + static const alreadyHaveAccount = 'alreadyHaveAccount'; + static const loginNow = 'loginNow'; + static const notice = 'notice'; + static const oR = 'oR'; + static const sendOTPWHATSAPP = 'sendOTPWHATSAPP'; + static const sendOTPSMS = 'sendOTPSMS'; } diff --git a/lib/presentation/authentication/login.dart b/lib/presentation/authentication/login.dart index fb88ba1..2771b36 100644 --- a/lib/presentation/authentication/login.dart +++ b/lib/presentation/authentication/login.dart @@ -1,17 +1,21 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/context_extensions.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'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/authentication/register.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'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; -import 'package:sizer/sizer.dart'; // Import sizer +import 'package:provider/provider.dart'; class LoginScreen extends StatefulWidget { @override @@ -31,99 +35,165 @@ class _LoginScreen extends State { @override Widget build(BuildContext context) { - return Sizer(// Wrap with Sizer - builder: (context, orientation, deviceType) { - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - appBar: CustomAppBar( - onBackPressed: () { - }, - onLanguageChanged: (String value) { - print(value); - context.setLocale(value == 'en' ? Locale('ar', 'SA') : Locale('en', 'US')); - }, + AuthenticationViewModel authVm = context.read(); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + appBar: CustomAppBar( + onBackPressed: () { + Navigator.of(context).pop(); + }, + onLanguageChanged: (String value) { + // context.setLocale(value == 'en' ? Locale('ar', 'SA') : Locale('en', 'US')); + }, + ), + body: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); // Dismiss the keyboard when tapping outside + }, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.showLottie(context: context, assetPath: AppAnimations.login, width: 200.h, height: 200.h, repeat: true, fit: BoxFit.cover), + SizedBox(height: 130.h), // Adjusted to sizer unit + LocaleKeys.welcomeToDrSulaiman.tr().toText32(isBold: true, color: AppColors.textColor), + SizedBox(height: 32.h), + TextInputWidget( + labelText: "${LocaleKeys.nationalId.tr()} / ${LocaleKeys.fileNo.tr()}", + hintText: "xxxxxxxxx", + controller: authVm.nationalIdController, + keyboardType: TextInputType.number, + isEnable: true, + prefix: null, + autoFocus: true, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 10.h), + leadingIcon: AppAssets.student_card, + errorMessage: "Please enter a valid national ID or file number", + hasError: true, + ), + SizedBox(height: 16.h), // Adjusted to sizer unit (approx 16px) + CustomButton( + text: LocaleKeys.login.tr(), + icon: AppAssets.login1, + iconColor: Colors.white, + onPressed: () { + showLoginModel(context: context, textController: authVm.phoneNumberController); + // if (nationIdController.text.isNotEmpty) { + + // } else { + // showBottomSheet( + // child: ExceptionBottomSheet( + // message: TranslationBase.of(context).pleaseEnterNationalIdOrFileNo, + // showCancel: false, + // onOkPressed: () { + // Navigator.of(context).pop(); + // }, + // ), + // ); + // } + }, + ), + SizedBox(height: 10.h), // Adjusted to sizer unit (approx 14px) + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: context.dynamicTextStyle( + color: Colors.black, + fontSize: 14.fSize, // Adjusted to sizer unit + height: 26 / 16, // This height is a ratio, may need re-evaluation + fontWeight: FontWeight.w500, + ), + children: [ + TextSpan(text: LocaleKeys.dontHaveAccount.tr(), style: context.dynamicTextStyle()), + TextSpan(text: " "), + TextSpan( + text: LocaleKeys.registernow.tr(), + style: context.dynamicTextStyle( + color: AppColors.primaryRedColor, + fontSize: 14.fSize, // Adjusted to sizer unit + height: 26 / 16, // Ratio + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + Navigator.of(context).push( + MaterialPageRoute(builder: (BuildContext context) => RegisterNew()), + ); + }, + ), + ], + ), + ).withVerticalPadding(2), // Adjusted to sizer unit + ), + ], + ), + ), ), - body: GestureDetector( - onTap: () { - FocusScope.of(context).unfocus(); // Dismiss the keyboard when tapping outside - }, - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.only(left: 6.w, right: 6.w), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Utils.showLottie(context: context, assetPath: AppAnimations.login, width: 45.w, height: 22.h, repeat: true, fit: BoxFit.cover), - SizedBox(height: 19.h), // Adjusted to sizer unit - LocaleKeys.welcomeToDrSulaiman.tr().toText22(isBold: true, color: AppColors.textColor), - // Text( - // LocaleKeys.welcomeToDrSulaiman.tr(), - // style: context.dynamicTextStyle( - // fontSize: 22, - // fontWeight: FontWeight.w600, - // color: AppColors.textColor, - // letterSpacing: -0.4, - // height: 40 / 28, - // ), - // ), - SizedBox(height: 4.h), // Adjusted to sizer unit (approx 32px) - TextInputWidget( - labelText: "${LocaleKeys.nationalId.tr()} / ${LocaleKeys.fileNo.tr()}", - hintText: "xxxxxxxxx", - controller: TextEditingController(), - keyboardType: TextInputType.number, - isEnable: true, - prefix: null, - autoFocus: true, - isBorderAllowed: false, - isAllowLeadingIcon: true, - padding: EdgeInsets.symmetric(vertical: 1.h, horizontal: 2.w), - leadingIcon: AppAssets.student_card, - errorMessage: "Please enter a valid national ID or file number", - hasError: true, - ), - SizedBox(height: 2.h), // Adjusted to sizer unit (approx 16px) - CustomButton( - text: LocaleKeys.login.tr(), - icon: AppAssets.login1, - iconColor: Colors.white, - onPressed: () {}, + ), + ); + } + + void showLoginModel({required BuildContext context, TextEditingController? textController}) { + context.showBottomSheet( + isScrollControlled: true, + isDismissible: false, + useSafeArea: true, + backgroundColor: Colors.transparent, + child: StatefulBuilder(builder: (BuildContext context, StateSetter setModalState) { + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: SingleChildScrollView( + child: GenericBottomSheet( + countryCode: "966", + initialPhoneNumber: "", + textController: textController, + isEnableCountryDropdown: true, + onCountryChange: (value) {}, + onChange: (String? value) {}, + buttons: [ + Padding( + padding: EdgeInsets.only(bottom: 10.h), + child: CustomButton( + text: LocaleKeys.sendOTPSMS.tr(), + onPressed: () {}, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedBorderColor, + textColor: AppColors.whiteColor, + icon: AppAssets.message, + ), ), - SizedBox(height: 1.8.h), // Adjusted to sizer unit (approx 14px) - Center( - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: context.dynamicTextStyle( - color: Colors.black, - fontSize: 14.sp, // Adjusted to sizer unit - height: 26 / 16, // This height is a ratio, may need re-evaluation - fontWeight: FontWeight.w500, - ), - children: [ - TextSpan(text: LocaleKeys.dontHaveAccount.tr(), style: context.dynamicTextStyle()), - TextSpan(text: " "), - TextSpan( - text: LocaleKeys.registernow.tr(), - style: context.dynamicTextStyle( - color: AppColors.primaryRedColor, - fontSize: 14.sp, // Adjusted to sizer unit - height: 26 / 16, // Ratio - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ], + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 8.h), + child: LocaleKeys.oR.tr().toText16(color: AppColors.textColor), ), - ).withVerticalPadding(2.h), // Adjusted to sizer unit + ], + ), + Padding( + padding: EdgeInsets.only(bottom: 10.h, top: 10.h), + child: CustomButton( + text: LocaleKeys.sendOTPWHATSAPP.tr(), + onPressed: () {}, + backgroundColor: Colors.white, + borderColor: AppColors.borderOnlyColor, + textColor: AppColors.textColor, + icon: AppAssets.whatsapp, + ), ), ], ), ), - ), - ), - ); - }); + ); + })); } } diff --git a/lib/presentation/authentication/register.dart b/lib/presentation/authentication/register.dart new file mode 100644 index 0000000..b445631 --- /dev/null +++ b/lib/presentation/authentication/register.dart @@ -0,0 +1,315 @@ +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'; +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/generated/locale_keys.g.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'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart' show CustomButton; +import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart'; +import 'package:hmg_patient_app_new/widgets/dropdown/dropdown_widget.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/otp/otp.dart'; + +class RegisterNew extends StatefulWidget { + @override + _RegisterNew createState() => _RegisterNew(); +} + +class _RegisterNew extends State { + bool isTermsAccepted = true; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + AppState appState = getIt.get(); + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + appBar: CustomAppBar( + onBackPressed: () { + Navigator.of(context).pop(); + }, + onLanguageChanged: (String value) { + // context.setLocale(value == 'en' ? Locale('ar', 'SA') : Locale('en', 'US')); + }, + ), + body: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(overscroll: false, physics: const ClampingScrollPhysics()), + child: NotificationListener( + onNotification: (notification) { + notification.disallowIndicator(); + return true; + }, + child: SingleChildScrollView( + physics: ClampingScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.showLottie(context: context, + assetPath: 'assets/animations/lottie/register.json', + width: 200.h, + height: 200.h, + fit: BoxFit.cover, + repeat: true), + SizedBox(height: 16.h), + LocaleKeys.prepareToElevate.tr().toText32(isBold: true), + SizedBox(height: 24.h), + Directionality( + textDirection: Directionality.of(context), + child: Container( + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(24)), + padding: EdgeInsets.symmetric(horizontal: 16.h), + child: Column( + children: [ + CustomCountryDropdown( + countryList: CountryEnum.values, + onCountryChange: (CountryEnum? value) {}, + isRtl: Directionality.of(context) == TextDirection.LTR, + ).withVerticalPadding(8.h), + Divider(height: 1.h), + TextInputWidget( + labelText: LocaleKeys.nationalIdNumber.tr(), + hintText: "xxxxxxxxx", + controller: TextEditingController(), + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + autoFocus: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.student_card, + onChange: (value) { + print(value); + }).withVerticalPadding(8), + Divider(height: 1), + TextInputWidget( + labelText: LocaleKeys.dob.tr(), + hintText: "11 July, 1994", + controller: TextEditingController(), + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 8.h), + leadingIcon: AppAssets.birthday_cake, + onChange: (value) {}, + ).withVerticalPadding(8), + ], + ), + ), + ), + SizedBox(height: 25.h), + GestureDetector( + onTap: () {}, + child: Row( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 24.h, + width: 24.h, + decoration: BoxDecoration( + color: isTermsAccepted ? const Color(0xFFE92227) : Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: isTermsAccepted ? const Color(0xFFE92227) : Colors.grey, + width: 2.h, + ), + ), + child: isTermsAccepted ? Icon(Icons.check, size: 16.fSize, color: Colors.white) : null, + ), + SizedBox(width: 12.h), + Expanded( + child: Text( + LocaleKeys.iAcceptTermsConditions.tr(), + style: context.dynamicTextStyle(fontSize: 14.fSize, fontWeight: FontWeight.w500, color: Color(0xFF2E3039)), + ), + ), + ], + ), + ), + SizedBox(height: 25.h), + CustomButton( + text: "Register", + icon: AppAssets.note_edit, + onPressed: () { + showRegisterModel(context: context); + }, + ), + SizedBox(height: 14), + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: context.dynamicTextStyle( + color: Colors.black, + fontSize: 16.fSize, + height: 26 / 16, + fontWeight: FontWeight.w500, + ), + children: [ + TextSpan(text: LocaleKeys.alreadyHaveAccount.tr(), style: context.dynamicTextStyle()), + TextSpan(text: " "), + TextSpan( + text: LocaleKeys.loginNow.tr(), + style: context.dynamicTextStyle( + color: AppColors.primaryRedColor, + fontSize: 16.fSize, + height: 26 / 16, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ), + ), + SizedBox(height: 30), + ], + ), + ), + ), + ), + )); + } + + void showRegisterModel({required BuildContext context, TextEditingController? textController}) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + useSafeArea: true, + backgroundColor: Colors.transparent, + builder: (bottomSheetContext) => + Padding( + padding: EdgeInsets.only(bottom: MediaQuery + .of(bottomSheetContext) + .viewInsets + .bottom), + child: SingleChildScrollView( + child: GenericBottomSheet( + countryCode: "966", + initialPhoneNumber: "", + textController: TextEditingController(), + onChange: (String? value) {}, + buttons: [ + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: CustomButton( + text: LocaleKeys.sendOTPSMS.tr(), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (BuildContext context) => OTPVerificationPage(phoneNumber: '12234567',))); + + + // if (mobileNo.isEmpty) { + // context.showBottomSheet( + // child: ExceptionBottomSheet( + // message: TranslationBase.of(context).pleaseEnterMobile, + // showCancel: false, + // onOkPressed: () { + // Navigator.of(context).pop(); + // }, + // ), + // ); + // } else if (!Utils.validateMobileNumber(mobileNo)) { + // context.showBottomSheet( + // child: ExceptionBottomSheet( + // message: TranslationBase.of(context).pleaseEnterValidMobile, + // showCancel: false, + // onOkPressed: () { + // Navigator.of(context).pop(); + // }, + // ), + // ); + // } else { + // registerUser(1); + // } + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedBorderColor, + textColor: AppColors.whiteColor, + icon: AppAssets.message, + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 8.h), + child: LocaleKeys.oR.tr().toText16(color: AppColors.textColor), + ), + ], + ), + Padding( + padding: EdgeInsets.only(bottom: 10.h, top: 10.h), + child: CustomButton( + text: LocaleKeys.sendOTPWHATSAPP.tr(), + onPressed: () { + // if (mobileNo.isEmpty) { + // context.showBottomSheet( + // child: ExceptionBottomSheet( + // message: TranslationBase.of(context).pleaseEnterMobile, + // showCancel: false, + // onOkPressed: () { + // Navigator.of(context).pop(); + // }, + // ), + // ); + // } else if (!Utils.validateMobileNumber(mobileNo)) { + // context.showBottomSheet( + // child: ExceptionBottomSheet( + // message: TranslationBase.of(context).pleaseEnterValidMobile, + // showCancel: false, + // onOkPressed: () { + // Navigator.of(context).pop(); + // }, + // ), + // ); + // } else { + // registerUser(4); + // } + // int? val = Utils.onOtpBtnPressed(OTPType.whatsapp, mobileNo, context); + // registerUser(val); + }, + backgroundColor: AppColors.whiteColor, + borderColor: AppColors.borderOnlyColor, + textColor: AppColors.textColor, + icon: AppAssets.whatsapp, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 8421856..f2017b2 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -11,6 +11,7 @@ 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'; 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/data/landing_page_data.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/habib_wallet_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; @@ -50,7 +51,9 @@ class _LandingPageState extends State { CustomButton( text: LocaleKeys.loginOrRegister.tr(context: context), onPressed: () async { - await authenticationViewModel.selectDeviceImei(); + + // await authenticationViewModel.selectDeviceImei(); + Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => LoginScreen())); }, backgroundColor: Color(0xffFEE9EA), borderColor: Color(0xffFEE9EA), diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index f4ba507..1e5143d 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -40,7 +40,6 @@ class MedicalFilePage extends StatelessWidget { isAllowLeadingIcon: true, padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), leadingIcon: AppAssets.student_card, - hasError: true, ), SizedBox(height: 16.h), Container( diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index a6b3362..3aefebf 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -9,7 +9,6 @@ class AppColors { static const buttonColor = Color(0xFF6A46F5); static const splashBgColor = Color(0xFF3C355D); - static const blackColor = Color(0xFF000000); static const lightGray = Color(0xFFF4F5F7); static const lightPurple = Color(0xFFB7A3E6); static const scaffoldBgColor = Color(0xFFF8F8F8); @@ -31,19 +30,22 @@ class AppColors { static const Color textColor = Color(0xFF2E3039); static const Color borderOnlyColor = Color(0xFF2E3039); static const Color dividerColor = Color(0xFFD2D2D2); + static const Color blackBgColor = Color(0xFF2E3039); + static const blackColor = textColor; + static const inputLabelTextColor = Color(0xff898A8D); //Chips -static const Color successColor = Color(0xff18C273); -static const Color errorColor = Color(0xFFED1C2B); -static const Color alertColor = Color(0xFFD48D05); -static const Color infoColor = Color(0xFF0B85F7); -static const Color warningColor = Color(0xFFFFCC00); -static const Color greyColor = Color(0xFFEFEFF0); + static const Color successColor = Color(0xff18C273); + static const Color errorColor = Color(0xFFED1C2B); + static const Color alertColor = Color(0xFFD48D05); + static const Color infoColor = Color(0xFF0B85F7); + static const Color warningColor = Color(0xFFFFCC00); + static const Color greyColor = Color(0xFFEFEFF0); -static const Color successLightColor = Color(0xFF18C27326); -static const Color errorLightColor = Color(0xFFED1C2B1A); -static const Color alertLightColor = Color(0xFFD48D0526); -static const Color infoLightColor = Color(0xFF0B85F726); -static const Color warningLightColor = Color(0xFFFFCC0026); -static const Color greyLightColor = Color(0xFFEFEFF026); + static const Color successLightColor = Color(0xFF18C27326); + static const Color errorLightColor = Color(0xFFED1C2B1A); + static const Color alertLightColor = Color(0xFFD48D0526); + static const Color infoLightColor = Color(0xFF0B85F726); + static const Color warningLightColor = Color(0xFFFFCC0026); + static const Color greyLightColor = Color(0xFFEFEFF026); } diff --git a/lib/widgets/appbar/app_bar_widget.dart b/lib/widgets/appbar/app_bar_widget.dart index dde02ed..4bc08a9 100644 --- a/lib/widgets/appbar/app_bar_widget.dart +++ b/lib/widgets/appbar/app_bar_widget.dart @@ -1,7 +1,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/widgets/language_switcher.dart'; import '../../generated/locale_keys.g.dart'; @@ -9,11 +11,13 @@ import '../../generated/locale_keys.g.dart'; class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { final VoidCallback onBackPressed; final ValueChanged onLanguageChanged; + bool hideLogoAndLang; - const CustomAppBar({ + CustomAppBar({ Key? key, required this.onBackPressed, required this.onLanguageChanged, + this.hideLogoAndLang = false, }) : super(key: key); @override @@ -24,45 +28,45 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { return AppBar( backgroundColor: Colors.transparent, leading: null, - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Arrow Back with click handler - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: GestureDetector( - onTap: onBackPressed, - child: Utils.buildSvgWithAssets( - icon: AppAssets.arrow_back, - width: 32, - height: 32, + automaticallyImplyLeading: false, + title: Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: onBackPressed, + child: Utils.buildSvgWithAssets(icon: AppAssets.arrow_back, width: 32.h, height: 32.h), ), ), ), - ), - // Logo - Utils.buildSvgWithAssets( - icon: AppAssets.habiblogo, - ), + // Logo + if (!hideLogoAndLang) + Utils.buildSvgWithAssets( + icon: AppAssets.habiblogo, + ), - // Language Selector - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: LanguageSelector( - currentLanguage: context.locale.languageCode, - showOnlyIcon: false, - onLanguageChanged: onLanguageChanged, - languages: [ - {'code': 'ar', 'name': LocaleKeys.arabic.tr()}, - {'code': 'en', 'name': LocaleKeys.english.tr()} - ], + if (!hideLogoAndLang) + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: LanguageSelector( + currentLanguage: context.locale.languageCode, + showOnlyIcon: false, + onLanguageChanged: onLanguageChanged, + languages: [ + {'code': 'ar', 'name': LocaleKeys.arabic.tr()}, + {'code': 'en', 'name': LocaleKeys.english.tr()} + ], + ), + ), ), - ), - ), - ], + ], + ), ), centerTitle: true, ); diff --git a/lib/widgets/bottomsheet/exception_bottom_sheet.dart b/lib/widgets/bottomsheet/exception_bottom_sheet.dart new file mode 100644 index 0000000..f0ea651 --- /dev/null +++ b/lib/widgets/bottomsheet/exception_bottom_sheet.dart @@ -0,0 +1,111 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; + +class ExceptionBottomSheet extends StatelessWidget { + String message; + bool showOKButton; + bool showCancel; + Function() onOkPressed; + Function()? onCancelPressed; + + ExceptionBottomSheet({Key? key, required this.message, this.showOKButton = true, this.showCancel = false, required this.onOkPressed, this.onCancelPressed}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SafeArea( + bottom: Platform.isIOS ? false : true, // Adjust for iOS to avoid bottom padding + child: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); // Dismiss the keyboard when tapping outside + }, + child: Builder(builder: (context) { + return Directionality( + textDirection: Directionality.of(context), + child: Container( + padding: EdgeInsets.all(24.h), + decoration: BoxDecoration( + color: Color(0xFFF8F8FA), + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.notice.tr().toText28(), + InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Utils.buildSvgWithAssets(icon: AppAssets.cross_circle), + ) + ], + ), + SizedBox(height: 10.h), + (message ?? "").toText16(isBold: false, color: AppColors.textColor), + SizedBox(height: 10.h), + SizedBox(height: 24.h), + if (showOKButton && showCancel) + Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.cancel.tr(), + onPressed: onCancelPressed != null + ? onCancelPressed! + : () { + Navigator.of(context).pop(); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + icon: AppAssets.cancel, + iconColor: AppColors.primaryRedColor, + ), + ), + SizedBox(width: 5.h), + Expanded( + child: CustomButton( + text: showCancel ? LocaleKeys.confirm.tr() : LocaleKeys.ok.tr(), + onPressed: onOkPressed, + backgroundColor: AppColors.bgGreenColor, + borderColor: AppColors.bgGreenColor, + textColor: Colors.white, + icon: AppAssets.confirm, + ), + ), + ], + ), + if (showOKButton && !showCancel) + Padding( + padding: EdgeInsets.only(bottom: 10.h), + child: CustomButton( + text: LocaleKeys.ok.tr(), + onPressed: onOkPressed, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedBorderColor, + textColor: Colors.white, + icon: AppAssets.confirm, + ), + ), + ], + ), + ), + ); + }), + ), + ); + } +} diff --git a/lib/widgets/bottomsheet/generic_bottom_sheet.dart b/lib/widgets/bottomsheet/generic_bottom_sheet.dart new file mode 100644 index 0000000..1d07099 --- /dev/null +++ b/lib/widgets/bottomsheet/generic_bottom_sheet.dart @@ -0,0 +1,145 @@ +import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.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'; +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/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; + +class GenericBottomSheet extends StatefulWidget { + String? countryCode; + String? initialPhoneNumber; + final List buttons; + TextEditingController? textController; + final bool isForEmail; + Function(CountryEnum)? onCountryChange; + final bool isEnableCountryDropdown; + final bool isFromSavedLogin; + Function(String?)? onChange; + + // FocusNode myFocusNode; + + GenericBottomSheet({ + this.countryCode = "", + this.initialPhoneNumber = "", + required this.buttons, + this.textController, + this.isForEmail = false, + this.onCountryChange, + this.isEnableCountryDropdown = false, + this.isFromSavedLogin = false, + this.onChange, + // required this.myFocusNode + }); + + @override + _GenericBottomSheetState createState() => _GenericBottomSheetState(); +} + +class _GenericBottomSheetState extends State { + @override + void initState() { + super.initState(); + + if (!widget.isForEmail) { + widget.textController = TextEditingController(text: widget.initialPhoneNumber); + } + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + bottom: Platform.isIOS ? false : true, + child: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Directionality( + textDirection: Directionality.of(context), + child: Container( + padding: EdgeInsets.all(24.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.bgScaffoldColor, borderRadius: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Title + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: widget.isFromSavedLogin + ? LocaleKeys.receiveOtpToast.tr().toText24() + : widget.isForEmail + ? LocaleKeys.enterEmail.tr().toText24() + : LocaleKeys.enterPhoneNumber.tr().toText24()), + InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Padding( + padding: EdgeInsets.only(top: 10.h), + child: Utils.buildSvgWithAssets(icon: AppAssets.cross_circle), + ), + ), + ], + ), + SizedBox(height: 8.h), + // Subtitle + widget.isFromSavedLogin + ? LocaleKeys.pleaseChooseOption.tr().toText16() + : widget.isForEmail + ? LocaleKeys.enterEmailDesc.tr().toText16() + : LocaleKeys.enterPhoneDesc.tr().toText16(), + + if (widget.isFromSavedLogin) + ...[] + else ...[ + widget.textController != null + ? TextInputWidget( + labelText: widget.isForEmail ? LocaleKeys.email : LocaleKeys.phoneNumber, + hintText: widget.isForEmail ? "demo@gmail.com" : "5xxxxxxxx", + controller: widget.textController!, + padding: EdgeInsets.all(8.h), + keyboardType: widget.isForEmail ? TextInputType.emailAddress : TextInputType.number, + onChange: (value) { + widget.textController!.text = value!; + if (widget.onChange != null) { + widget.onChange!(value); + } + }, + isEnable: true, + // focusNode: widget.myFocusNode, + isReadOnly: widget.isFromSavedLogin, + prefix: widget.isForEmail ? null : widget.countryCode, + isBorderAllowed: false, + isAllowLeadingIcon: true, + isCountryDropDown: widget.isEnableCountryDropdown, + leadingIcon: widget.isForEmail ? AppAssets.email : AppAssets.smart_phone, + ) + : SizedBox(), + ], + + SizedBox(height: 24.h), + ...widget.buttons, + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/dropdown/country_dropdown_widget.dart b/lib/widgets/dropdown/country_dropdown_widget.dart new file mode 100644 index 0000000..5e4bddf --- /dev/null +++ b/lib/widgets/dropdown/country_dropdown_widget.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.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'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; + +class CustomCountryDropdown extends StatefulWidget { + final List countryList; + final Function(CountryEnum)? onCountryChange; + final bool isRtl; + + const CustomCountryDropdown({ + Key? key, + required this.countryList, + this.onCountryChange, + required this.isRtl, + }) : super(key: key); + + @override + _CustomCountryDropdownState createState() => _CustomCountryDropdownState(); +} + +class _CustomCountryDropdownState extends State { + CountryEnum? selectedCountry; + late OverlayEntry _overlayEntry; + bool _isDropdownOpen = false; + + @override + void initState() { + super.initState(); + selectedCountry = CountryEnum.saudiArabia; + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (_isDropdownOpen) { + _closeDropdown(); + } else { + _openDropdown(); + } + }, + child: Container( + height: 40.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 10.h), + 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), + Text( + selectedCountry != null ? selectedCountry!.displayName : "Select Country", + style: TextStyle( + fontSize: 14.fSize, + height: 21 / 14, + fontWeight: FontWeight.w500, + letterSpacing: -0.2, + ), + ), + ], + ), + ), + ); + } + + void _openDropdown() { + RenderBox renderBox = context.findRenderObject() as RenderBox; + Offset offset = renderBox.localToGlobal(Offset.zero); + + _overlayEntry = OverlayEntry( + builder: (context) => Stack( + children: [ + // Dismiss dropdown when tapping outside + Positioned.fill( + child: GestureDetector( + onTap: _closeDropdown, + behavior: HitTestBehavior.translucent, + child: Container(), + ), + ), + Positioned( + top: offset.dy + renderBox.size.height, + left: widget.isRtl ? offset.dx + 15.h : offset.dx - 15.h, + width: renderBox.size.width, + child: Material( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: Colors.white, + borderRadius: 12, + ), + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(12), + // boxShadow: [ + // BoxShadow( + // color: Color(0xFFF8F8FA), + // blurRadius: 8.h, + // offset: Offset( + // 0, + // 2, + // ), + // ), + // ], + // ), + child: Column( + children: widget.countryList + .map( + (country) => GestureDetector( + onTap: () { + setState(() { + selectedCountry = country; + }); + widget.onCountryChange?.call(country); + _closeDropdown(); + }, + child: Container( + padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 16.h, + ), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: country.iconPath, + width: 38.h, + height: 38.h, + ), + SizedBox(width: 12.h), + Text(country.displayName, + style: TextStyle( + fontSize: 14.fSize, + height: 21 / 14, + fontWeight: FontWeight.w500, + letterSpacing: -0.2, + )), + ], + ), + ), + ), + ) + .toList(), + ), + ), + ), + ), + ], + ), + ); + + Overlay.of(context)?.insert(_overlayEntry); + setState(() { + _isDropdownOpen = true; + }); + } + + void _closeDropdown() { + _overlayEntry.remove(); + setState(() { + _isDropdownOpen = false; + }); + } +} diff --git a/lib/widgets/dropdown_widget.dart b/lib/widgets/dropdown/dropdown_widget.dart similarity index 56% rename from lib/widgets/dropdown_widget.dart rename to lib/widgets/dropdown/dropdown_widget.dart index a778f2e..268e44a 100644 --- a/lib/widgets/dropdown_widget.dart +++ b/lib/widgets/dropdown/dropdown_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart' show Icons, PopupMenuItem, showMenu, Colors; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; @@ -38,10 +39,8 @@ class DropdownWidget extends StatelessWidget { alignment: Alignment.center, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: Colors.white, - borderRadius: isAllowRadius ? 15 : null, - side: isBorderAllowed - ? BorderSide(color: const Color(0xffefefef), width: 1) - : null, + borderRadius: isAllowRadius ? 15.h : null, + side: isBorderAllowed ? BorderSide(color: const Color(0xffefefef), width: 1) : null, ), child: Column( mainAxisSize: MainAxisSize.min, @@ -57,8 +56,8 @@ class DropdownWidget extends StatelessWidget { Widget _buildLabelText() { return Text( labelText, - style: const TextStyle( - fontSize: 12, + style: TextStyle( + fontSize: 12.fSize, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -0.2, @@ -71,33 +70,41 @@ class DropdownWidget extends StatelessWidget { return GestureDetector( onTap: isEnable ? () async { - final renderBox = context.findRenderObject() as RenderBox; - final offset = renderBox.localToGlobal(Offset.zero); - final selected = await showMenu( - context: context, - position: RelativeRect.fromLTRB( - offset.dx, - offset.dy + renderBox.size.height, - offset.dx + renderBox.size.width, - 0, - ), - items: dropdownItems - .map( - (e) => PopupMenuItem( - value: e, - child: Text(e), - ), - ) - .toList(), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ); + final renderBox = context.findRenderObject() as RenderBox; + final offset = renderBox.localToGlobal(Offset.zero); + final selected = await showMenu( + context: context, + position: RelativeRect.fromLTRB( + offset.dx, + offset.dy + renderBox.size.height, + offset.dx + renderBox.size.width, + 0, + ), + items: dropdownItems + .map( + (e) => PopupMenuItem( + value: e, + child: Text( + e, + style: TextStyle( + fontSize: 14.fSize, + height: 21 / 14, + fontWeight: FontWeight.w500, + letterSpacing: -0.2, + ), + ), + ), + ) + .toList(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ); - if (selected != null && onChange != null) { - onChange!(selected); - } - } + if (selected != null && onChange != null) { + onChange!(selected); + } + } : null, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -105,28 +112,21 @@ class DropdownWidget extends StatelessWidget { children: [ Expanded( child: Text( - (selectedValue == null || selectedValue!.isEmpty) - ? hintText - : selectedValue!, + (selectedValue == null || selectedValue!.isEmpty) ? hintText : selectedValue!, textAlign: TextAlign.left, textDirection: TextDirection.ltr, style: TextStyle( - fontSize: 14, + fontSize: 14.fSize, height: 21 / 14, fontWeight: FontWeight.w500, - color: (selectedValue != null && selectedValue!.isNotEmpty) - ? const Color(0xff2E3039) - : const Color(0xffB0B0B0), + color: (selectedValue != null && selectedValue!.isNotEmpty) ? const Color(0xff2E3039) : const Color(0xffB0B0B0), letterSpacing: -0.2, ), ), ), - if (hasSelectionCustomIcon && selectionCustomIcon != null) - Utils.buildSvgWithAssets(icon: selectionCustomIcon!) - else - const Icon(Icons.keyboard_arrow_down_outlined), + if (hasSelectionCustomIcon && selectionCustomIcon != null) Utils.buildSvgWithAssets(icon: selectionCustomIcon!) else const Icon(Icons.keyboard_arrow_down_outlined), ], ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index f2446a7..ca72fe8 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; // TODO: Import AppColors if bgRedColor is defined there // import 'package:hmg_patient_app_new/core/ui_utils/app_colors.dart'; @@ -54,9 +56,7 @@ class TextInputWidget extends StatelessWidget { @override Widget build(BuildContext context) { - // Assuming AppColors.bgRedColor exists, otherwise using Colors.red - final errorColor = Colors.red; // Replace with AppColors.bgRedColor if available - + final errorColor = AppColors.primaryRedColor; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -66,7 +66,7 @@ class TextInputWidget extends StatelessWidget { alignment: Alignment.center, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: Colors.white, - borderRadius: isAllowRadius ? 15 : null, + borderRadius: isAllowRadius ? 12 : null, side: isBorderAllowed ? BorderSide(color: hasError ? errorColor : const Color(0xffefefef), width: 1) : null, ), child: Row( @@ -88,12 +88,12 @@ class TextInputWidget extends StatelessWidget { ), if (hasError && errorMessage != null) Padding( - padding: const EdgeInsets.only(top: 4.0, left: 12.0), // Adjust padding as needed + padding: EdgeInsets.only(top: 4.h, left: 12.h), // Adjust padding as needed child: Text( errorMessage!, style: TextStyle( color: errorColor, - fontSize: 12, + fontSize: 12.fSize, ), ), ), @@ -103,21 +103,24 @@ class TextInputWidget extends StatelessWidget { Widget _buildLeadingIcon(BuildContext context) { return Container( - height: 40, - width: 40, - margin: const EdgeInsets.only(right: 10), - padding: const EdgeInsets.all(8), - decoration: const BoxDecoration(color: Color(0xFFEFEFF0), borderRadius: BorderRadius.all(Radius.circular(10))), + height: 40.h, + width: 40.h, + margin: EdgeInsets.only(right: 10.h), + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 10.h, + color: AppColors.greyColor, + ), child: Utils.buildSvgWithAssets(icon: leadingIcon!)); } Widget _buildLabelText() { return Text( labelText, - style: const TextStyle( - fontSize: 12, + style: TextStyle( + fontSize: 12.fSize, fontWeight: FontWeight.w500, - color: Color(0xff898A8D), + color: AppColors.inputLabelTextColor, letterSpacing: -0.2, height: 18 / 12, ), @@ -137,30 +140,30 @@ class TextInputWidget extends StatelessWidget { onChanged: onChange, focusNode: focusNode, autofocus: autoFocus, - style: const TextStyle( - fontSize: 14, + style: TextStyle( + fontSize: 14.fSize, height: 21 / 14, fontWeight: FontWeight.w500, - color: Color(0xff2E3039), + color: AppColors.textColor, letterSpacing: -0.2, ), decoration: InputDecoration( isDense: true, hintText: hintText, - hintStyle: const TextStyle( - fontSize: 14, + hintStyle: TextStyle( + fontSize: 14.fSize, height: 21 / 16, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -0.2, ), - prefixIconConstraints: const BoxConstraints(minWidth: 45), + prefixIconConstraints: BoxConstraints(minWidth: 45.h), prefixIcon: prefix == null ? null : Text( "+" + prefix!, - style: const TextStyle( - fontSize: 14, + style: TextStyle( + fontSize: 14.fSize, height: 21 / 14, fontWeight: FontWeight.w500, color: Color(0xff2E303A), diff --git a/lib/widgets/language_switcher.dart b/lib/widgets/language_switcher.dart index 4c621b7..4a81217 100644 --- a/lib/widgets/language_switcher.dart +++ b/lib/widgets/language_switcher.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -51,18 +52,18 @@ class _LanguageSelectorState extends State { widget.onLanguageChanged(newLanguage); }, child: Container( - padding: EdgeInsets.all(8), + padding: EdgeInsets.all(8.h), decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)), child: Row( mainAxisSize: MainAxisSize.min, children: [ Utils.buildSvgWithAssets(icon: AppAssets.language), - const SizedBox(width: 6), + SizedBox(width: 6.h), Text( currentLangData['name']?.toUpperCase() ?? 'EN', style: context.dynamicTextStyle( fontWeight: FontWeight.w500, - fontSize: 14, + fontSize: 14.fSize, color: AppColors.primaryRedColor, letterSpacing: 0.1, isLanguageSwitcher: true, diff --git a/lib/widgets/otp/otp.dart b/lib/widgets/otp/otp.dart new file mode 100644 index 0000000..5cfd3e9 --- /dev/null +++ b/lib/widgets/otp/otp.dart @@ -0,0 +1,227 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart'; + +class OTPVerificationPage extends StatefulWidget { + final String phoneNumber; + + const OTPVerificationPage({Key? key, required this.phoneNumber}) : super(key: key); + + @override + State createState() => _OTPVerificationPageState(); +} + +class _OTPVerificationPageState extends State { + final int _otpLength = 4; + late final List _controllers; + late final List _focusNodes; + + Timer? _resendTimer; + int _resendTime = 60; + bool _isOtpComplete = false; + + @override + void initState() { + super.initState(); + _controllers = List.generate(_otpLength, (_) => TextEditingController()); + _focusNodes = List.generate(_otpLength, (_) => FocusNode()); + _startResendTimer(); + + // Focus the first field once the screen is built + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_focusNodes.isNotEmpty) { + FocusScope.of(context).requestFocus(_focusNodes[0]); + } + }); + } + + @override + void dispose() { + for (final c in _controllers) c.dispose(); + for (final f in _focusNodes) f.dispose(); + _resendTimer?.cancel(); + super.dispose(); + } + + void _startResendTimer() { + _resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_resendTime > 0) { + setState(() => _resendTime--); + } else { + timer.cancel(); + } + }); + } + + void _onOtpChanged(int index, String value) { + if (value.length == 1 && index < _otpLength - 1) { + _focusNodes[index + 1].requestFocus(); + } else if (value.isEmpty && index > 0) { + _focusNodes[index - 1].requestFocus(); + } + _checkOtpCompletion(); + } + + void _checkOtpCompletion() { + final isComplete = _controllers.every((c) => c.text.isNotEmpty); + + if (isComplete != _isOtpComplete) { + setState(() => _isOtpComplete = isComplete); + + if (isComplete) { + _verifyOtp(); + } + } + } + + void _resendOtp() { + if (_resendTime == 0) { + setState(() => _resendTime = 60); + _startResendTimer(); + autoFillOtp("1234"); + + // call resend API here + } + } + + String _getMaskedPhoneNumber() { + final phone = widget.phoneNumber; + return phone.length > 4 ? '05xxxxxx${phone.substring(phone.length - 2)}' : phone; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + hideLogoAndLang: true, + onBackPressed: () { + Navigator.of(context).pop(); + }, + onLanguageChanged: (lang) {}, + ), + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 40.h), + Text( + 'OTP Verification', + style: TextStyle(fontSize: 24.fSize, fontWeight: FontWeight.bold), + ), + SizedBox(height: 16.h), + Text( + 'We have sent you the OTP code on ${_getMaskedPhoneNumber()} via SMS for registration verification', + style: TextStyle(fontSize: 16.fSize, color: Colors.grey), + ), + SizedBox(height: 40.h), + + // OTP Input Fields + SizedBox( + height: 100, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(_otpLength, (index) { + return ValueListenableBuilder( + valueListenable: _controllers[index], + builder: (context, value, _) { + final hasText = value.text.isNotEmpty; + + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + width: 70.h, + margin: EdgeInsets.symmetric(horizontal: 4.h), + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration(color: _isOtpComplete ? AppColors.successColor : (hasText ? AppColors.blackBgColor : AppColors.whiteColor), borderRadius: 16), + child: Center( + child: TextField( + controller: _controllers[index], + focusNode: _focusNodes[index], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + style: TextStyle( + fontSize: 40.fSize, + fontWeight: FontWeight.bold, + color: AppColors.whiteColor, + ), + decoration: InputDecoration( + counterText: '', + filled: true, + fillColor: Colors.transparent, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: BorderSide.none, + ), + ), + onChanged: (v) => _onOtpChanged(index, v), + ), + ), + ); + }, + ); + }), + ), + ), + + const SizedBox(height: 32), + + // Resend OTP + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Didn't receive it? "), + if (_resendTime > 0) + Text( + 'resend in (${_resendTime.toString().padLeft(2, '0')}:00). ', + style: const TextStyle(color: Colors.grey), + ) + else + GestureDetector( + onTap: _resendOtp, + child: const Text( + 'Resend', + style: TextStyle( + color: AppColors.primaryRedColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + void _verifyOtp() { + final otp = _controllers.map((c) => c.text).join(); + debugPrint('Verifying OTP: $otp'); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Verifying OTP: $otp')), + ); + } + + /// Auto fill OTP into text fields + void autoFillOtp(String otp) { + if (otp.length != _otpLength) return; + + for (int i = 0; i < _otpLength; i++) { + _controllers[i].text = otp[i]; + } + + // Move focus to the last field + _focusNodes[_otpLength - 1].requestFocus(); + + // Trigger completion check and color update + _checkOtpCompletion(); + } +} diff --git a/pubspec.lock b/pubspec.lock index c503e1b..679a57e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" audio_session: dependency: transitive description: @@ -874,26 +874,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "11.0.1" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.1" lints: dependency: transitive description: @@ -1455,10 +1455,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.4" time: dependency: transitive description: @@ -1583,18 +1583,18 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" vm_service: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" web: dependency: "direct main" description: @@ -1636,5 +1636,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.7.0 <4.0.0" flutter: ">=3.29.0"