forked from eatvio/eatvio_app
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -54,11 +54,13 @@ jobs:
|
||||
|
||||
- name: Flutter build
|
||||
run: |
|
||||
flutter upgrade
|
||||
flutter clean
|
||||
flutter pub get
|
||||
flutter pub upgrade
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
# Configure Flutter for release mode
|
||||
pod repo update
|
||||
flutter build ios --config-only --release
|
||||
cd ios
|
||||
pod update
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class CubitRegistrations {
|
||||
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl(), profileRepository: sl()));
|
||||
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
|
||||
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
|
||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl(), prefs: sl()));
|
||||
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
|
||||
sl.registerFactory<RegistrationCubitV2>(() => RegistrationCubitV2(registrationRepository: sl()));
|
||||
// Feed related cubits
|
||||
|
||||
@@ -1 +1 @@
|
||||
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown }
|
||||
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown, isEmailVerificationSkipped }
|
||||
|
||||
@@ -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<Prefs>();
|
||||
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<ProfileRepository>();
|
||||
final profile = await profileRepository.getMyProfile();
|
||||
|
||||
final firebaseApi = sl<FirebaseApi>();
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -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<AuthState> {
|
||||
final AuthRepository authRepository;
|
||||
@@ -40,23 +37,10 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
Future<void> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ class SocialAuthFacade {
|
||||
),
|
||||
title: Text(s.authorization),
|
||||
),
|
||||
body: Expanded(child: WebViewWidget(controller: controller)),
|
||||
body: WebViewWidget(controller: controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -105,15 +105,6 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
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<DietPreviewScreen> {
|
||||
},
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -237,7 +237,16 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
|
||||
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<CreateRecipeState> {
|
||||
Future<List<GlossaryModel>> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ abstract class CreateRecipeState with _$CreateRecipeState {
|
||||
String? servingsValidationError,
|
||||
String? energyPerServingValidationError,
|
||||
String? imageValidationError,
|
||||
String? stepsValidationError,
|
||||
}) = _CreateRecipeState;
|
||||
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ class _AddPortionWidgetState extends State<AddPortionWidget> {
|
||||
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<AddPortionWidget> {
|
||||
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 {}
|
||||
|
||||
@@ -75,6 +75,7 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
|
||||
ListTile(
|
||||
key: AppKeys.recipeCreateScreenAddIngredientButton,
|
||||
onTap: () {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
nameController.clear();
|
||||
gramsController.clear();
|
||||
pcsController.clear();
|
||||
|
||||
@@ -55,6 +55,13 @@ class _StepsWidgetState extends State<StepsWidget> {
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -50,11 +50,14 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
|
||||
Future<bool> 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<DiaryState> {
|
||||
|
||||
Future<bool> 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<void> updateCurrentDiary() async {
|
||||
@@ -109,13 +118,16 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
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) {
|
||||
|
||||
@@ -127,6 +127,7 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
|
||||
child: _isDiaryFinishing
|
||||
? const Center(child: Loader())
|
||||
: FinishDayButtonWidget(
|
||||
hasProducts: state.diary.hasProducts,
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
_isDiaryFinishing = true;
|
||||
|
||||
@@ -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<void> Function() onPressed;
|
||||
final bool hasProducts;
|
||||
|
||||
const FinishDayButtonWidget({super.key, required this.onPressed});
|
||||
const FinishDayButtonWidget({super.key, required this.onPressed, required this.hasProducts});
|
||||
|
||||
@override
|
||||
State<FinishDayButtonWidget> createState() => _FinishDayButtonWidgetState();
|
||||
}
|
||||
|
||||
class _FinishDayButtonWidgetState extends State<FinishDayButtonWidget> {
|
||||
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),
|
||||
|
||||
@@ -86,9 +86,10 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
|
||||
fats: 1,
|
||||
carbohydrates: 1,
|
||||
))?.glossary;
|
||||
setState(() {
|
||||
glossaryState = GlossaryState.success;
|
||||
});
|
||||
if (context.mounted) {
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.of(context).pop(glossary);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
|
||||
@@ -21,7 +21,11 @@ class DiaryRepository {
|
||||
Future<DiaryDayModel> getDiaryByDate(String date) async {
|
||||
try {
|
||||
Set<int> 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) {
|
||||
|
||||
@@ -228,7 +228,9 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
),
|
||||
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 {
|
||||
|
||||
@@ -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<VerifyEmailState> {
|
||||
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<void> sendEmailCode() async {
|
||||
try {
|
||||
@@ -48,4 +51,9 @@ class VerifyEmailCubit extends Cubit<VerifyEmailState> {
|
||||
emit(const VerifyEmailState.changeEmailError());
|
||||
}
|
||||
}
|
||||
|
||||
void skipVerification() {
|
||||
prefs.setBool(PrefsKeys.isEmailVerificationSkipped, true);
|
||||
emit(const VerifyEmailState.skipped());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,6 @@ abstract class VerifyEmailState with _$VerifyEmailState {
|
||||
const factory VerifyEmailState.changeEmailSuccess() = VerifyEmailStateChangeEmailSuccess;
|
||||
|
||||
const factory VerifyEmailState.changeEmailError() = VerifyEmailStateChangeEmailError;
|
||||
|
||||
const factory VerifyEmailState.skipped() = VerifyEmailStateSkipped;
|
||||
}
|
||||
|
||||
@@ -101,6 +101,8 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
|
||||
// Диалог закроется автоматически в 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<VerifyEmailScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
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<VerifyEmailCubit>().skipVerification();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
test('can be compared', () {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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<ProfileRepository>()])
|
||||
@GenerateNiceMocks([MockSpec<ProfileRepository>(), MockSpec<Prefs>()])
|
||||
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<VerifyEmailStateLoading>());
|
||||
});
|
||||
|
||||
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<VerifyEmailStateLoaded>());
|
||||
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<VerifyEmailStateSkipped>());
|
||||
});
|
||||
|
||||
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'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user