@ -7,7 +7,6 @@ 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/enums.dart ' ;
import ' package:hmg_patient_app_new/core/utils/size_utils.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/core/utils/utils.dart ' ;
import ' package:hmg_patient_app_new/core/utils/validation_utils.dart ' ;
import ' package:hmg_patient_app_new/extensions/string_extensions.dart ' ;
import ' package:hmg_patient_app_new/extensions/string_extensions.dart ' ;
import ' package:hmg_patient_app_new/extensions/widget_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/features/authentication/authentication_view_model.dart ' ;
@ -34,19 +33,35 @@ class _RegisterNew extends State<RegisterNew> {
super . initState ( ) ;
super . initState ( ) ;
_nationalIdFocusNode = FocusNode ( ) ;
_nationalIdFocusNode = FocusNode ( ) ;
_dobFocusNode = FocusNode ( ) ;
_dobFocusNode = FocusNode ( ) ;
/ / Clear errors when entering register screen
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
final authVm = context . read < AuthenticationViewModel > ( ) ;
authVm . clearNationalIdError ( ) ;
authVm . clearDobError ( ) ;
authVm . clearPhoneNumberError ( ) ;
} ) ;
}
}
@ override
@ override
void dispose ( ) {
void dispose ( ) {
_nationalIdFocusNode . dispose ( ) ;
_nationalIdFocusNode . dispose ( ) ;
_dobFocusNode . dispose ( ) ;
_dobFocusNode . dispose ( ) ;
/ / Clear errors when leaving register screen
/ / Use post frame callback to avoid calling notifyListeners during dispose
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
final authVm = context . read < AuthenticationViewModel > ( ) ;
authVm . clearNationalIdError ( ) ;
authVm . clearDobError ( ) ;
authVm . clearPhoneNumberError ( ) ;
} ) ;
super . dispose ( ) ;
super . dispose ( ) ;
}
}
@ override
@ override
Widget build ( BuildContext context ) {
Widget build ( BuildContext context ) {
AuthenticationViewModel authVm = context . read < AuthenticationViewModel > ( ) ;
return Scaffold (
return Scaffold (
backgroundColor: AppColors . bgScaffoldColor ,
backgroundColor: AppColors . bgScaffoldColor ,
appBar: CustomAppBar (
appBar: CustomAppBar (
@ -57,199 +72,216 @@ class _RegisterNew extends State<RegisterNew> {
context . setLocale ( value = = ' en ' ? Locale ( ' en ' , ' US ' ) : Locale ( ' ar ' , ' SA ' ) ) ;
context . setLocale ( value = = ' en ' ? Locale ( ' en ' , ' US ' ) : Locale ( ' ar ' , ' SA ' ) ) ;
} ,
} ,
) ,
) ,
body: GestureDetector (
body: Consumer < AuthenticationViewModel > (
onTap: ( ) {
builder: ( context , authVm , child ) {
/ / Dismiss keyboard and unfocus all input fields
return GestureDetector (
_nationalIdFocusNode . unfocus ( ) ;
onTap: ( ) {
_dobFocusNode . unfocus ( ) ;
/ / Dismiss keyboard and unfocus all input fields
FocusScope . of ( context ) . unfocus ( ) ;
_nationalIdFocusNode . unfocus ( ) ;
} ,
_dobFocusNode . unfocus ( ) ;
child: ScrollConfiguration (
FocusScope . of ( context ) . unfocus ( ) ;
behavior: ScrollConfiguration . of ( context ) . copyWith ( overscroll: false , physics: const ClampingScrollPhysics ( ) ) ,
child: NotificationListener < OverscrollIndicatorNotification > (
onNotification: ( notification ) {
notification . disallowIndicator ( ) ;
return true ;
} ,
} ,
child: SingleChildScrollView (
child: ScrollConfiguration (
physics: ClampingScrollPhysics ( ) ,
behavior: ScrollConfiguration . of ( context ) . copyWith ( overscroll: false , physics: const ClampingScrollPhysics ( ) ) ,
padding: EdgeInsets . symmetric ( horizontal: 24. h ) ,
child: NotificationListener < OverscrollIndicatorNotification > (
child: Column (
onNotification: ( notification ) {
mainAxisSize: MainAxisSize . min ,
notification . disallowIndicator ( ) ;
crossAxisAlignment: CrossAxisAlignment . start ,
return true ;
children: [
} ,
Utils . showLottie ( context: context , assetPath: ' assets/animations/lottie/register.json ' , width: 200. w , height: 200. h , fit: BoxFit . cover , repeat: true ) ,
child: SingleChildScrollView (
SizedBox ( height: 16. h ) ,
physics: ClampingScrollPhysics ( ) ,
LocaleKeys . prepareToElevate . tr ( context: context ) . toText32 ( isBold: true ) ,
padding: EdgeInsets . symmetric ( horizontal: 24. h ) ,
SizedBox ( height: 24. h ) ,
child: Column (
Directionality (
mainAxisSize: MainAxisSize . min ,
textDirection: Directionality . of ( context ) ,
crossAxisAlignment: CrossAxisAlignment . start ,
child: Container (
children: [
decoration: BoxDecoration ( color: AppColors . whiteColor , borderRadius: BorderRadius . circular ( 24 ) ) ,
Utils . showLottie ( context: context , assetPath: ' assets/animations/lottie/register.json ' , width: 200. w , height: 200. h , fit: BoxFit . cover , repeat: true ) ,
padding: EdgeInsets . symmetric ( horizontal: 16. h ) ,
SizedBox ( height: 16. h ) ,
child: Column (
LocaleKeys . prepareToElevate . tr ( context: context ) . toText32 ( isBold: true ) ,
children: [
SizedBox ( height: 24. h ) ,
CustomCountryDropdown (
Directionality (
countryList: CountryEnum . values . where ( ( c ) = > c ! = CountryEnum . others ) . toList ( ) ,
textDirection: Directionality . of ( context ) ,
onCountryChange: authVm . onCountryChange ,
child: Container (
/ / isRtl: Directionality . of ( context ) = = TextDirection . LTR ,
decoration: BoxDecoration (
) . withVerticalPadding ( 8. h ) ,
color: AppColors . whiteColor ,
Divider ( height: 1. h ) ,
borderRadius: BorderRadius . circular ( 24 ) ,
TextInputWidget (
border: Border . all (
labelText: LocaleKeys . nationalIdNumber . tr ( context: context ) ,
color: authVm . hasRegistrationFormError ? AppColors . primaryRedBorderColor : Colors . transparent ,
hintText: " xxxxxxxxx " ,
width: 1 ,
controller: authVm . nationalIdController ,
) ,
focusNode: _nationalIdFocusNode ,
) ,
keyboardType: TextInputType . number ,
padding: EdgeInsets . symmetric ( horizontal: 16. h ) ,
isEnable: true ,
child: Column (
prefix: null ,
children: [
isAllowRadius: true ,
CustomCountryDropdown (
isBorderAllowed: false ,
countryList: CountryEnum . values . where ( ( c ) = > c ! = CountryEnum . others ) . toList ( ) ,
isAllowLeadingIcon: true ,
onCountryChange: authVm . onCountryChange ,
autoFocus: true ,
/ / isRtl: Directionality . of ( context ) = = TextDirection . LTR ,
padding: EdgeInsets . symmetric ( vertical: 8. h ) ,
) . withVerticalPadding ( 8. h ) ,
leadingIcon: AppAssets . student_card ,
Divider ( height: 1. h ) ,
fontFamily: " Poppins " ,
TextInputWidget (
) . withVerticalPadding ( 8 ) ,
labelText: LocaleKeys . nationalIdNumber . tr ( context: context ) ,
Divider ( height: 1 ) ,
hintText: " xxxxxxxxx " ,
TextInputWidget (
controller: authVm . nationalIdController ,
labelText: LocaleKeys . dob . tr ( context: context ) ,
focusNode: _nationalIdFocusNode ,
hintText: " 11 July, 1994 " ,
keyboardType: TextInputType . number ,
controller: authVm . dobController ,
isEnable: true ,
focusNode: _dobFocusNode ,
prefix: null ,
isEnable: true ,
isAllowRadius: true ,
prefix: null ,
isBorderAllowed: false ,
isAllowRadius: true ,
isAllowLeadingIcon: true ,
isBorderAllowed: false ,
autoFocus: true ,
isAllowLeadingIcon: true ,
padding: EdgeInsets . symmetric ( vertical: 8. h ) ,
padding: EdgeInsets . symmetric ( vertical: 8. h ) ,
leadingIcon: AppAssets . student_card ,
leadingIcon: AppAssets . birthday_cake ,
fontFamily: " Poppins " ,
selectionType: SelectionTypeEnum . calendar ,
hasError: false , / / Don ' t show individual field border
onCalendarTypeChanged: authVm . onCalenderTypeChange ,
errorMessage: authVm . nationalIdError , / / Show error message if exists
onChange: authVm . onDobChange ,
onChange: ( value ) {
fontFamily: " Poppins " ,
/ / Clear error when user starts typing
) . withVerticalPadding ( 8 ) ,
authVm . clearNationalIdError ( ) ;
] ,
} ,
) ,
) . withVerticalPadding ( 8 ) ,
) ,
Divider ( height: 1 ) ,
) ,
TextInputWidget (
SizedBox ( height: 25. h ) ,
labelText: LocaleKeys . dob . tr ( context: context ) ,
GestureDetector (
hintText: " 11 July, 1994 " ,
onTap: authVm . onTermAccepted ,
controller: authVm . dobController ,
child: Row (
focusNode: _dobFocusNode ,
children: [
isEnable: true ,
Selector < AuthenticationViewModel , bool > (
prefix: null ,
selector: ( _ , viewModel ) = > viewModel . isTermsAccepted ,
isAllowRadius: true ,
shouldRebuild: ( previous , next ) = > previous ! = next ,
isBorderAllowed: false ,
builder: ( context , isTermsAccepted , child ) {
isAllowLeadingIcon: true ,
return AnimatedContainer (
padding: EdgeInsets . symmetric ( vertical: 8. h ) ,
duration: const Duration ( milliseconds: 200 ) ,
leadingIcon: AppAssets . birthday_cake ,
height: 24. h ,
selectionType: SelectionTypeEnum . calendar ,
width: 24. h ,
onCalendarTypeChanged: authVm . onCalenderTypeChange ,
decoration: BoxDecoration (
onChange: ( value ) {
color: isTermsAccepted ? AppColors . primaryRedColor : Colors . transparent ,
authVm . onDobChange ( value ) ;
borderRadius: BorderRadius . circular ( 6 ) ,
/ / Clear error when user selects a date
border: Border . all ( color: isTermsAccepted ? AppColors . primaryRedBorderColor : AppColors . greyColor , width: 2. h ) ,
authVm . clearDobError ( ) ;
) ,
} ,
child: isTermsAccepted ? Icon ( Icons . check , size: 16. f , color: Colors . white ) : null ,
fontFamily: " Poppins " ,
) ;
hasError: false , / / Don ' t show individual field border
} ,
errorMessage: authVm . dobError , / / Show error message if exists
) . withVerticalPadding ( 8 ) ,
] ,
) ,
) ,
) ,
SizedBox ( width: 12. h ) ,
) ,
Row (
SizedBox ( height: 25. h ) ,
GestureDetector (
onTap: authVm . onTermAccepted ,
child: Row (
children: [
children: [
Text (
Selector < AuthenticationViewModel , bool > (
LocaleKeys . iAcceptThe . tr ( context: context ) ,
selector: ( _ , viewModel ) = > viewModel . isTermsAccepted ,
style: context . dynamicTextStyle ( fontSize: 14. f , fontWeight: FontWeight . w600 , color: Color ( 0xFF2E3039 ) ) ,
shouldRebuild: ( previous , next ) = > previous ! = next ,
) ,
builder: ( context , isTermsAccepted , child ) {
GestureDetector (
return AnimatedContainer (
onTap: ( ) {
duration: const Duration ( milliseconds: 200 ) ,
/ / Navigate to terms and conditions page
height: 24. h ,
Utils . openWebView (
width: 24. h ,
url: ' https://hmg.com/en/Pages/Terms.aspx ' ,
decoration: BoxDecoration (
color: isTermsAccepted ? AppColors . primaryRedColor : Colors . transparent ,
borderRadius: BorderRadius . circular ( 6 ) ,
border: Border . all ( color: isTermsAccepted ? AppColors . primaryRedBorderColor : AppColors . greyColor , width: 2. h ) ,
) ,
child: isTermsAccepted ? Icon ( Icons . check , size: 16. f , color: Colors . white ) : null ,
) ;
) ;
} ,
} ,
child: Text (
) ,
" ${ LocaleKeys . termsConditoins . tr ( context: context ) } " ,
SizedBox ( width: 12. h ) ,
style: context . dynamicTextStyle (
Row (
fontSize: 14. f ,
children: [
fontWeight: FontWeight . w600 ,
Text (
color: AppColors . primaryRedColor ,
LocaleKeys . iAcceptThe . tr ( context: context ) ,
decoration: TextDecoration . underline ,
style: context . dynamicTextStyle ( fontSize: 14. f , fontWeight: FontWeight . w600 , color: Color ( 0xFF2E3039 ) ) ,
decorationColor: AppColors . primaryRedBorderColor ,
) ,
) ,
) ,
GestureDetector (
onTap: ( ) {
/ / Navigate to terms and conditions page
Utils . openWebView (
url: ' https://hmg.com/en/Pages/Terms.aspx ' ,
) ;
} ,
child: Text (
" ${ LocaleKeys . termsConditoins . tr ( context: context ) } " ,
style: context . dynamicTextStyle (
fontSize: 14. f ,
fontWeight: FontWeight . w600 ,
color: AppColors . primaryRedColor ,
decoration: TextDecoration . underline ,
decorationColor: AppColors . primaryRedBorderColor ,
) ,
) ,
) ,
] ,
) ,
) ,
/ / Expanded (
/ / child: Text (
/ / LocaleKeys . iAcceptTermsConditions . tr ( context: context ) . split ( " the " ) . first ,
/ / style: context . dynamicTextStyle ( fontSize: 14. fSize , isBold: true , color: Color ( 0xFF2E3039 ) ) ,
/ / ) ,
/ / ) ,
] ,
] ,
) ,
) ,
/ / Expanded (
) ,
/ / child: Text (
SizedBox ( height: 25. h ) ,
/ / LocaleKeys . iAcceptTermsConditions . tr ( context: context ) . split ( " the " ) . first ,
CustomButton (
/ / style: context . dynamicTextStyle ( fontSize: 14. fSize , isBold: true , color: Color ( 0xFF2E3039 ) ) ,
text: LocaleKeys . registernow . tr ( context: context ) ,
/ / ) ,
icon: AppAssets . note_edit ,
/ / ) ,
onPressed: ( ) {
] ,
/ / Dismiss keyboard before proceeding
) ,
_nationalIdFocusNode . unfocus ( ) ;
) ,
_dobFocusNode . unfocus ( ) ;
SizedBox ( height: 25. h ) ,
FocusScope . of ( context ) . unfocus ( ) ;
CustomButton (
text: LocaleKeys . registernow . tr ( context: context ) ,
icon: AppAssets . note_edit ,
onPressed: ( ) {
/ / Dismiss keyboard before proceeding
_nationalIdFocusNode . unfocus ( ) ;
_dobFocusNode . unfocus ( ) ;
FocusScope . of ( context ) . unfocus ( ) ;
if ( ValidationUtils . isValidatedId (
/ / Use ViewModel validation method
nationalId: authVm . nationalIdController . text ,
if ( authVm . validateRegistrationForm ( ) ) {
selectedCountry: authVm . selectedCountrySignup ,
showRegisterModel ( context: context , authVM: authVm ) ;
isTermsAccepted: authVm . isTermsAccepted ,
}
dob: authVm . dobController . text ,
} ,
onOkPress: ( ) {
) ,
Navigator . of ( context ) . pop ( ) ;
SizedBox ( height: 14 ) ,
} ) ) {
Center (
showRegisterModel ( context: context , authVM: authVm ) ;
child: RichText (
}
textAlign: TextAlign . center ,
} ,
text: TextSpan (
) ,
SizedBox ( height: 14 ) ,
Center (
child: RichText (
textAlign: TextAlign . center ,
text: TextSpan (
style: context . dynamicTextStyle (
color: Colors . black ,
fontSize: 16. f ,
height: 26 / 16 ,
fontWeight: FontWeight . w600 ,
) ,
children: < TextSpan > [
TextSpan ( text: LocaleKeys . alreadyHaveAccount . tr ( context: context ) , style: context . dynamicTextStyle ( ) ) ,
TextSpan ( text: " " ) ,
TextSpan (
text: LocaleKeys . loginNow . tr ( context: context ) ,
style: context . dynamicTextStyle (
style: context . dynamicTextStyle (
color: AppColors. primaryRedColor ,
color: Colors . black ,
fontSize: 16. f ,
fontSize: 16. f ,
height: 26 / 16 ,
height: 26 / 16 ,
fontWeight: FontWeight . w600 ,
fontWeight: FontWeight . w600 ,
) ,
) ,
recognizer: TapGestureRecognizer ( )
children: < TextSpan > [
. . onTap = ( ) {
TextSpan ( text: LocaleKeys . alreadyHaveAccount . tr ( context: context ) , style: context . dynamicTextStyle ( ) ) ,
Navigator . of ( context ) . pop ( ) ;
TextSpan ( text: " " ) ,
} ,
TextSpan (
text: LocaleKeys . loginNow . tr ( context: context ) ,
style: context . dynamicTextStyle (
color: AppColors . primaryRedColor ,
fontSize: 16. f ,
height: 26 / 16 ,
fontWeight: FontWeight . w600 ,
) ,
recognizer: TapGestureRecognizer ( )
. . onTap = ( ) {
Navigator . of ( context ) . pop ( ) ;
} ,
) ,
] ,
) ,
) ,
] ,
) ,
) ,
) ,
) ,
SizedBox ( height: 30. h ) ,
] ,
) ,
) ,
SizedBox ( height: 30. h ) ,
) ,
] ,
) ,
) ,
) ,
) ,
) ,
) ;
) ,
} ,
) ) ;
) ) ;
}
}
@ -264,73 +296,75 @@ class _RegisterNew extends State<RegisterNew> {
builder: ( bottomSheetContext ) = > Padding (
builder: ( bottomSheetContext ) = > Padding (
padding: EdgeInsets . only ( bottom: MediaQuery . of ( bottomSheetContext ) . viewInsets . bottom ) ,
padding: EdgeInsets . only ( bottom: MediaQuery . of ( bottomSheetContext ) . viewInsets . bottom ) ,
child: SingleChildScrollView (
child: SingleChildScrollView (
child: GenericBottomSheet (
child: Consumer < AuthenticationViewModel > (
countryCode: authVM . selectedCountrySignup . countryCode ,
builder: ( context , authVm , child ) {
initialPhoneNumber: authVM . phoneNumberController . text ,
return GenericBottomSheet (
textController: authVM . phoneNumberController ,
countryCode: authVm . selectedCountrySignup . countryCode ,
isEnableCountryDropdown: false ,
initialPhoneNumber: authVm . phoneNumberController . text ,
onCountryChange: authVM . onCountryChange ,
textController: authVm . phoneNumberController ,
onChange: authVM . onPhoneNumberChange ,
isEnableCountryDropdown: false ,
autoFocus: true ,
onCountryChange: authVm . onCountryChange ,
buttons: [
onChange: ( value ) {
Padding (
authVm . onPhoneNumberChange ( value ) ;
padding: const EdgeInsets . only ( bottom: 10 ) ,
/ / Clear error when user starts typing
child: CustomButton (
authVm . clearPhoneNumberError ( ) ;
text: LocaleKeys . sendOTPSMS . tr ( context: context ) ,
} ,
onPressed: ( ) async {
autoFocus: true ,
/ / Dismiss keyboard before validation
phoneNumberError: authVm . phoneNumberError ,
FocusScope . of ( context ) . unfocus ( ) ;
buttons: [
Padding (
padding: const EdgeInsets . only ( bottom: 10 ) ,
child: CustomButton (
text: LocaleKeys . sendOTPSMS . tr ( context: context ) ,
onPressed: ( ) async {
/ / Dismiss keyboard before validation
FocusScope . of ( context ) . unfocus ( ) ;
if ( ValidationUtils . isValidatePhone (
/ / Use ViewModel validation method
phoneNumber: authVM . phoneNumberController . text ,
if ( authVm . validatePhoneNumber ( ) ) {
onOkPress: ( ) {
appState . setSelectDeviceByImeiRespModelElement ( null ) ;
Navigator . of ( context ) . pop ( ) ;
await authVm . onRegistrationStart ( otpTypeEnum: OTPTypeEnum . sms ) ;
}
} ,
} ,
) ) {
backgroundColor: AppColors . primaryRedColor ,
appState . setSelectDeviceByImeiRespModelElement ( null ) ;
borderColor: AppColors . primaryRedBorderColor ,
await authVM . onRegistrationStart ( otpTypeEnum: OTPTypeEnum . sms ) ;
textColor: AppColors . whiteColor ,
}
icon: AppAssets . message ,
} ,
) ,
backgroundColor: AppColors . primaryRedColor ,
) ,
borderColor: AppColors . primaryRedBorderColor ,
Row (
textColor: AppColors . whiteColor ,
crossAxisAlignment: CrossAxisAlignment . center ,
icon: AppAssets . message ,
mainAxisAlignment: MainAxisAlignment . center ,
) ,
children: [
) ,
Padding (
Row (
padding: EdgeInsets . symmetric ( horizontal: 8. h ) ,
crossAxisAlignment: CrossAxisAlignment . center ,
child: LocaleKeys . oR . tr ( context: context ) . toText16 ( color: AppColors . textColor ) ,
mainAxisAlignment: MainAxisAlignment . center ,
) ,
children: [
] ,
) ,
Padding (
Padding (
padding: EdgeInsets . symmetric ( horizontal: 8. h ) ,
padding: EdgeInsets . only ( bottom: 10. h , top: 10. h ) ,
child: LocaleKeys . oR . tr ( context: context ) . toText16 ( color: AppColors . textColor ) ,
child: CustomButton (
text: LocaleKeys . sendOTPWHATSAPP . tr ( context: context ) ,
onPressed: ( ) async {
FocusScope . of ( context ) . unfocus ( ) ;
/ / Use ViewModel validation method
if ( authVm . validatePhoneNumber ( ) ) {
appState . setSelectDeviceByImeiRespModelElement ( null ) ;
await authVm . onRegistrationStart ( otpTypeEnum: OTPTypeEnum . whatsapp ) ;
}
} ,
backgroundColor: AppColors . whiteColor ,
borderColor: AppColors . borderOnlyColor ,
textColor: AppColors . textColor ,
icon: AppAssets . whatsapp ,
iconColor: null ,
) ,
) ,
) ,
] ,
] ,
) ,
) ;
Padding (
} ,
padding: EdgeInsets . only ( bottom: 10. h , top: 10. h ) ,
child: CustomButton (
text: LocaleKeys . sendOTPWHATSAPP . tr ( context: context ) ,
onPressed: ( ) async {
FocusScope . of ( context ) . unfocus ( ) ;
if ( ValidationUtils . isValidatePhone (
phoneNumber: authVM . phoneNumberController . text ,
onOkPress: ( ) {
Navigator . of ( context ) . pop ( ) ;
} ,
) ) {
appState . setSelectDeviceByImeiRespModelElement ( null ) ;
await authVM . onRegistrationStart ( otpTypeEnum: OTPTypeEnum . whatsapp ) ;
}
} ,
backgroundColor: AppColors . whiteColor ,
borderColor: AppColors . borderOnlyColor ,
textColor: AppColors . textColor ,
icon: AppAssets . whatsapp ,
iconColor: null ,
) ,
) ,
] ,
) ,
) ,
) ,
) ,
) ,
) ,