diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 3abc813c..b4071fe3 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -3,7 +3,7 @@ name: Eatvio App Android Test on: [ pull_request ] env: - flutter_version: "3.32.0" + flutter_version: "3.38.6" java_version: "21.x" flutter_channel: "stable" diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 0921f560..a26ff075 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -3,7 +3,7 @@ name: Eatvio App Android Test on: [ pull_request ] env: - flutter_version: "3.38.5" + flutter_version: "3.38.6" java_version: "21.x" flutter_channel: "stable" my_local_cache_dir: "/opt/hostedtoolcache/" diff --git a/android/app/build.gradle b/android/app/build.gradle index 79e6d71d..5fb4bf83 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -4,6 +4,7 @@ plugins { id 'com.google.gms.google-services' // END: FlutterFire Configuration id "kotlin-android" + id "kotlin-parcelize" id "dev.flutter.flutter-gradle-plugin" id 'com.huawei.agconnect' } @@ -68,10 +69,10 @@ android { } dependencies { - implementation 'com.huawei.agconnect:agconnect-core:1.9.1.301' - implementation 'com.huawei.hms:push:6.12.0.300' + implementation 'com.huawei.agconnect:agconnect-core:1.9.5.300' + implementation 'com.huawei.hms:push:6.13.0.301' // Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8 - implementation 'com.huawei.hms:hianalytics:6.12.0.300' + implementation 'com.huawei.hms:hianalytics:6.12.0.301' // BouncyCastle provider required by Huawei secure utils used transitively by HMS implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1' } diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index d5b109e4..7b12e0fb 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -79,7 +79,7 @@ class CubitRegistrations { sl.registerFactory(() => AuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl(), profileRepository: sl())); sl.registerFactory(() => RegistrationCubit(registrationRepository: sl(), prefs: sl())); sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); - sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); + sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl(), prefs: sl())); sl.registerFactory(() => OAuthCubit(authRepository: sl(), prefs: sl())); sl.registerFactory(() => RegistrationCubitV2(registrationRepository: sl())); // Feed related cubits diff --git a/lib/core/prefs_keys.dart b/lib/core/prefs_keys.dart index ea23e1d4..2d64cae6 100644 --- a/lib/core/prefs_keys.dart +++ b/lib/core/prefs_keys.dart @@ -1 +1 @@ -enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown } +enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown, isEmailVerificationSkipped } diff --git a/lib/core/routing/other_routes.dart b/lib/core/routing/other_routes.dart index 9b29ad98..038474ca 100644 --- a/lib/core/routing/other_routes.dart +++ b/lib/core/routing/other_routes.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/prefs.dart'; import 'package:eatvio_app/core/prefs_keys.dart'; import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; @@ -59,9 +62,26 @@ class OtherRoutes { try { // Prefs уже инициализирован в locator_service, но на всякий случай проверяем final prefs = await sl.getAsync(); - final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false; + final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false; if (!welcomeScreenWasShowed) { return null; + } + + if (await TokenStorage.haveTokenInStorage()) { + try { + final profileRepository = sl(); + final profile = await profileRepository.getMyProfile(); + + final firebaseApi = sl(); + await firebaseApi.registerPushToken(); + + if (profile.emailVerifiedAt != null || (prefs.getBool(PrefsKeys.isEmailVerificationSkipped) ?? false)) { + return RouteConstants.feedScreen; + } + return RouteConstants.verifyEmailScreen; + } catch (e) { + return '/auth-screen'; + } } else { return '/auth-screen'; } diff --git a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart index c80daada..942b3191 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -1,10 +1,8 @@ import 'dart:developer'; -import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/prefs.dart'; import 'package:eatvio_app/core/prefs_keys.dart'; -import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; @@ -12,7 +10,6 @@ import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; -import 'package:go_router/go_router.dart'; class AuthCubit extends Cubit { final AuthRepository authRepository; @@ -40,23 +37,10 @@ class AuthCubit extends Cubit { Future initializeUser() async { try { await registerAnonymousPushToken(); - if (await TokenStorage.haveTokenInStorage()) { - final profile = await profileRepository.getMyProfile(); - if (profile.emailVerifiedAt != null) { - await registerPushToken(); - FlutterNativeSplash.remove(); - AppRouter.navigatorKey.currentContext?.go(RouteConstants.feedScreen); - return; - } - FlutterNativeSplash.remove(); - AppRouter.navigatorKey.currentContext?.go(RouteConstants.verifyEmailScreen); - } else { - FlutterNativeSplash.remove(); - } + FlutterNativeSplash.remove(); } catch (e) { log('Error in initializeUser: $e'); - FlutterNativeSplash.remove(); // Убираем сплэш даже при ошибке - rethrow; + FlutterNativeSplash.remove(); } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index da6bed80..de6e14e7 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -278,7 +278,7 @@ class SocialAuthFacade { ), title: Text(s.authorization), ), - body: Expanded(child: WebViewWidget(controller: controller)), + body: WebViewWidget(controller: controller), ), ), ), diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart index 23d946af..f57548f0 100644 --- a/lib/features/create_diet/presentation/diet_preview_screen.dart +++ b/lib/features/create_diet/presentation/diet_preview_screen.dart @@ -105,15 +105,6 @@ class _DietPreviewScreenState extends State { ], ), ), - const SizedBox(width: 12), - InfoBouble( - backgroundColor: appTheme.whiteA700, - children: [ - SvgPicture.asset('assets/icons/comment2.svg', width: 13, height: 12), - const SizedBox(width: 8), - Text(widget.diet.totalComments.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)), - ], - ), ], ), ), @@ -157,16 +148,11 @@ class _DietPreviewScreenState extends State { }, itemCount: widget.diet.dailySchedule.length, ), - MainBackgroundBouble( - padding: const EdgeInsets.symmetric(vertical: 16), + const MainBackgroundBouble( + padding: EdgeInsets.symmetric(vertical: 16), child: Column( children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 16), - alignment: Alignment.centerLeft, - child: Text('0 ${getCommentForm(widget.diet.totalComments, s)}', style: TextStyles.textMMedium.copyWith(color: appTheme.gray500)), - ), - const SizedBox(height: 16), + SizedBox(height: 16), ], ), ), diff --git a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart index 4541782b..ded7cb1f 100644 --- a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart +++ b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart @@ -237,7 +237,16 @@ class CreateRecipeCubit extends Cubit { final isActiveCookTimeValid = validateActiveCookTime(); final isEnergyPerServingsValid = validateEnergyPerServing(); - return isTitleValid && isDescValid && isImageValid && areServingsValid && isCookTimeValid && isActiveCookTimeValid && isEnergyPerServingsValid; + final isStepsValid = validateSteps(); + + return isTitleValid && + isDescValid && + isImageValid && + areServingsValid && + isCookTimeValid && + isActiveCookTimeValid && + isEnergyPerServingsValid && + isStepsValid; } bool validateTitle() { @@ -328,4 +337,20 @@ class CreateRecipeCubit extends Cubit { Future> getGlossaryListByQuery(String query) async { return await glossaryRepository.getGlossaryListByQuery(query); } + + bool validateSteps() { + bool isValid = true; + for (var step in state.entity.steps) { + if (step.description == null || step.description!.trim().isEmpty) { + isValid = false; + break; + } + } + if (!isValid) { + emit(state.copyWith(stepsValidationError: 'Заполните описание для всех шагов')); + } else { + emit(state.copyWith(stepsValidationError: null)); + } + return isValid; + } } diff --git a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart index 41e88611..ef0efe78 100644 --- a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart +++ b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart @@ -15,6 +15,7 @@ abstract class CreateRecipeState with _$CreateRecipeState { String? servingsValidationError, String? energyPerServingValidationError, String? imageValidationError, + String? stepsValidationError, }) = _CreateRecipeState; } diff --git a/lib/features/create_recipe/presentation/widgets/add_portion_widget.dart b/lib/features/create_recipe/presentation/widgets/add_portion_widget.dart index cfe1c5ce..597bc09b 100644 --- a/lib/features/create_recipe/presentation/widgets/add_portion_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/add_portion_widget.dart @@ -131,6 +131,7 @@ class _AddPortionWidgetState extends State { if (widget.portionIndex != null) InkWell( onTap: () { + FocusScope.of(context).unfocus(); widget.createRecipeCubit.removeIngredient(widget.portionIndex!); Navigator.of(context).pop(); }, @@ -189,6 +190,7 @@ class _AddPortionWidgetState extends State { Ingredient(name: portion.name, unit: portion.portion.getValue(), portion: portion.quantity.toString(), productId: glossary.id), ); if (context.mounted) { + FocusScope.of(context).unfocus(); Navigator.of(context).pop(); } } else {} diff --git a/lib/features/create_recipe/presentation/widgets/ingredients_widget.dart b/lib/features/create_recipe/presentation/widgets/ingredients_widget.dart index d9769505..4b3261ab 100644 --- a/lib/features/create_recipe/presentation/widgets/ingredients_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/ingredients_widget.dart @@ -75,6 +75,7 @@ class _IngredientsWidgetState extends State { ListTile( key: AppKeys.recipeCreateScreenAddIngredientButton, onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); nameController.clear(); gramsController.clear(); pcsController.clear(); diff --git a/lib/features/create_recipe/presentation/widgets/steps_widget.dart b/lib/features/create_recipe/presentation/widgets/steps_widget.dart index 7f0e4b87..9beaa707 100644 --- a/lib/features/create_recipe/presentation/widgets/steps_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/steps_widget.dart @@ -55,6 +55,13 @@ class _StepsWidgetState extends State { ), ), ), + if (state.stepsValidationError != null) ...[ + const SizedBox(height: 8), + Text( + state.stepsValidationError!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ], const SizedBox(height: 12), ListView.separated( shrinkWrap: true, diff --git a/lib/features/diary/entities/diary_day_entity.dart b/lib/features/diary/entities/diary_day_entity.dart index 65f45570..e533dda7 100644 --- a/lib/features/diary/entities/diary_day_entity.dart +++ b/lib/features/diary/entities/diary_day_entity.dart @@ -18,5 +18,8 @@ abstract class DiaryDayEntity with _$DiaryDayEntity { @JsonKey(name: "day_of_chosen_diet") required int? dayOfChosenDiet, @JsonKey(name: "chosen_diet", fromJson: parseDiet) required DiaryDietModel? diet, }) = _DiaryDayEntity; - + + const DiaryDayEntity._(); + + bool get hasProducts => meals.any((meal) => meal.portions.isNotEmpty); } diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 53beebe0..0bbb4f82 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -50,11 +50,14 @@ class DiaryCubit extends Cubit { Future setSelectedDay(DateTime day) async { final prevDay = selectedDay; + final prevMeal = selectedMeal; selectedDay = day; + selectedMeal = 1; final canSelect = await fetchDiaryV2(needLoading: true); if (!canSelect) { selectedDay = prevDay; + selectedMeal = prevMeal; } return canSelect; @@ -62,34 +65,40 @@ class DiaryCubit extends Cubit { Future fetchDiaryV2({bool needLoading = false, int? diaryId}) async { final currentState = state; - if (needLoading) { - emit(const DiaryState.loading()); - } - if (diaryId != null) { - final diaryDayById = await diaryRepository.getDiaryDay(diaryId); - final diaryEntity = diaryDayById.toEntity(); + try { + if (needLoading) { + emit(const DiaryState.loading()); + } + if (diaryId != null) { + final diaryDayById = await diaryRepository.getDiaryDay(diaryId); + final diaryEntity = diaryDayById.toEntity(); + if (diaryEntity != null) { + setDiary(diaryDay: diaryEntity, diaryId: diaryId); + return true; + } + } + final diaryDay = await getDiary(); + if (diaryDay == null) { + emit(const DiaryState.error(errorMessage: 'Diary not found')); + return true; + } + + final diaryEntity = diaryDay.toEntity(); if (diaryEntity != null) { setDiary(diaryDay: diaryEntity, diaryId: diaryId); return true; } + if (diaryDay.typeOfDiary.hasChosenDiet == true && diaryDay.typeOfDiary.diaryState == null) { + setDiaryForDietOnly(diaryDay.toDiaryFutureDayEntity()); + return true; + } + emit(currentState); + return false; + } catch (e, st) { + log(e.toString(), stackTrace: st); + emit(DiaryState.error(errorMessage: e.toString())); + return false; } - final diaryDay = await getDiary(); - if (diaryDay == null) { - emit(const DiaryState.error(errorMessage: 'Diary not found')); - return true; - } - - final diaryEntity = diaryDay.toEntity(); - if (diaryEntity != null) { - setDiary(diaryDay: diaryEntity, diaryId: diaryId); - return true; - } - if (diaryDay.typeOfDiary.hasChosenDiet == true && diaryDay.typeOfDiary.diaryState == null) { - setDiaryForDietOnly(diaryDay.toDiaryFutureDayEntity()); - return true; - } - emit(currentState); - return false; } Future updateCurrentDiary() async { @@ -109,13 +118,16 @@ class DiaryCubit extends Cubit { await diaryRepository.createDiaryDay(day: selectedDay.toISO8601WithTimeZoneOffset()); final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset()); return diaryDay; - } on DioException catch (e) { - if (e.response?.statusCode == 422) { - final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset()); - return diaryDay; + } catch (e, st) { + log(e.toString(), stackTrace: st); + if (e is DioException) { + if (e.response?.statusCode == 422) { + final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset()); + return diaryDay; + } } + return null; } - return null; } void setSelectedMeal(int meal) { diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart index 607e97d2..c7f832d4 100644 --- a/lib/features/diary/presentation/pages/diary_screen.dart +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -127,6 +127,7 @@ class _DiaryScreenContentState extends State { child: _isDiaryFinishing ? const Center(child: Loader()) : FinishDayButtonWidget( + hasProducts: state.diary.hasProducts, onPressed: () async { setState(() { _isDiaryFinishing = true; diff --git a/lib/features/diary/presentation/widgets/finish_day_button_widget.dart b/lib/features/diary/presentation/widgets/finish_day_button_widget.dart index af58216a..ba13dd96 100644 --- a/lib/features/diary/presentation/widgets/finish_day_button_widget.dart +++ b/lib/features/diary/presentation/widgets/finish_day_button_widget.dart @@ -1,20 +1,89 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:flutter/material.dart'; class FinishDayButtonWidget extends StatefulWidget { final Future Function() onPressed; + final bool hasProducts; - const FinishDayButtonWidget({super.key, required this.onPressed}); + const FinishDayButtonWidget({super.key, required this.onPressed, required this.hasProducts}); @override State createState() => _FinishDayButtonWidgetState(); } class _FinishDayButtonWidgetState extends State { + void _onTap() { + if (!widget.hasProducts) { + showErrorDialog( + context, + title: 'Не добавлено продуктов', + description: 'Добавьте хотя бы один продукт в любой из приёмов пищи, чтобы закончить день.', + ); + return; + } + _showConfirmationBottomSheet(); + } + + void _showConfirmationBottomSheet() { + showBottomSheetWithBlur( + context: context, + height: null, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + const BottomSheetHandlerBar(), + const SizedBox(height: 20), + Text('Закончить день', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 16), + Text( + 'После завершения дня, его уже нельзя будет отредактировать', + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + CustomOutlinedButton( + text: 'Закончить', + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + onPressed: () async { + Navigator.of(context).pop(); + await widget.onPressed(); + }, + ), + const SizedBox(height: 8), + CustomOutlinedButton( + text: 'Отмена', + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), + buttonStyle: const ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(Color(0xFFEAEAEA)), + ), + onPressed: () => Navigator.of(context).pop(), + ), + const SizedBox(height: 40), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { return InkWell( - onTap: widget.onPressed, + onTap: _onTap, borderRadius: BorderRadius.circular(20), child: Ink( padding: const EdgeInsets.symmetric(horizontal: 8), diff --git a/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart b/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart index a48e1fca..286126a6 100644 --- a/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart +++ b/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart @@ -86,9 +86,10 @@ class _GlossaryModerationRequestModalWidgetState extends State getDiaryByDate(String date) async { try { Set ids = {}; - final DiaryDayModel diaryDay = (await getDiaryIndexByDate(date)).last; + final diaryDays = await getDiaryIndexByDate(date); + if (diaryDays.isEmpty) { + throw Exception('No diary entry found for $date'); + } + final DiaryDayModel diaryDay = diaryDays.last; for (var meal in diaryDay.meals) { for (var portion in meal.portions) { if (portion.productNutritionalValueId != null) { diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index ebf116df..408f00c4 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -228,7 +228,9 @@ class _FeedScreenContentState extends State { ), const SizedBox(width: 12), switch (_selectedChipIndex) { - 1 => AddItemButton(onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name)), + 1 => AddItemButton( + onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name) : null, + ), 2 => AddItemButton( onTap: state.profile?.roles.contains('consultant') ?? false ? () async { diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart index 2ab19801..7c9468d8 100644 --- a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart @@ -1,3 +1,5 @@ +import 'package:eatvio_app/core/prefs.dart'; +import 'package:eatvio_app/core/prefs_keys.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; @@ -5,8 +7,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class VerifyEmailCubit extends Cubit { final ProfileRepository profileRepository; + final Prefs prefs; - VerifyEmailCubit({required this.profileRepository}) : super(const VerifyEmailState.loading()); + VerifyEmailCubit({required this.profileRepository, required this.prefs}) : super(const VerifyEmailState.loading()); Future sendEmailCode() async { try { @@ -48,4 +51,9 @@ class VerifyEmailCubit extends Cubit { emit(const VerifyEmailState.changeEmailError()); } } + + void skipVerification() { + prefs.setBool(PrefsKeys.isEmailVerificationSkipped, true); + emit(const VerifyEmailState.skipped()); + } } diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart index 6cb91a32..402a1044 100644 --- a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart @@ -23,4 +23,6 @@ abstract class VerifyEmailState with _$VerifyEmailState { const factory VerifyEmailState.changeEmailSuccess() = VerifyEmailStateChangeEmailSuccess; const factory VerifyEmailState.changeEmailError() = VerifyEmailStateChangeEmailError; + + const factory VerifyEmailState.skipped() = VerifyEmailStateSkipped; } diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index 12fd9287..d95084e9 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -101,6 +101,8 @@ class _VerifyEmailScreenState extends State { // Диалог закроется автоматически в ChangeEmailDialog при успешной смене } else if (state is VerifyEmailStateChangeEmailError) { showErrorToast('Ошибка при смене email. Попробуйте снова.'); + } else if (state is VerifyEmailStateSkipped) { + goRouter.pushNamed('feedScreen'); } }, builder: (context, state) { @@ -163,6 +165,19 @@ class _VerifyEmailScreenState extends State { ); }, ), + const SizedBox(height: 16), + CustomOutlinedButton( + text: 'Позже', + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black900), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.gray.withValues(alpha: 0.1)), + ), + onPressed: () { + context.read().skipVerification(); + }, + ), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 2be3e4c5..9a17e842 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.3.12+93 +version: 1.3.13+94 publish_to: 'none' environment: @@ -43,7 +43,7 @@ dependencies: flutter_keyboard_visibility: ^6.0.0 share_plus: ^12.0.1 flutter_html: ^3.0.0 - file_picker: ^10.3.8 + file_picker: 10.3.8 audioplayers: ^6.5.1 intl: ^0.20.2 mask_text_input_formatter: ^2.9.0 diff --git a/test/core/enums/prefs_keys_test.dart b/test/core/enums/prefs_keys_test.dart index bf3b3771..8e4afe40 100644 --- a/test/core/enums/prefs_keys_test.dart +++ b/test/core/enums/prefs_keys_test.dart @@ -4,11 +4,12 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('PrefsKeys', () { test('has all expected values', () { - expect(PrefsKeys.values.length, 4); + expect(PrefsKeys.values.length, 5); expect(PrefsKeys.values, contains(PrefsKeys.hasAnonymousTokenBeenSent)); expect(PrefsKeys.values, contains(PrefsKeys.welcomeScreenWasShowed)); expect(PrefsKeys.values, contains(PrefsKeys.registrationDate)); expect(PrefsKeys.values, contains(PrefsKeys.marketingBottomSheetHasBeenShown)); + expect(PrefsKeys.values, contains(PrefsKeys.isEmailVerificationSkipped)); }); test('can be compared', () { @@ -21,6 +22,7 @@ void main() { expect(PrefsKeys.welcomeScreenWasShowed.name, 'welcomeScreenWasShowed'); expect(PrefsKeys.registrationDate.name, 'registrationDate'); expect(PrefsKeys.marketingBottomSheetHasBeenShown.name, 'marketingBottomSheetHasBeenShown'); + expect(PrefsKeys.isEmailVerificationSkipped.name, 'isEmailVerificationSkipped'); }); }); } diff --git a/test/core/prefs_keys_test.dart b/test/core/prefs_keys_test.dart index 42aa9ddd..1b063419 100644 --- a/test/core/prefs_keys_test.dart +++ b/test/core/prefs_keys_test.dart @@ -4,11 +4,12 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('PrefsKeys', () { test('все значения enum определены', () { - expect(PrefsKeys.values.length, 4); + expect(PrefsKeys.values.length, 5); expect(PrefsKeys.values, contains(PrefsKeys.hasAnonymousTokenBeenSent)); expect(PrefsKeys.values, contains(PrefsKeys.welcomeScreenWasShowed)); expect(PrefsKeys.values, contains(PrefsKeys.registrationDate)); expect(PrefsKeys.values, contains(PrefsKeys.marketingBottomSheetHasBeenShown)); + expect(PrefsKeys.values, contains(PrefsKeys.isEmailVerificationSkipped)); }); test('имена ключей корректны', () { @@ -16,6 +17,7 @@ void main() { expect(PrefsKeys.welcomeScreenWasShowed.name, 'welcomeScreenWasShowed'); expect(PrefsKeys.registrationDate.name, 'registrationDate'); expect(PrefsKeys.marketingBottomSheetHasBeenShown.name, 'marketingBottomSheetHasBeenShown'); + expect(PrefsKeys.isEmailVerificationSkipped.name, 'isEmailVerificationSkipped'); }); }); } diff --git a/test/features/create_diet/presentation/diet_preview_screen_smoke_test.dart b/test/features/create_diet/presentation/diet_preview_screen_smoke_test.dart index a5466dc9..823b556c 100644 --- a/test/features/create_diet/presentation/diet_preview_screen_smoke_test.dart +++ b/test/features/create_diet/presentation/diet_preview_screen_smoke_test.dart @@ -137,7 +137,6 @@ void main() { expect(find.text('Test Diet Name'), findsOneWidget); expect(find.text('100'), findsOneWidget); // totalViews expect(find.text('10'), findsOneWidget); // totalLikes - expect(find.text('5'), findsOneWidget); // totalComments }); }); }); diff --git a/test/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit_test.dart b/test/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit_test.dart index 9a34746a..241a9a91 100644 --- a/test/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit_test.dart +++ b/test/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit_test.dart @@ -433,6 +433,7 @@ void main() { final tempFile = File('${Directory.systemTemp.path}/test_image.jpg'); await tempFile.writeAsBytes([1, 2, 3]); cubit.replaceImage(tempFile); + cubit.replaceDescriptionInStep('Step 1 description', 0); expect(cubit.validateAll(), true); }); diff --git a/test/features/diary/presentation/widgets/basic_widget_test.dart b/test/features/diary/presentation/widgets/basic_widget_test.dart index 02c97f23..aa9c23f5 100644 --- a/test/features/diary/presentation/widgets/basic_widget_test.dart +++ b/test/features/diary/presentation/widgets/basic_widget_test.dart @@ -220,7 +220,7 @@ void main() { testWidgets('should display FinishDayButtonWidget', (WidgetTester tester) async { // Arrange - final widget = FinishDayButtonWidget(onPressed: () async {}); + final widget = FinishDayButtonWidget(onPressed: () async {}, hasProducts: true); // Act await tester.pumpWidget(createTestWidget(widget)); @@ -234,7 +234,7 @@ void main() { // Arrange diaryCubit.emit(DiaryState.loaded(diary: testDiaryEntity, nutrition: testNutrition, selectedDay: DateTime(2024, 1, 15), selectedMeal: 1)); - final widget = FinishDayButtonWidget(onPressed: () async {}); + final widget = FinishDayButtonWidget(onPressed: () async {}, hasProducts: true); await tester.pumpWidget(createTestWidget(widget)); await tester.pumpAndSettle(); @@ -244,7 +244,8 @@ void main() { await tester.pump(); // Assert - expect(find.text('Закончить день'), findsOneWidget); + // Теперь на экране 2 текста "Закончить день": один на кнопке, другой в заголовке bottom sheet + expect(find.text('Закончить день'), findsNWidgets(2)); }); testWidgets('should display MealWidget with mood rating when showMood is true', (WidgetTester tester) async { diff --git a/test/features/diary/presentation/widgets/finish_day_button_widget_test.dart b/test/features/diary/presentation/widgets/finish_day_button_widget_test.dart index 6eee2102..7b6f29bb 100644 --- a/test/features/diary/presentation/widgets/finish_day_button_widget_test.dart +++ b/test/features/diary/presentation/widgets/finish_day_button_widget_test.dart @@ -13,7 +13,7 @@ void main() { } testWidgets('renders finish day button widget', (tester) async { - await tester.pumpWidget(wrap(FinishDayButtonWidget(onPressed: () async {}))); + await tester.pumpWidget(wrap(FinishDayButtonWidget(onPressed: () async {}, hasProducts: true))); await tester.pump(); expect(find.byType(FinishDayButtonWidget), findsOneWidget); diff --git a/test/features/diary/presentation/widgets/glossary_moderation_request_modal_widget_test.dart b/test/features/diary/presentation/widgets/glossary_moderation_request_modal_widget_test.dart index f77710e0..b1e8534b 100644 --- a/test/features/diary/presentation/widgets/glossary_moderation_request_modal_widget_test.dart +++ b/test/features/diary/presentation/widgets/glossary_moderation_request_modal_widget_test.dart @@ -33,7 +33,7 @@ void main() { expect(find.text('Сохранить'), findsOneWidget); }); - testWidgets('alert -> refuse triggers sendProductToModeration and success', (tester) async { + testWidgets('alert -> refuse triggers sendProductToModeration and closes modal', (tester) async { final cubit = MockDiaryCubit(); when( cubit.sendProductToModeration( @@ -65,9 +65,11 @@ void main() { await tester.pump(); await tester.tap(find.text('Отказаться')); - await tester.pump(); + await tester.pumpAndSettle(); // Wait for navigation animation - expect(find.textContaining('Продукт успешно отправлен'), findsOneWidget); + expect(find.textContaining('Продукт успешно отправлен'), findsNothing); + expect(find.byType(GlossaryModerationRequestModalWidget), findsNothing); + verify( cubit.sendProductToModeration( name: anyNamed('name'), diff --git a/test/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit_test.dart b/test/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit_test.dart index b176fa79..2a78ce73 100644 --- a/test/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit_test.dart +++ b/test/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit_test.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:eatvio_app/core/prefs.dart'; +import 'package:eatvio_app/core/prefs_keys.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; @@ -10,16 +12,18 @@ import 'package:mockito/mockito.dart'; import 'verify_email_cubit_test.mocks.dart'; -@GenerateNiceMocks([MockSpec()]) +@GenerateNiceMocks([MockSpec(), MockSpec()]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('VerifyEmailCubit', () { VerifyEmailCubit? cubit; late MockProfileRepository mockRepository; + late MockPrefs mockPrefs; setUp(() { mockRepository = MockProfileRepository(); + mockPrefs = MockPrefs(); }); tearDown(() async { @@ -30,13 +34,13 @@ void main() { }); test('начальное состояние - loading', () { - cubit = VerifyEmailCubit(profileRepository: mockRepository); + cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs); expect(cubit!.state, isA()); }); test('sendEmailCode - успешная отправка кода', () async { - cubit = VerifyEmailCubit(profileRepository: mockRepository); + cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs); const testProfile = ProfileModel( id: 1, @@ -69,7 +73,7 @@ void main() { }); test('tryVerify - успешная верификация', () async { - cubit = VerifyEmailCubit(profileRepository: mockRepository); + cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs); const testProfile = ProfileModel( id: 1, @@ -111,7 +115,7 @@ void main() { }); test('tryVerify - неверный код', () async { - cubit = VerifyEmailCubit(profileRepository: mockRepository); + cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs); const testProfile = ProfileModel( id: 1, @@ -145,7 +149,7 @@ void main() { }); test('changeEmail - успешная смена email', () async { - cubit = VerifyEmailCubit(profileRepository: mockRepository); + cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs); const testProfile = ProfileModel( id: 1, @@ -175,10 +179,20 @@ void main() { expect(cubit!.state, isA()); final loadedState = cubit!.state as VerifyEmailStateLoaded; expect(loadedState.email, 'new@example.com'); + expect(loadedState.email, 'new@example.com'); + }); + + test('skipVerification - пропуск верификации', () { + cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs); + + cubit!.skipVerification(); + + verify(mockPrefs.setBool(PrefsKeys.isEmailVerificationSkipped, true)).called(1); + expect(cubit!.state, isA()); }); test('sendEmailCode - обработка ошибки', () async { - cubit = VerifyEmailCubit(profileRepository: mockRepository); + cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs); when(mockRepository.getMyProfile()).thenThrow(Exception('Network error')); @@ -190,7 +204,7 @@ void main() { }); test('tryVerify - обработка ошибки при верификации', () async { - cubit = VerifyEmailCubit(profileRepository: mockRepository); + cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs); when(mockRepository.tryVerifyEmailByCode('123456')).thenThrow(Exception('Network error')); @@ -200,7 +214,7 @@ void main() { }); test('changeEmail - обработка ошибки', () async { - cubit = VerifyEmailCubit(profileRepository: mockRepository); + cubit = VerifyEmailCubit(profileRepository: mockRepository, prefs: mockPrefs); when(mockRepository.sendEmailCode(newEmail: 'new@example.com')).thenThrow(Exception('Network error'));