diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 57f9bcd..64477a3 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -787,7 +787,8 @@ "iAcceptTermsConditions": "أوافق على الشروط والأحكام", "alreadyHaveAccount": "هل لديك حساب بالفعل؟", "loginNow": "تسجيل الدخول الآن", - "notice": "إشعار" - - + "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 311db65..cd15a5a 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -774,7 +774,7 @@ "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", + "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?", @@ -783,5 +783,8 @@ "iAcceptTermsConditions": "I Accept the Terms and Conditions", "alreadyHaveAccount": "Already have an account?", "loginNow": "Login Now", - "notice": "Notice" + "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/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/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index f219526..c5f2592 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -786,5 +786,8 @@ abstract class LocaleKeys { 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 87f40ca..f74348f 100644 --- a/lib/presentation/authentication/login.dart +++ b/lib/presentation/authentication/login.dart @@ -4,12 +4,14 @@ 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/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'; @@ -77,7 +79,22 @@ class _LoginScreen extends State { text: LocaleKeys.login.tr(), icon: AppAssets.login1, iconColor: Colors.white, - onPressed: () {}, + onPressed: () { + showLoginModel(context: context); + // 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( @@ -119,4 +136,61 @@ class _LoginScreen extends State { ), ); } + + 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: TextEditingController(), + 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, + ), + ), + 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: () {}, + 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 index 5564c46..0fcad5c 100644 --- a/lib/presentation/authentication/register.dart +++ b/lib/presentation/authentication/register.dart @@ -12,10 +12,12 @@ 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 @@ -39,173 +41,275 @@ class _RegisterNew extends State { 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( + 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: Country.values, - onCountryChange: (Country? value) {}, - isRtl: Directionality.of(context) == TextDirection.LTR, - ).withVerticalPadding(8.h), - - // DropdownWidget( - // labelText: LocaleKeys.country.tr(), - // hintText: LocaleKeys.ksa.tr(), - // isEnable: true, - // selectedValue: appState.getLanguageID(context) == "1" ? "selectedCountry.nameArabic" : "selectedCountry.displayName", - // dropdownItems: Country.values.map((e) => appState.getLanguageID(context) == " 1" ? e.displayName : e.displayName).toList(), - // onChange: (val) { - // if (val != null) {} - // }, - // isBorderAllowed: false, - // hasSelectionCustomIcon: true, - // isAllowRadius: false, - // padding: EdgeInsets.symmetric(vertical: 8.h), - // selectionCustomIcon: AppAssets.arrow_down, - // ).withVerticalPadding(8), - 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), - ], + 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: Country.values, + onCountryChange: (Country? 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), + ], + ), ), ), ), - 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, + )); + } + + 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, ), ), - 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)), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 8.h), + child: LocaleKeys.oR.tr().toText16(color: AppColors.textColor), + ), + ], ), - ), - ], - ), - ), - SizedBox(height: 25.h), - CustomButton( - text: "Register", - icon: AppAssets.note_edit, - onPressed: () {}, - ), - 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(); + 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, + ), ), ], ), ), ), - SizedBox(height: 30), - ], - ), - ), - ),) - , - ) ); } } diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 252d887..2c8a1fc 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -29,6 +29,7 @@ class AppColors { static const Color bgGreenColor = Color(0xFF18C273); static const Color textColor = Color(0xFF2E3039); static const Color borderOnlyColor = Color(0xFF2E3039); + static const Color blackBgColor = Color(0xFF2E3039); static const blackColor = textColor; static const inputLabelTextColor = Color(0xff898A8D); diff --git a/lib/widgets/appbar/app_bar_widget.dart b/lib/widgets/appbar/app_bar_widget.dart index 2fdbc94..4bc08a9 100644 --- a/lib/widgets/appbar/app_bar_widget.dart +++ b/lib/widgets/appbar/app_bar_widget.dart @@ -11,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 @@ -28,7 +30,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { leading: null, automaticallyImplyLeading: false, title: Padding( - padding: EdgeInsets.symmetric(horizontal: 10.h), + padding: EdgeInsets.symmetric(horizontal: 10.h), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -43,25 +45,26 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { ), // Logo - Utils.buildSvgWithAssets( - icon: AppAssets.habiblogo, - ), + 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()} + ], + ), ), ), - ), ], ), ), diff --git a/lib/widgets/bottomsheet/generic_bottom_sheet.dart b/lib/widgets/bottomsheet/generic_bottom_sheet.dart index 13a02a5..eefb187 100644 --- a/lib/widgets/bottomsheet/generic_bottom_sheet.dart +++ b/lib/widgets/bottomsheet/generic_bottom_sheet.dart @@ -3,9 +3,12 @@ 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 { @@ -18,20 +21,21 @@ class GenericBottomSheet extends StatefulWidget { 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 - }); + 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(); @@ -64,11 +68,8 @@ class _GenericBottomSheetState extends State { child: Directionality( textDirection: Directionality.of(context), child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: Color(0xFFF8F8FA), - borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), - ), + padding: EdgeInsets.all(24.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.bgScaffoldColor, borderRadius: 16), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, @@ -86,18 +87,17 @@ class _GenericBottomSheetState extends State { ? LocaleKeys.enterEmail.tr().toText24() : LocaleKeys.enterPhoneNumber.tr().toText24()), InkWell( - onTap: () { - Navigator.of(context).pop(); - }, - child: Padding( - padding: const EdgeInsets.only(top: 10), - child: Utils.buildSvgWithAssets( - icon: AppAssets.cross_circle, - ), - )), + onTap: () { + Navigator.of(context).pop(); + }, + child: Padding( + padding: EdgeInsets.only(top: 10.h), + child: Utils.buildSvgWithAssets(icon: AppAssets.cross_circle), + ), + ), ], ), - const SizedBox(height: 8), + SizedBox(height: 8.h), // Subtitle widget.isFromSavedLogin ? LocaleKeys.pleaseChooseOption.tr().toText16() @@ -113,7 +113,7 @@ class _GenericBottomSheetState extends State { labelText: widget.isForEmail ? LocaleKeys.email : LocaleKeys.phoneNumber, hintText: widget.isForEmail ? "demo@gmail.com" : "5xxxxxxxx", controller: widget.textController!, - padding: EdgeInsets.only(top: 8, bottom: 8, left: 8, right: 8), + padding: EdgeInsets.all(8.h), keyboardType: widget.isForEmail ? TextInputType.emailAddress : TextInputType.number, onChange: (value) { widget.textController!.text = value!; @@ -133,7 +133,7 @@ class _GenericBottomSheetState extends State { : SizedBox(), ], - SizedBox(height: 24), + SizedBox(height: 24.h), ...widget.buttons, ], ), diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart index e08189a..8a08a5e 100644 --- a/lib/widgets/buttons/custom_button.dart +++ b/lib/widgets/buttons/custom_button.dart @@ -43,50 +43,40 @@ class CustomButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: isDisabled ? null : onPressed, - child: Container( - height: height, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: isDisabled ? Colors.transparent : backgroundColor, - borderRadius: borderRadius, - side: BorderSide( - width: borderWidth, - color: isDisabled ? borderColor.withOpacity(0.5) : borderColor, - )), - child: Padding( - padding: padding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (icon != null) - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Utils.buildSvgWithAssets(icon: icon!, iconColor: iconColor, isDisabled: isDisabled), - ), - Text( - text, - style: context.dynamicTextStyle( - fontSize: fontSize.fSize, - color: isDisabled ? textColor.withOpacity(0.5) : textColor, - letterSpacing: -0.4, - fontWeight: fontWeight, - ), + onTap: isDisabled ? null : onPressed, + child: Container( + height: height, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: isDisabled ? Colors.transparent : backgroundColor, + borderRadius: borderRadius, + side: BorderSide( + width: borderWidth, + color: isDisabled ? borderColor.withOpacity(0.5) : borderColor, + )), + child: Padding( + padding: padding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Utils.buildSvgWithAssets(icon: icon!, iconColor: iconColor, isDisabled: isDisabled), ), - ], - ), + Text( + text, + style: context.dynamicTextStyle( + fontSize: fontSize.fSize, + color: isDisabled ? textColor.withOpacity(0.5) : textColor, + letterSpacing: -0.4, + fontWeight: fontWeight, + ), + ), + ], ), - ) - - // .toSmoothContainer( - // smoothness: 1, - // side: BorderSide(width: borderWidth, color: backgroundColor), - // borderRadius: BorderRadius.circular(borderRadius * 1.2), - // foregroundDecoration: BoxDecoration( - // color: isDisabled ? backgroundColor.withOpacity(0.5) : Colors.transparent, - // borderRadius: BorderRadius.circular(borderRadius), - // ), - // ), - ); + ), + ), + ); } } 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(); + } +}