Merge pull request 'recipes, plans, email and phone verify' (#463) from mso7nueh/eatvio_app:main into main

Reviewed-on: eatvio/eatvio_app#463
This commit is contained in:
mso7nueh
2025-05-28 12:59:21 +00:00
52 changed files with 896 additions and 84 deletions

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ]
env:
flutter_version: "3.29.1"
flutter_version: "3.32.0"
java_version: "21.x"
flutter_channel: "stable"

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test on custom docker
on: [ pull_request ]
env:
flutter_version: "3.29.1"
flutter_version: "3.32.0"
java_version: "21.x"
flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/"

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ]
env:
flutter_version: "3.29.3"
flutter_version: "3.32.0"
java_version: "21.x"
flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/"

File diff suppressed because one or more lines are too long

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip

View File

@@ -20,7 +20,7 @@ plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.7.3" apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false
id "com.google.gms.google-services" version "4.4.2" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "2.0.21" apply false
}

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,2 +1,3 @@
extensions:
- provider: true
- provider: true
- shared_preferences: true

View File

@@ -0,0 +1,32 @@
#
# Generated file, do not edit.
#
import lldb
def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
"""Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
base = frame.register["x0"].GetValueAsAddress()
page_len = frame.register["x1"].GetValueAsUnsigned()
# Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
# first page to see if handled it correctly. This makes diagnosing
# misconfiguration (e.g. missing breakpoint) easier.
data = bytearray(page_len)
data[0:8] = b'IHELPED!'
error = lldb.SBError()
frame.GetThread().GetProcess().WriteMemory(base, data, error)
if not error.Success():
print(f'Failed to write into {base}[+{page_len}]', error)
return
def __lldb_init_module(debugger: lldb.SBDebugger, _):
target = debugger.GetDummyTarget()
# Caveat: must use BreakpointCreateByRegEx here and not
# BreakpointCreateByName. For some reasons callback function does not
# get carried over from dummy target for the later.
bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
bp.SetAutoContinue(True)
print("-- LLDB integration loaded --")

View File

@@ -0,0 +1,5 @@
#
# Generated file, do not edit.
#
command script import --relative-to-command-file flutter_lldb_helper.py

View File

@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 16;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -538,7 +538,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 16;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -576,7 +576,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 16;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>16</string>
<string>18</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -40,9 +40,11 @@ import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
import 'package:eatvio_app/features/recipe/presentation/about_recipe_screen.dart';
import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart';
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart';
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart';
@@ -76,8 +78,11 @@ class AppRouter {
name: 'authScreen',
builder: (context, state) => const AuthScreen(),
routes: [
// GoRoute(path: 'webView-registration-screen', name: 'webViewRegistrationScreen', builder: (context, state) => const WebViewRegistrationScreen()),
GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
GoRoute(
path: 'registration-screen',
name: 'registrationScreen',
builder: (context, state) => const RegistrationScreen(),
),
],
),
ShellRoute(
@@ -201,7 +206,11 @@ class AppRouter {
name: 'aboutRecipesScreen',
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
return AboutRecipeScreen(recipe: data['recipe'] as Recipe, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
return AboutRecipeScreen(
recipe: data['recipe'] as Recipe,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool,
);
},
),
GoRoute(
@@ -355,6 +364,16 @@ class AppRouter {
name: 'verifyScreen',
builder: (context, state) => VerifyScreen(passKey: state.extra as String),
),
GoRoute(
path: '/verify-email-screen',
name: 'verifyEmailScreen',
builder: (context, state) => const VerifyEmailScreen(),
),
GoRoute(
path: MyRecipesScreen.path,
name: MyRecipesScreen.name,
builder: (context, state) => const MyRecipesScreen(),
),
],
);
}

View File

@@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:go_router/go_router.dart';
@@ -13,6 +14,8 @@ class PhoneVerifyErrorInterceptor extends Interceptor {
if (data is Map<String, dynamic> && data['type'] == 'unverificated_phone') {
final String phone = await sl<ProfileRepository>().getMyPhone();
final String passKey = await sl<ProfileRepository>().getPassKey(phone);
await showErrorDialog(AppRouter.navigatorKey.currentContext!,
title: 'Привяжите номер телефона', description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона');
AppRouter.navigatorKey.currentContext?.pushNamed('verifyScreen', extra: passKey);
return handler.reject(
DioException(

View File

@@ -81,9 +81,13 @@ import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recip
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
@@ -138,6 +142,8 @@ Future<void> init() async {
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -165,6 +171,7 @@ Future<void> init() async {
sl.registerLazySingleton<CancelPlanRepository>(() => CancelPlanRepository(cancelService: sl()));
sl.registerLazySingleton<CreatePlanRepository>(() => CreatePlanRepository(createPlanService: sl()));
sl.registerLazySingleton<NotificationsRepository>(() => NotificationsRepository(notificationsService: sl()));
sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -193,6 +200,7 @@ Future<void> init() async {
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
// External
sl.registerLazySingleton(

View File

@@ -6,14 +6,12 @@ import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
@@ -100,37 +98,12 @@ class CustomPopup extends StatelessWidget {
),
);
});
addItem('assets/icons/edit_paint.svg', 'Редактировать', () async {
if (complainedContentType == ComplainedContent.posts) {
GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedBlogPostModel);
} else if (complainedContentType == ComplainedContent.recipe) {
GoRouter.of(context).pushNamed(RecipeCreateScreen.name, extra: recipe);
} else if (complainedContentType == ComplainedContent.diet) {
await showErrorDialog(context,
title: 'Предупреждение', description: 'Диета будет перенесена в архив. Вы создаете новую диету на основе существующей.');
await sl<MyDietsCubit>().unpublishDiet(id!);
GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: diet);
} else {
showInfoToast('В разработке');
return;
}
});
if (complainedContentType == ComplainedContent.plan || complainedContentType == ComplainedContent.diet) {
addItem('assets/icons/remove_from_public.svg', 'Снять с публикации', () async {
if (complainedContentType == ComplainedContent.diet) {
final result = await sl<MyDietsCubit>().unpublishDiet(id!);
if (!result) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
return;
}
showSuccessToast('Диета перенесена в архив, обновите ленту');
} else if (complainedContentType == ComplainedContent.plan) {
final result = await sl<MyPlansCubit>().unpublish(slug!);
if (!result) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
return;
}
showSuccessToast('План перенесен в архив, обновите ленту');
if (complainedContentType == ComplainedContent.posts || complainedContentType == ComplainedContent.recipe) {
addItem('assets/icons/edit_paint.svg', 'Редактировать', () async {
if (complainedContentType == ComplainedContent.posts) {
GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedBlogPostModel);
} else if (complainedContentType == ComplainedContent.recipe) {
GoRouter.of(context).pushNamed(RecipeCreateScreen.name, extra: recipe);
}
});
}
@@ -145,21 +118,21 @@ class CustomPopup extends StatelessWidget {
} else if (complainedContentType == ComplainedContent.recipe) {
final result = await sl<RecipesCubit>().deleteRecipe(slug!);
if (!result) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
showErrorDialog(context, title: 'Не удалось удалить рецепт');
return;
}
showSuccessToast('Рецепт удален, обновите ленту');
} else if (complainedContentType == ComplainedContent.diet) {
final result = await sl<DietsCubit>().deleteDiet(id!);
if (!result) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
showErrorDialog(context, title: 'Не удалось удалить диету');
return;
}
showSuccessToast('Диета удалена, обновите ленту');
} else if (complainedContentType == ComplainedContent.plan) {
final result = await sl<MyPlansCubit>().deletePlan(slug!);
if (!result) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
showErrorDialog(context, title: 'Не удалось удалить план');
return;
}
showSuccessToast('План удален, обновите ленту');

View File

@@ -1,17 +1,19 @@
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:lottie/lottie.dart';
class Loader extends StatelessWidget {
const Loader({super.key});
@override
Widget build(BuildContext context) {
return const Center(
return Center(
child: SizedBox(
height: 32,
width: 32,
child: RiveAnimation.asset(
'assets/rive_animations/loading.riv',
height: 42,
width: 42,
child: Lottie.asset(
'assets/lottie_animations/loading.json',
width: 42,
height: 42,
),
),
);

View File

@@ -186,7 +186,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
CircleAvatar(
radius: 16,
foregroundImage: NetworkImage(
widget.blogPost.author.authorProfileIcon,
baseUrl + widget.blogPost.author.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),

View File

@@ -12,7 +12,11 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
getMyProfile() async {
emit(const ProfileAvatarState.loading());
try {
emit(ProfileAvatarState.loaded(profile: await profileRepository.getMyProfile()));
emit(
ProfileAvatarState.loaded(
profile: await profileRepository.getMyProfile(),
),
);
} catch (e) {
emit(ProfileAvatarState.error(errorMessage: e.toString()));
}

View File

@@ -1,4 +1,5 @@
import 'package:badges/badges.dart' as badges;
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
@@ -81,7 +82,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
BottomNavigationBarItem(
label: '',
icon: CircleAvatar(
foregroundImage: NetworkImage(widget.profileModel.profilePhotoUrl),
foregroundImage: NetworkImage(baseUrl + widget.profileModel.profilePhotoUrl),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 16,
),

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'dart:math';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
@@ -141,7 +142,7 @@ class _ChatScreenState extends State<ChatScreen> {
if (state.chatRoom.users.receiver.profilePhotoUrl != null)
CircleAvatar(
radius: 20,
foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!),
foregroundImage: NetworkImage(baseUrl + state.chatRoom.users.receiver.profilePhotoUrl!),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
if (state.chatRoom.users.receiver.profilePhotoUrl == null)

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
@@ -41,7 +42,7 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
CircleAvatar(
radius: 24,
foregroundImage: NetworkImage(
widget.chatRoom.users.toChatUser.profilePhotoUrl,
baseUrl + widget.chatRoom.users.toChatUser.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
backgroundColor: Colors.white,

View File

@@ -20,7 +20,7 @@ abstract class ConsultantService {
@Part(name: 'photos[]', contentType: 'multipart/form-data') List<MultipartFile>? photos,
});
@GET('/api/consultant_request/{slug}/status')
@GET('/api/profile/consultant/{slug}/status')
Future<ConsultationRequestModel> getConsultantRequestStatus({
@Path() required String slug,
});

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
@@ -36,7 +37,7 @@ class AuthorCard extends StatelessWidget {
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: NetworkImage(author.authorProfileIcon),
image: NetworkImage(baseUrl + author.authorProfileIcon),
fit: BoxFit.cover,
),
),

View File

@@ -16,8 +16,8 @@ abstract class Recipe with _$Recipe {
@JsonKey(name: 'description_md') required String description,
required List<Step>? steps,
@JsonKey(name: 'video_url') required String? videoUrl,
@JsonKey(name: 'created_at') required DateTime createdAt,
@JsonKey(name: 'updated_at') required DateTime updatedAt,
@JsonKey(name: 'created_at') DateTime? createdAt,
@JsonKey(name: 'updated_at') DateTime? updatedAt,
required String? title,
required String? type,
required String slug,

View File

@@ -78,7 +78,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
radius: 20,
backgroundColor: Colors.black12,
foregroundImage: NetworkImage(
state.feedBlogPostList[widget.index].author.authorProfileIcon,
baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
@@ -245,7 +245,9 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}',),
uri: Uri.parse(
'$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}',
),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
@@ -77,7 +78,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
);
},
child: CircleAvatar(
foregroundImage: NetworkImage(comment.authorProfileIcon),
foregroundImage: NetworkImage(baseUrl + comment.authorProfileIcon),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: radius,
),

View File

@@ -66,7 +66,7 @@ class ConsultantWidget extends StatelessWidget {
),
const SizedBox(width: 8),
CircleAvatar(
foregroundImage: NetworkImage(consultant.photo),
foregroundImage: NetworkImage(baseUrl + consultant.photo),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
const SizedBox(width: 8),

View File

@@ -185,7 +185,7 @@ class _PlanWidgetState extends State<PlanWidget> {
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: NetworkImage(widget.plan.author.authorProfileIcon),
image: NetworkImage(baseUrl + widget.plan.author.authorProfileIcon),
fit: BoxFit.cover,
),
),

View File

@@ -8,6 +8,7 @@ import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -28,7 +29,7 @@ class MenuScreen extends StatelessWidget {
_tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push('/recipe-select-screen')),
_tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push(MyRecipesScreen.path)),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)),

View File

@@ -26,6 +26,7 @@ import 'package:share_plus/share_plus.dart';
class MyDietWidget extends StatefulWidget {
final Diet diet;
const MyDietWidget({
super.key,
required this.diet,

View File

@@ -8,7 +8,7 @@ part 'my_plans_service.g.dart';
abstract class MyPlansService {
factory MyPlansService(Dio dio, {String baseUrl}) = _MyPlansService;
@GET('/api/plans/index')
@GET('/api/profile/my_plans')
Future<PlanPaginationModel> getPlans({
@Query('page') required int page,
@Query('filters[name][\$startsWith]') String? query,

View File

@@ -1044,7 +1044,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
tag: state.profile.profilePhotoUrl,
child: CircleAvatar(
radius: 32,
foregroundImage: NetworkImage(state.profile.profilePhotoUrl),
foregroundImage: NetworkImage(baseUrl + state.profile.profilePhotoUrl),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
),

View File

@@ -66,7 +66,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
radius: 20,
backgroundColor: Colors.black12,
foregroundImage: NetworkImage(
state.blogPosts[widget.index].author.authorProfileIcon,
baseUrl + state.blogPosts[widget.index].author.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/profile/models/sub_model.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/sub_action_button_widget.dart';
@@ -35,7 +36,7 @@ class _SubWidgetState extends State<SubWidget> {
child: Row(
children: [
CircleAvatar(
foregroundImage: NetworkImage(widget.subModel.profilePhotoUrl),
foregroundImage: NetworkImage(baseUrl + widget.subModel.profilePhotoUrl),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 28,
),

View File

@@ -183,7 +183,8 @@ class ProfileRepository {
}
Future<String> getPassKey(String phone) async {
return (jsonDecode((await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}'))))['key_is']!;
return (jsonDecode(
(await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}'))))['key_is']!;
}
Future<String> getMyPhone() async {
@@ -194,7 +195,12 @@ class ProfileRepository {
return await profileService.sendEmailCode();
}
Future<void> tryVerifyEmailByCode(String code) async {
return await profileService.tryVerifyEmailByCode(code: "{\"code\": \"$code\"}");
Future<bool> tryVerifyEmailByCode(String code) async {
try {
await profileService.tryVerifyEmailByCode(code: "{\"code\": \"$code\"}");
return true;
} catch (e) {
return false;
}
}
}

View File

@@ -0,0 +1,36 @@
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'my_recipe_model.g.dart';
part 'my_recipe_model.freezed.dart';
@freezed
abstract class MyRecipePaginationModel with _$MyRecipePaginationModel {
const factory MyRecipePaginationModel({
required List<MyRecipeModel> data,
}) = _MyRecipePaginationModel;
factory MyRecipePaginationModel.fromJson(Map<String, Object?> json) => _$MyRecipePaginationModelFromJson(json);
}
@freezed
abstract class MyRecipeModel with _$MyRecipeModel {
const factory MyRecipeModel({
required int id,
required String image,
required String state,
@JsonKey(name: 'cooking_time') required int cookingTime,
@JsonKey(name: 'active_time') required int activeCookingTime,
@JsonKey(name: 'description_md') required String description,
required List<Step>? steps,
@JsonKey(name: 'video_url') required String? videoUrl,
required String? title,
required String slug,
@JsonKey(name: 'energy_per_serving') required double calories,
@JsonKey(name: 'total_likes') required int likesCount,
@JsonKey(name: 'total_views') required int viewsCount,
}) = _MyRecipeModel;
factory MyRecipeModel.fromJson(Map<String, Object?> json) => _$MyRecipeModelFromJson(json);
}

View File

@@ -0,0 +1,56 @@
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart';
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MyRecipesCubit extends Cubit<MyRecipesState> {
final MyRecipesRepository myRecipesRepository;
final AboutRecipeRepository aboutRecipeRepository;
static const String active = 'active';
static const String archived = 'archived';
MyRecipesCubit({
required this.myRecipesRepository,
required this.aboutRecipeRepository,
}) : super(const MyRecipesState.loading());
getMyRecipes() async {
emit(const MyRecipesState.loading());
try {
final myRecipes = await myRecipesRepository.getMyRecipes();
final List<MyRecipeModel> activeRecipes = myRecipes.where((recipe) => recipe.state == active).toList();
emit(MyRecipesState.loaded(recipes: activeRecipes, selectedState: active));
} catch (e) {
emit(const MyRecipesState.error());
}
}
setActiveState() async {
emit(const MyRecipesState.loading());
try {
final myRecipes = await myRecipesRepository.getMyRecipes();
final List<MyRecipeModel> activeRecipes = myRecipes.where((recipe) => recipe.state == active).toList();
emit(MyRecipesState.loaded(recipes: activeRecipes, selectedState: active));
} catch (e) {
emit(const MyRecipesState.error());
}
}
setArchivedState() async {
emit(const MyRecipesState.loading());
try {
final myRecipes = await myRecipesRepository.getMyRecipes();
final List<MyRecipeModel> archivedRecipes = myRecipes.where((recipe) => recipe.state == archived).toList();
emit(MyRecipesState.loaded(recipes: archivedRecipes, selectedState: archived));
} catch (e) {
emit(const MyRecipesState.error());
}
}
Future<Recipe> getRecipeById(String slug) async {
return await aboutRecipeRepository.getRecipe(slug);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'my_recipes_state.freezed.dart';
@freezed
abstract class MyRecipesState with _$MyRecipesState {
const factory MyRecipesState() = _MyRecipesState;
const factory MyRecipesState.loading() = MyRecipesStateLoading;
const factory MyRecipesState.loaded({
required List<MyRecipeModel> recipes,
required String selectedState,
}) = MyRecipesStateLoaded;
const factory MyRecipesState.error() = MyRecipesStateError;
}

View File

@@ -0,0 +1,49 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart';
import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipe_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MyRecipesScreen extends StatelessWidget {
const MyRecipesScreen({super.key});
static const name = 'myRecipesScreen';
static const path = '/my-recipes-screen';
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: appTheme.gray100,
appBar: AppBar(
centerTitle: true,
title: Text(
'Мои рецепты',
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: BlocProvider<MyRecipesCubit>(
create: (BuildContext context) => sl<MyRecipesCubit>()..getMyRecipes(),
child: BlocBuilder<MyRecipesCubit, MyRecipesState>(
builder: (BuildContext context, MyRecipesState state) {
if (state is MyRecipesStateLoaded) {
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 16),
itemBuilder: (context, index) => MyRecipeWidget(recipe: state.recipes[index]),
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemCount: state.recipes.length,
);
}
return Container();
},
),
),
);
}
}

View File

@@ -0,0 +1,242 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class MyRecipeWidget extends StatefulWidget {
final MyRecipeModel recipe;
const MyRecipeWidget({
super.key,
required this.recipe,
});
@override
State<MyRecipeWidget> createState() => _MyRecipeWidgetState();
}
class _MyRecipeWidgetState extends State<MyRecipeWidget> {
late final int hours;
late final int minutes;
late final String timeTitle;
@override
void initState() {
super.initState();
hours = widget.recipe.cookingTime ~/ 60;
minutes = widget.recipe.cookingTime % 60;
if (hours > 0) {
timeTitle = '$hours ${getHourForm(hours)} $minutes ${getMinuteForm(minutes)}';
} else {
timeTitle = '$minutes ${getMinuteForm(minutes)}';
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
InkWell(
onTap: () async => GoRouter.of(context).pushNamed(
'aboutRecipesScreen',
extra: {
'recipe': await context.read<MyRecipesCubit>().getRecipeById(widget.recipe.slug),
'navigateToComments': false,
},
),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.recipe.image}',
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
),
),
Positioned(
left: 8,
bottom: 8,
child: Wrap(
runSpacing: 8,
children: [
_iconButton('assets/icons/clock.svg', timeTitle),
const SizedBox(width: 8),
_iconButton('assets/icons/steps.svg', '${widget.recipe.steps?.length ?? 0} ${getStepForm(widget.recipe.steps?.length ?? 0)}'),
const SizedBox(width: 8),
_iconButton('assets/icons/calories.svg', '${widget.recipe.calories.toStringAsFixed(0)} ккал'),
],
),
),
],
),
const SizedBox(height: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.recipe.title!,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
),
if (widget.recipe.description.isNotEmpty) ...[
const SizedBox(
height: 12,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.recipe.description.length > 200 ? '${widget.recipe.description.substring(0, 200)}...' : widget.recipe.description,
style: {
"*": Style(
margin: Margins.zero,
color: appTheme.black,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
),
},
),
),
],
],
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: CustomOutlinedButton(
onPressed: () async {
GoRouter.of(context).pushNamed(
RecipeCreateScreen.name,
extra: await context.read<MyRecipesCubit>().getRecipeById(widget.recipe.slug),
);
},
text: s.edit,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: CustomOutlinedButton(
onPressed: () async {
final result = await sl<RecipesCubit>().deleteRecipe(widget.recipe.slug);
if (!result) {
showErrorToast('Не удалось удалить рецепт');
return;
}
showSuccessToast('Рецепт удален');
if (context.mounted) context.read<MyRecipesCubit>().getMyRecipes();
},
text: 'Удалить',
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
const SizedBox(width: 8),
Expanded(
child: CustomOutlinedButton(
onPressed: () async {
final box = context.findRenderObject() as RenderBox?;
await SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.slug}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);
},
height: 52,
width: 52,
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
),
text: s.share,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
),
),
],
),
const SizedBox(height: 8),
],
),
),
],
),
);
}
_iconButton(String iconPath, [String? text]) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(16),
),
height: 32,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
iconPath,
),
if (text != null) const SizedBox(width: 8),
if (text != null)
Text(
text,
style: const TextStyle(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,68 @@
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/animated_option_button.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart';
class MyRecipesTab extends StatefulWidget {
const MyRecipesTab({super.key, this.additionalOnTap});
final void Function()? additionalOnTap;
@override
State<MyRecipesTab> createState() => _MyRecipesTabState();
}
class _MyRecipesTabState extends State<MyRecipesTab> {
final List<String> tabTitles = [];
@override
void didChangeDependencies() {
final s = S.of(context);
if (tabTitles.isEmpty) {
tabTitles.add(s.published);
tabTitles.add(s.drafted);
}
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.centerLeft,
child: Row(
children: [
Expanded(
child: Container(
height: 48,
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(100),
),
padding: const EdgeInsets.all(4),
child: Row(
children: [
...List.generate(
2,
(index) =>
Expanded(
child: AnimatedOptionButton(
textStyle: TextStyles.textSMedium,
isSelected: false, // state.selectedStateIndex == index,
text: tabTitles[index],
onTap: () {
widget.additionalOnTap?.call();
// context.read<MyPlansCubit>().changeSelectedStateIndex(index);
},
),
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
class MyRecipesRepository {
final MyRecipesService myRecipesService;
MyRecipesRepository({required this.myRecipesService});
Future<List<MyRecipeModel>> getMyRecipes() async {
try {
return (await myRecipesService.getMyRecipes()).data;
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,13 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
import 'package:retrofit/retrofit.dart';
part 'my_recipes_service.g.dart';
@RestApi()
abstract class MyRecipesService {
factory MyRecipesService(Dio dio, {String baseUrl}) = _MyRecipesService;
@GET('/api/recipes/my')
Future<MyRecipePaginationModel> getMyRecipes();
}

View File

@@ -59,6 +59,7 @@ class RegistrationCubit extends Cubit<RegistrationState> {
);
await TokenStorage.saveTokenInStorage(token);
AppRouter.navigatorKey.currentContext?.go('/feed-screen');
AppRouter.navigatorKey.currentContext?.goNamed('verifyEmailScreen');
} catch (e) {
log(e.toString());
}

View File

@@ -0,0 +1,35 @@
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:flutter_bloc/flutter_bloc.dart';
class VerifyEmailCubit extends Cubit<VerifyEmailState> {
final ProfileRepository profileRepository;
VerifyEmailCubit({
required this.profileRepository,
}) : super(const VerifyEmailState.loading());
sendEmailCode() async {
try {
final profile = await profileRepository.getMyProfile();
await profileRepository.sendEmailCode();
emit(VerifyEmailState.loaded(email: profile.email));
} catch (e) {
emit(const VerifyEmailState.error(errorMessage: 'Ошибка отправки кода.\nПовторите попытку позже'));
}
}
tryVerify(String code) async {
try {
emit(const VerifyEmailState.verifyLoading());
final result = await profileRepository.tryVerifyEmailByCode(code);
if (result == true) {
emit(const VerifyEmailState.verifySuccess());
} else {
emit(const VerifyEmailState.verifyError());
}
} catch (e) {
emit(const VerifyEmailState.verifyError());
}
}
}

View File

@@ -0,0 +1,24 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'verify_email_state.freezed.dart';
@freezed
abstract class VerifyEmailState with _$VerifyEmailState {
const factory VerifyEmailState.loading() = VerifyEmailStateLoading;
const factory VerifyEmailState.loaded({
required String email,
}) = VerifyEmailStateLoaded;
const factory VerifyEmailState.error({
required String errorMessage,
}) = VerifyEmailStateError;
const factory VerifyEmailState() = _VerifyEmailState;
const factory VerifyEmailState.verifyLoading() = VerifyEmailStateVerifyLoading;
const factory VerifyEmailState.verifySuccess() = VerifyEmailStateVerifySuccess;
const factory VerifyEmailState.verifyError() = VerifyEmailStateVerifyError;
}

View File

@@ -0,0 +1,117 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class VerifyEmailScreen extends StatefulWidget {
const VerifyEmailScreen({super.key});
@override
State<VerifyEmailScreen> createState() => _VerifyEmailScreenState();
}
class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
final List<FocusNode> _focusNodes = List.generate(4, (_) => FocusNode());
final List<TextEditingController> _controllers = List.generate(4, (_) => TextEditingController());
String email = '';
@override
void dispose() {
for (var controller in _controllers) {
controller.dispose();
}
for (var node in _focusNodes) {
node.dispose();
}
super.dispose();
}
void _checkAndSubmitCode(BuildContext context) {
final code = _controllers.map((c) => c.text).join();
if (code.length == 4) {
context.read<VerifyEmailCubit>().tryVerify(code);
}
}
@override
Widget build(BuildContext context) {
return BlocProvider<VerifyEmailCubit>(
create: (_) => sl<VerifyEmailCubit>()..sendEmailCode(),
child: BlocConsumer<VerifyEmailCubit, VerifyEmailState>(
listener: (context, state) {
if (state is VerifyEmailStateLoaded) {
setState(() {
email = state.email;
});
} else if (state is VerifyEmailStateVerifySuccess) {
showSuccessToast('Почта успешно подтверждена');
GoRouter.of(context).pop();
} else if (state is VerifyEmailStateVerifyError) {
showErrorToast("Неверный код. Попробуйте снова.");
for (var controller in _controllers) {
controller.clear();
}
FocusScope.of(context).requestFocus(_focusNodes[0]);
}
},
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: const Text("Подтвердите почту")),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Код отправлен на почту\n$email", textAlign: TextAlign.center),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(4, (index) => _buildCodeField(context, index)),
),
const SizedBox(height: 20),
const Text("Запросить код повторно", style: TextStyle(color: Colors.grey)),
TextButton(
onPressed: () {
context.read<VerifyEmailCubit>().sendEmailCode();
},
child: const Text("Не приходит код?"),
),
],
),
),
);
},
),
);
}
Widget _buildCodeField(BuildContext context, int index) {
return SizedBox(
width: 50,
child: TextField(
controller: _controllers[index],
focusNode: _focusNodes[index],
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 1,
style: const TextStyle(fontSize: 24),
decoration: InputDecoration(
counterText: "",
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
),
onChanged: (value) {
if (value.isNotEmpty && index < 3) {
FocusScope.of(context).requestFocus(_focusNodes[index + 1]);
}
if (value.isEmpty && index > 0) {
FocusScope.of(context).requestFocus(_focusNodes[index - 1]);
}
_checkAndSubmitCode(context);
},
),
);
}
}

View File

@@ -1,5 +1,8 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:lottie/lottie.dart';
import 'package:webview_flutter/webview_flutter.dart';
class VerifyScreen extends StatefulWidget {
@@ -22,6 +25,80 @@ class _VerifyScreenState extends State<VerifyScreen> {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) async {
if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) {
GoRouter.of(context).pop();
await showModalBottomSheet(
useRootNavigator: true,
showDragHandle: true,
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Номер телефона\nуспешно привязан',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
height: 1,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
Lottie.asset(
'assets/lottie_animations/success.json',
width: 160,
height: 160,
),
const SizedBox(height: 16),
const Text(
'Теперь Вам доступен весь функционал!',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
height: 1,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 16),
InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: const Center(
child: Text(
'Закрыть',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
),
),
],
),
),
);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse('$baseUrl/login_with_pass_key?id=${widget.passKey}'));
}

View File

@@ -47,7 +47,6 @@ dependencies:
file_picker: ^10.1.9
audioplayers: ^6.4.0
intl: ^0.20.2
rive: ^0.13.20
mask_text_input_formatter: ^2.9.0
lottie: ^3.3.1
flutter_portal: ^1.1.4
@@ -71,9 +70,6 @@ dependencies:
version: ^3.0.2
package_info_plus: ^8.3.0
dependency_overrides:
intl: ^0.19.0
dev_dependencies:
build_runner: ^2.4.15
freezed: ^3.0.6
@@ -83,6 +79,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
@@ -93,7 +90,6 @@ flutter:
- assets/audio/
- assets/icons/
- assets/splash/
- assets/rive_animations/
- assets/lottie_animations/
flutter_intl: