From d70aaf955f678bc937c2e0ddbe37e73730712354 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 9 Feb 2026 23:15:12 +0300 Subject: [PATCH 1/3] feat: Implement auto-scrolling to the first validation error in the create diet form and refine image validation logic. --- .../presentation/create_diet_screen.dart | 54 ++++++++++++++++--- .../presentation/cubit/create_diet_cubit.dart | 4 +- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/lib/features/create_diet/presentation/create_diet_screen.dart b/lib/features/create_diet/presentation/create_diet_screen.dart index b77ac3b9..2497b633 100644 --- a/lib/features/create_diet/presentation/create_diet_screen.dart +++ b/lib/features/create_diet/presentation/create_diet_screen.dart @@ -58,6 +58,39 @@ class _CreateDietScreenContentState extends State { final GlobalKey _mdKey = GlobalKey(); bool _mdToolbarVisible = false; + final GlobalKey _nameKey = GlobalKey(); + final GlobalKey _durationKey = GlobalKey(); + final GlobalKey _statusKey = GlobalKey(); + final GlobalKey _typeKey = GlobalKey(); + final GlobalKey _imageKey = GlobalKey(); + final GlobalKey _planKey = GlobalKey(); + + void _scrollToFirstError(CreateDietState state) { + GlobalKey? key; + if (state.dietNameError != null) { + key = _nameKey; + } else if (state.dietDescriptionError != null) { + key = _mdKey; + } else if (state.dietDurationError != null) { + key = _durationKey; + } else if (state.dietStateError != null) { + key = _statusKey; + } else if (state.dietImageError != null) { + key = _imageKey; + } else if (state.dailyScheduleErrors.any((e) => e.isNotEmpty)) { + key = _planKey; + } + + if (key != null && key.currentContext != null) { + Scrollable.ensureVisible( + key.currentContext!, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + alignment: 0.1, + ); + } + } + @override void initState() { super.initState(); @@ -118,6 +151,7 @@ class _CreateDietScreenContentState extends State { children: [ const SizedBox(height: 16), CustomTextFormField( + key: _nameKey, hintText: s.enter_diet_name, controller: nameController, hasError: state.dietNameError != null, @@ -143,19 +177,20 @@ class _CreateDietScreenContentState extends State { ), if (state.dietDescriptionError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietDescriptionError ?? '')], const SizedBox(height: 12), - const DietDurationDropDownWidget(), + DietDurationDropDownWidget(key: _durationKey), const SizedBox(height: 12), - StatusDropDownWidget(dietState: widget.diet?.state), + StatusDropDownWidget(key: _statusKey, dietState: widget.diet?.state), const SizedBox(height: 16), - const DietTypeChoiceWidget(), + DietTypeChoiceWidget(key: _typeKey), const SizedBox(height: 16), - const DietImagePickerWidget(), + DietImagePickerWidget(key: _imageKey), const SizedBox(height: 16), ], ), ), const SizedBox(height: 16), MainBackgroundBouble( + key: _planKey, child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Center( @@ -185,7 +220,11 @@ class _CreateDietScreenContentState extends State { ? () {} : () async { final diet = await context.read().getDietForPreview(); - if (diet == null) return; + if (diet == null) { + // ignore: use_build_context_synchronously + _scrollToFirstError(context.read().state); + return; + } if (!context.mounted) return; GoRouter.of(context).pushNamed(DietPreviewScreen.name, extra: diet); }, @@ -204,7 +243,10 @@ class _CreateDietScreenContentState extends State { CustomOutlinedButton( onPressed: () async { final result = await context.read().createDiet(); - if (result == 'error') { + if (result == 'validation') { + // ignore: use_build_context_synchronously + _scrollToFirstError(context.read().state); + } else if (result == 'error') { if (!context.mounted) return; showErrorDialog(context, title: '${s.could_not_publish_diet} ${state.dietData.name}'); } diff --git a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart index 8ecf89ba..f8eb5a73 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -394,11 +394,11 @@ class CreateDietCubit extends Cubit { result = false; emit(state.copyWith(dietStateError: 'Статус диеты не выбран')); } - if (state.dietData.image == null) { + if (state.dietData.image == null && state.dietData.imageUrl == null) { result = false; emit(state.copyWith(dietImageError: 'Не добавлена обложка')); } - result = _isDailyScheduleValid(); + result = _isDailyScheduleValid() && result; return result; } From 34eccb7db5c252cec282459f77402d779945ac99 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 10 Feb 2026 18:36:18 +0300 Subject: [PATCH 2/3] feat: Enhance OAuth error state to include specific registration validation errors and pass email in the verification request. --- .../bloc/oauth_cubit/oauth_cubit.dart | 29 +++++++++++++++---- .../bloc/oauth_cubit/oauth_state.dart | 7 ++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart index daf156a8..a066c959 100644 --- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -36,9 +36,29 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin { final emailErrors = errors?['email'] as List?; final phoneErrors = errors?['phone'] as List?; - if ((emailErrors != null && emailErrors.isNotEmpty) || (phoneErrors != null && phoneErrors.isNotEmpty)) { - // Это ошибка валидации - нужно показать экран подтверждения - emit(const OAuthState.error(errorMessage: 'validation_error')); + final errorsList = []; + if (emailErrors != null && emailErrors.isNotEmpty) { + for (final error in emailErrors) { + final errorStr = error.toString(); + if (errorStr.contains('Некорректный формат email')) { + errorsList.add(RegistrationValidationErrors.incorrectEmailFormat); + } + if (errorStr.contains('почта уже существует') || errorStr.contains('Эл. почта уже существует')) { + errorsList.add(RegistrationValidationErrors.emailExist); + } + } + } + if (phoneErrors != null && phoneErrors.isNotEmpty) { + for (final error in phoneErrors) { + final errorStr = error.toString(); + if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) { + errorsList.add(RegistrationValidationErrors.phoneExist); + } + } + } + + if (errorsList.isNotEmpty) { + emit(OAuthState.error(errorMessage: 'validation_error', registrationValidationErrors: errorsList)); } else { emit(OAuthState.error(errorMessage: e.toString())); } @@ -126,7 +146,7 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin { try { emit(const OAuthState.loading()); log('OAuthCubit: Requesting verification code for email: $email, phone: $phone'); - final response = await authRepository.requestOAuthVerification(email: null, phone: phone); + final response = await authRepository.requestOAuthVerification(email: email, phone: phone); if (response != null) { log('OAuthCubit: Verification code requested successfully, masked email: ${response.email}'); } @@ -188,4 +208,3 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin { } } -enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat } diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart index 7da9ab7c..517222de 100644 --- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart @@ -10,5 +10,10 @@ abstract class OAuthState with _$OAuthState { const factory OAuthState.loaded({required String token}) = OAuthStateLoaded; - const factory OAuthState.error({required String errorMessage}) = OAuthStateError; + const factory OAuthState.error({ + required String errorMessage, + List? registrationValidationErrors, + }) = OAuthStateError; } + +enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat } From ae07851396495e74072d60a380f542fcbb99fc7e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 10 Feb 2026 18:47:53 +0300 Subject: [PATCH 3/3] test: update OAuth verification mock to use `anyNamed` arguments for `email` and `phone`. --- .../auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart b/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart index 347ae773..8bf682e2 100644 --- a/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart +++ b/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart @@ -287,8 +287,7 @@ void main() { const verificationResponse = OAuthVerificationRequestResponse(email: 't***@example.com', status: 'sent'); - // OAuthCubit всегда передает email: null в репозиторий, независимо от переданного параметра - when(mockAuthRepository.requestOAuthVerification(email: null, phone: null)).thenAnswer((_) async => verificationResponse); + when(mockAuthRepository.requestOAuthVerification(email: anyNamed('email'), phone: anyNamed('phone'))).thenAnswer((_) async => verificationResponse); final result = await oauthCubit!.requestOAuthVerification(email: 'test@example.com');