From ff49562e869bf759029bf5ed667e5ca49455b6c1 Mon Sep 17 00:00:00 2001 From: misha Date: Thu, 11 Sep 2025 18:04:35 +0300 Subject: [PATCH 01/49] feat(views): added view handling for diets, recipes, plans and posts --- .gitignore | 3 +- ios/Runner.xcodeproj/project.pbxproj | 3 + lib/core/const.dart | 2 +- lib/features/feed/models/view_model.dart | 16 ++ .../bloc/feed_cubit/feed_cubit.dart | 8 + .../feed/presentation/pages/feed_screen.dart | 102 +++++++++++- .../feed/presentation/widgets/blog_posts.dart | 26 +-- .../feed/presentation/widgets/diets.dart | 26 +-- .../feed/presentation/widgets/plans.dart | 27 ++-- .../feed/presentation/widgets/recipes.dart | 19 ++- .../feed/repositories/feed_repository.dart | 150 ++++-------------- lib/features/feed/services/feed_service.dart | 6 + 12 files changed, 222 insertions(+), 166 deletions(-) create mode 100644 lib/features/feed/models/view_model.dart diff --git a/.gitignore b/.gitignore index 4f05e3a1..06e89a5e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ *.mocks.dart android/build/ ios/XCBuildData/ -env/.env \ No newline at end of file +env/.env +.env \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 198e6f7b..668075b4 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -410,6 +410,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -560,6 +561,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -598,6 +600,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/lib/core/const.dart b/lib/core/const.dart index 98f7bc4a..a5f06149 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,6 +1,6 @@ import 'package:eatvio_app/core/env_config.dart'; -String baseUrl = EnvConfig.baseUrl; +String baseUrl = EnvConfig.devBaseUrl; bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: ''); diff --git a/lib/features/feed/models/view_model.dart b/lib/features/feed/models/view_model.dart new file mode 100644 index 00000000..f04022e9 --- /dev/null +++ b/lib/features/feed/models/view_model.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'view_model.freezed.dart'; +part 'view_model.g.dart'; + +@freezed +abstract class ViewModel with _$ViewModel { + + factory ViewModel({ + required String type, + List? ids, + List? slugs, + }) = _ViewModel; + + factory ViewModel.fromJson(Map json) => _$ViewModelFromJson(json); +} \ No newline at end of file diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index d3ba615a..39ccdc6a 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -1,7 +1,9 @@ import 'dart:developer'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -81,4 +83,10 @@ class FeedCubit extends Cubit { Future viewRecipe(String slug) async => await feedRepository.viewRecipe(slug); Future viewDiet(int id) async => await feedRepository.viewDiet(id); Future viewPlan(String slug) async => await feedRepository.viewPlan(slug); + + Future viewContent({required ViewableContentType type, List? ids, List? slugs}) async { + feedRepository.viewContent( + viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids, slugs: slugs), + ); + } } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index ce29b884..2bfaf897 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/widgets/add_item_button.dart'; @@ -60,18 +62,19 @@ class _FeedScreenContentState extends State { final consultantsController = TextEditingController(); late final List chipsLabelList; + late final AppLifecycleListener _listener; int _selectedChipIndex = 0; final blogPostsScrollController = ScrollController(); final recipesScrollController = ScrollController(); final dietsScrollController = ScrollController(); final plansScrollController = ScrollController(); final consultantsScrollController = ScrollController(); - + final _feedCubit = sl(); @override void initState() { // Initialize chipsLabelList based on hidePlans setting chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; - + _listener = AppLifecycleListener(onDetach: _sendViews); consultantsScrollController.addListener(_onScrollConsultants); super.initState(); } @@ -80,6 +83,21 @@ class _FeedScreenContentState extends State { if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} } + void _sendViews() { + if (postsIds.isNotEmpty) { + _feedCubit.viewContent(type: ViewableContentType.posts, ids: postsIds.toList()); + } + if (plansIds.isNotEmpty) { + _feedCubit.viewContent(type: ViewableContentType.plans, ids: plansIds.toList()); + } + if (dietsIds.isNotEmpty) { + _feedCubit.viewContent(type: ViewableContentType.diets, ids: dietsIds.toList()); + } + if (recipesIds.isNotEmpty) { + _feedCubit.viewContent(type: ViewableContentType.recipes, ids: recipesIds.toList()); + } + } + @override void dispose() { blogPostsScrollController.dispose(); @@ -87,9 +105,64 @@ class _FeedScreenContentState extends State { dietsScrollController.dispose(); plansScrollController.dispose(); consultantsScrollController.dispose(); + for (final map in _visibilityTimers.values) { + for (final t in map.values) { + t.cancel(); + } + map.clear(); + } + _listener.dispose(); + _sendViews(); super.dispose(); } + final Set postsIds = {}; + final Set plansIds = {}; + final Set dietsIds = {}; + final Set recipesIds = {}; + + final Map> _visibilityTimers = { + ViewableContentType.posts: {}, + ViewableContentType.plans: {}, + ViewableContentType.diets: {}, + ViewableContentType.recipes: {}, + }; + + Set _idsForType(ViewableContentType type) { + switch (type) { + case ViewableContentType.posts: + return postsIds; + case ViewableContentType.plans: + return plansIds; + case ViewableContentType.diets: + return dietsIds; + case ViewableContentType.recipes: + return recipesIds; + } + } + + void _handleVisibility({required double visibleFraction, required int id, required ViewableContentType type}) { + final timers = _visibilityTimers[type]!; + final ids = _idsForType(type); + + if (ids.contains(id)) { + timers[id]?.cancel(); + timers.remove(id); + return; + } + + if (visibleFraction >= 0.5) { + if (timers.containsKey(id)) return; + timers[id] = Timer(const Duration(seconds: 1), () { + ids.add(id); + timers.remove(id); + }); + } else { + timers[id]?.cancel(); + timers.remove(id); + } + } + @override Widget build(BuildContext context) { FlutterNativeSplash.remove(); @@ -228,10 +301,10 @@ class _FeedScreenContentState extends State { ]; }, body: switch (_selectedChipIndex) { - 0 => const BlogPosts(), - 1 => const Recipes(), - 2 => const Diets(), - 3 when !hidePlans => const Plans(), + 0 => BlogPosts(handleVisibility: _handleVisibility), + 1 => Recipes(handleVisibility: _handleVisibility), + 2 => Diets(handleVisibility: _handleVisibility), + 3 when !hidePlans => Plans(handleVisibility: _handleVisibility), 3 when hidePlans => const Consultants(), 4 when !hidePlans => const Consultants(), _ => Center(child: Text(chipsLabelList[_selectedChipIndex])), @@ -240,3 +313,20 @@ class _FeedScreenContentState extends State { ); } } + +enum ViewableContentType { + plans, + posts, + diets, + recipes; + + String getStringifiedVersion() => switch (this) { + ViewableContentType.plans => 'plans', + + ViewableContentType.posts => 'blog_posts', + + ViewableContentType.diets => 'diets', + + ViewableContentType.recipes => 'recipes', + }; +} diff --git a/lib/features/feed/presentation/widgets/blog_posts.dart b/lib/features/feed/presentation/widgets/blog_posts.dart index 8c918515..b9da2ecc 100644 --- a/lib/features/feed/presentation/widgets/blog_posts.dart +++ b/lib/features/feed/presentation/widgets/blog_posts.dart @@ -3,13 +3,16 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class BlogPosts extends StatelessWidget { - const BlogPosts({super.key}); - + const BlogPosts({super.key, required this.handleVisibility}); + final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility; + @override Widget build(BuildContext context) { return RefreshIndicator( @@ -29,25 +32,26 @@ class BlogPosts extends StatelessWidget { if (index == state.feedBlogPostList.length) { return const Loader(); } - return BlogPostWidget(index: index); + return VisibilityDetector( + onVisibilityChanged: (info) { + final id = state.feedBlogPostList[index].id; + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + }, + key: ValueKey(index), + child: BlogPostWidget(index: index), + ); }, separatorBuilder: (context, index) => const SizedBox(height: 8), itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length, ), ); } else if (state is FeedStateError) { - return DataErrorWidget( - onReload: () => context.read().loadFeedBlogPostList(), - ); + return DataErrorWidget(onReload: () => context.read().loadFeedBlogPostList()); } else { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); + return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader()); } }, ), ); } } - diff --git a/lib/features/feed/presentation/widgets/diets.dart b/lib/features/feed/presentation/widgets/diets.dart index c08a82a6..c5e5592a 100644 --- a/lib/features/feed/presentation/widgets/diets.dart +++ b/lib/features/feed/presentation/widgets/diets.dart @@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/diets_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class Diets extends StatelessWidget { - const Diets({super.key}); - + const Diets({super.key, required this.handleVisibility}); + final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility; @override Widget build(BuildContext context) { return RefreshIndicator( @@ -19,10 +21,7 @@ class Diets extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, DietsState state) { if (state.isLoading) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); + return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader()); } else if (state.errorMessage != null) { return DataErrorWidget(onReload: () => context.read().loadDiets()); } else { @@ -39,7 +38,14 @@ class Diets extends StatelessWidget { shrinkWrap: true, physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { - return DietsWidget(diet: state.dietsList[index], index: index); + return VisibilityDetector( + key: ValueKey(index), + onVisibilityChanged: (info) { + final id = state.dietsList[index].id; + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + }, + child: DietsWidget(diet: state.dietsList[index], index: index), + ); }, separatorBuilder: (context, index) { return const SizedBox(height: 8); @@ -50,11 +56,7 @@ class Diets extends StatelessWidget { Positioned( right: 0, bottom: 74, - child: FilterBadge( - filterBottomSheet: DietFilterBottomSheet( - dietsCubit: context.read(), - ), - ), + child: FilterBadge(filterBottomSheet: DietFilterBottomSheet(dietsCubit: context.read())), ), ], ); diff --git a/lib/features/feed/presentation/widgets/plans.dart b/lib/features/feed/presentation/widgets/plans.dart index 005c9ee9..4a620945 100644 --- a/lib/features/feed/presentation/widgets/plans.dart +++ b/lib/features/feed/presentation/widgets/plans.dart @@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class Plans extends StatelessWidget { - const Plans({super.key}); - + const Plans({super.key, required this.handleVisibility}); + final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility; @override Widget build(BuildContext context) { return RefreshIndicator( @@ -19,10 +21,7 @@ class Plans extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, PlansState state) { if (state.isLoading) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); + return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader()); } else if (state.errorMessage != null) { return DataErrorWidget(onReload: () => context.read().loadPlans()); } @@ -39,9 +38,13 @@ class Plans extends StatelessWidget { shrinkWrap: true, physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { - return PlanWidget( - plan: state.plans[index], - index: index, + return VisibilityDetector( + key: ValueKey(index), + onVisibilityChanged: (info) { + final id = state.plans[index].id; + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + }, + child: PlanWidget(plan: state.plans[index], index: index), ); }, separatorBuilder: (context, index) { @@ -53,11 +56,7 @@ class Plans extends StatelessWidget { Positioned( right: 0, bottom: 74, - child: FilterBadge( - filterBottomSheet: PlansFilterBottomSheet( - plansCubit: context.read(), - ), - ), + child: FilterBadge(filterBottomSheet: PlansFilterBottomSheet(plansCubit: context.read())), ), ], ); diff --git a/lib/features/feed/presentation/widgets/recipes.dart b/lib/features/feed/presentation/widgets/recipes.dart index 914c31fe..d286949e 100644 --- a/lib/features/feed/presentation/widgets/recipes.dart +++ b/lib/features/feed/presentation/widgets/recipes.dart @@ -3,13 +3,15 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class Recipes extends StatelessWidget { - const Recipes({super.key}); - + const Recipes({super.key, required this.handleVisibility}); + final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility; @override Widget build(BuildContext context) { return RefreshIndicator( @@ -40,9 +42,16 @@ class Recipes extends StatelessWidget { shrinkWrap: true, physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { - return RecipesWidget( - recipe: state.recipesList[index], - index: index, + return VisibilityDetector( + key: ValueKey(index), + onVisibilityChanged: (info) { + final id = state.recipesList[index].id; + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + }, + child: RecipesWidget( + recipe: state.recipesList[index], + index: index, + ), ); }, separatorBuilder: (context, index) { diff --git a/lib/features/feed/repositories/feed_repository.dart b/lib/features/feed/repositories/feed_repository.dart index e6e95969..108a8b11 100644 --- a/lib/features/feed/repositories/feed_repository.dart +++ b/lib/features/feed/repositories/feed_repository.dart @@ -5,103 +5,55 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; import 'package:eatvio_app/features/feed/services/feed_service.dart'; class FeedRepository { final FeedService feedService; - FeedRepository({ - required this.feedService, - }); + FeedRepository({required this.feedService}); - Future getFeedBlogPosts({ - String? query, - int page = 1, - }) async { + Future getFeedBlogPosts({String? query, int page = 1}) async { try { - return (await feedService.getBlogPostList( - page: page, - query: query, - )); + return (await feedService.getBlogPostList(page: page, query: query)); } catch (e) { rethrow; } } - Future getBlogPost({ - required int id - }) async { + Future getBlogPost({required int id}) async { try { - return await feedService.getBlogPost( - postId: id - ); + return await feedService.getBlogPost(postId: id); } catch (e) { rethrow; } } - Future> getRecipes({ - String? query, - List? tags, - int page = 1, - }) async { + Future> getRecipes({String? query, List? tags, int page = 1}) async { try { - return (await feedService.getRecipes( - query: query, - tags: tags, - page: page, - )) - .recipesList; + return (await feedService.getRecipes(query: query, tags: tags, page: page)).recipesList; } catch (e) { rethrow; } } - Future getDiets({ - int page = 1, - String? query, - String? types, - int? duration, - String? sorting, - }) async { + Future getDiets({int page = 1, String? query, String? types, int? duration, String? sorting}) async { try { - return (await feedService.getDiets( - page: page, - query: query, - types: types, - duration: duration, - sorting: sorting, - )); + return (await feedService.getDiets(page: page, query: query, types: types, duration: duration, sorting: sorting)); } catch (e) { rethrow; } } - Future getPlans({ - int page = 1, - String? query, - String? types, - int? duration, - int? minPrice, - int? maxPrice, - }) async { + Future getPlans({int page = 1, String? query, String? types, int? duration, int? minPrice, int? maxPrice}) async { try { - return (await feedService.getPlans( - page: page, - query: query, - types: types, - duration: duration, - minPrice: minPrice, - maxPrice: maxPrice, - )); + return (await feedService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice)); } catch (e) { rethrow; } } - Future getBlogPostComments({ - required String id, - }) async { + Future getBlogPostComments({required String id}) async { try { return (await feedService.getBlogPostComments(id: id)); } catch (e) { @@ -109,25 +61,15 @@ class FeedRepository { } } - Future sendBlogPostComment({ - required String id, - required String commentText, - int? parentCommentId, - }) async { + Future sendBlogPostComment({required String id, required String commentText, int? parentCommentId}) async { try { - await feedService.sendBlogPostComment( - id: id, - body: commentText, - parentCommentId: parentCommentId, - ); + await feedService.sendBlogPostComment(id: id, body: commentText, parentCommentId: parentCommentId); } catch (e) { rethrow; } } - Future getDietsComments({ - required String id, - }) async { + Future getDietsComments({required String id}) async { try { return (await feedService.getDietsComments(id: id)); } catch (e) { @@ -135,25 +77,15 @@ class FeedRepository { } } - Future sendDietsComment({ - required String id, - required String commentText, - int? parentCommentId, - }) async { + Future sendDietsComment({required String id, required String commentText, int? parentCommentId}) async { try { - await feedService.sendDietsComment( - id: id, - body: commentText, - parentCommentId: parentCommentId, - ); + await feedService.sendDietsComment(id: id, body: commentText, parentCommentId: parentCommentId); } catch (e) { rethrow; } } - Future getRecipesComments({ - required String id, - }) async { + Future getRecipesComments({required String id}) async { try { return (await feedService.getRecipesComments(id: id)); } catch (e) { @@ -161,25 +93,15 @@ class FeedRepository { } } - Future sendRecipesComment({ - required String id, - required String commentText, - int? parentCommentId, - }) async { + Future sendRecipesComment({required String id, required String commentText, int? parentCommentId}) async { try { - await feedService.sendRecipesComment( - id: id, - body: commentText, - parentCommentId: parentCommentId, - ); + await feedService.sendRecipesComment(id: id, body: commentText, parentCommentId: parentCommentId); } catch (e) { rethrow; } } - Future getPlansComments({ - required String slug, - }) async { + Future getPlansComments({required String slug}) async { try { return (await feedService.getPlansComments(slug: slug)); } catch (e) { @@ -187,25 +109,15 @@ class FeedRepository { } } - Future sendPlansComment({ - required String slug, - required String commentText, - int? parentCommentId, - }) async { + Future sendPlansComment({required String slug, required String commentText, int? parentCommentId}) async { try { - await feedService.sendPlansComment( - slug: slug, - body: commentText, - parentCommentId: parentCommentId, - ); + await feedService.sendPlansComment(slug: slug, body: commentText, parentCommentId: parentCommentId); } catch (e) { rethrow; } } - Future likeBlogPost({ - required int id, - }) async { + Future likeBlogPost({required int id}) async { try { await feedService.likeBlogPost(id: id); } catch (e) { @@ -213,9 +125,7 @@ class FeedRepository { } } - Future unlikedBlogPost({ - required int id, - }) async { + Future unlikedBlogPost({required int id}) async { try { await feedService.unlikedBlogPost(id: id); } catch (e) { @@ -262,4 +172,12 @@ class FeedRepository { rethrow; } } + + Future viewContent({required ViewModel viewModel}) async { + try { + await feedService.viewContent(viewModel: viewModel); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/feed/services/feed_service.dart b/lib/features/feed/services/feed_service.dart index cb5de3d0..265c2d38 100644 --- a/lib/features/feed/services/feed_service.dart +++ b/lib/features/feed/services/feed_service.dart @@ -6,6 +6,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/recipes_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; import 'package:retrofit/retrofit.dart'; part 'feed_service.g.dart'; @@ -131,4 +132,9 @@ abstract class FeedService { Future viewPlan({ @Path() required String slug, }); + + @POST('/api/views') + Future viewContent({ + @Body() required ViewModel viewModel, + }); } From 2300bc4ab604df35bdb43df748040a64dcea0561 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 12 Sep 2025 16:29:29 +0300 Subject: [PATCH 02/49] fix(consultations): Issue-372 --- .../widgets/consultation_card_widget.dart | 16 ++++++++++++++++ .../widgets/notification_widget.dart | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index 760a8783..3d870e8d 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -3,9 +3,12 @@ import 'package:eatvio_app/core/const.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/widgets/custom_divider.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; +import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; @@ -284,6 +287,19 @@ class _ConsultationCardWidgetState extends State { ), ], ), + // Кнопка получения выплаты + if (widget.consultationsModel.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[ + const SizedBox(height: 16), + CustomOutlinedButton( + text: s.collectCharge, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), + onPressed: () async { + GoRouter.of(context).push(CardSettingsScreen.path, extra: widget.consultationsModel.id); + }, + ), + ], ], ), ), diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index ed196b00..6b675382 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -307,7 +307,7 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget { buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), onPressed: () { - GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId); + GoRouter.of(context).push(CardSettingsScreen.path, extra: notification.planId); }, ), ], From 1a6fb39d846c636f64b5bb1386b233cea182d265 Mon Sep 17 00:00:00 2001 From: misha Date: Mon, 15 Sep 2025 14:12:05 +0300 Subject: [PATCH 03/49] gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4f05e3a1..06e89a5e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ *.mocks.dart android/build/ ios/XCBuildData/ -env/.env \ No newline at end of file +env/.env +.env \ No newline at end of file From 41f77bd373375eb5e6ab1985a3ae7ade26eb2c6e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 16 Sep 2025 09:28:17 +0300 Subject: [PATCH 04/49] feat(;auth): social --- .../auth/models/social_exchange_request.dart | 11 ++ .../bloc/auth_cubit/auth_cubit.dart | 34 +++-- .../auth/presentation/pages/auth_screen.dart | 130 ++++++++++++++++++ .../auth/repositories/auth_repository.dart | 14 ++ lib/features/auth/sdk/social_auth_facade.dart | 24 ++++ lib/features/auth/services/auth_service.dart | 6 + 6 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 lib/features/auth/models/social_exchange_request.dart create mode 100644 lib/features/auth/sdk/social_auth_facade.dart diff --git a/lib/features/auth/models/social_exchange_request.dart b/lib/features/auth/models/social_exchange_request.dart new file mode 100644 index 00000000..85bf7330 --- /dev/null +++ b/lib/features/auth/models/social_exchange_request.dart @@ -0,0 +1,11 @@ +class SocialExchangeRequest { + final String provider; + final String accessToken; + + SocialExchangeRequest({required this.provider, required this.accessToken}); + + Map toJson() { + return {'provider': provider, 'access_token': accessToken}; + } +} + 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 20686cce..7b58a4b0 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -6,28 +6,17 @@ import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - class AuthCubit extends Cubit { final AuthRepository authRepository; - AuthCubit({ - required this.authRepository, - }) : super(const AuthState()); + AuthCubit({required this.authRepository}) : super(const AuthState()); Future getTokenByCredentials(String email, String password) async { emit(const AuthState.loading()); try { - final token = (await authRepository.getTokenByCredentials( - email, - password, - )) - .token; - emit( - AuthState.loaded( - token: token, - ), - ); - await TokenStorage.saveTokenInStorage(token); + final token = (await authRepository.getTokenByCredentials(email, password)).token; + emit(AuthState.loaded(token: token)); + await TokenStorage.saveTokenInStorage(token); return true; } catch (e) { log(e.toString()); @@ -36,4 +25,19 @@ class AuthCubit extends Cubit { return false; } } + + Future getTokenBySocial({required String provider, required String accessToken}) async { + emit(const AuthState.loading()); + try { + final token = (await authRepository.getTokenBySocial(provider: provider, accessToken: accessToken)).token; + emit(AuthState.loaded(token: token)); + await TokenStorage.saveTokenInStorage(token); + return true; + } catch (e) { + log(e.toString()); + showErrorToast('Не удалось войти через $provider'); + emit(AuthState.error(errorMessage: '$e')); + return false; + } + } } diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 73162bd2..61976300 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -24,6 +24,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:eatvio_app/features/auth/sdk/social_auth_facade.dart'; class AuthScreen extends StatefulWidget { const AuthScreen({super.key}); @@ -165,6 +166,52 @@ class _AuthScreenState extends State { ), ], ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _SocialButton( + onTap: _onVkPressed, + child: SvgPicture.asset( + 'assets/icons/vk.svg', + height: 24, + width: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + backgroundColor: const Color(0xFF2787F5), + tooltip: 'VK ID', + ), + _SocialButton( + onTap: _onOkPressed, + child: SvgPicture.asset( + 'assets/icons/ok.svg', + height: 24, + width: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + backgroundColor: const Color(0xFFF58220), + tooltip: 'Одноклассники', + ), + _SocialButton( + onTap: _onMailRuPressed, + child: const Text( + 'Mail', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), + ), + backgroundColor: const Color(0xFF005FF9), + tooltip: 'Mail.ru', + ), + _SocialButton( + onTap: _onYandexPressed, + child: const Text( + 'Yandex', + style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), + ), + backgroundColor: const Color(0xFFFFCC00), + tooltip: 'Яндекс ID', + ), + ], + ), const Spacer(), Center( child: RichText( @@ -317,3 +364,86 @@ class _AuthScreenState extends State { ); } } + +class _SocialButton extends StatelessWidget { + const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip}); + + final VoidCallback onTap; + final Widget child; + final Color backgroundColor; + final String tooltip; + + @override + Widget build(BuildContext context) { + return Tooltip( + message: tooltip, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: onTap, + child: Ink( + height: 48, + width: (MediaQuery.of(context).size.width - 16 * 2 - 12 * 3) / 4, + decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(12)), + child: Center(child: child), + ), + ), + ); + } +} + +void _onVkPressed() async { + final accessToken = await SocialAuthFacade.signInWithVk(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); + if (isAuthenticated) { + if (AppRouter.navigatorKey.currentContext != null) { + // Reuse success flow + // ignore: use_build_context_synchronously + await _reconnectAfterAuth(); + } + } +} + +void _onOkPressed() async { + final accessToken = await SocialAuthFacade.signInWithOk(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } +} + +void _onMailRuPressed() async { + final accessToken = await SocialAuthFacade.signInWithMailRu(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } +} + +void _onYandexPressed() async { + final accessToken = await SocialAuthFacade.signInWithYandex(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'yandex', accessToken: accessToken); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } +} + +Future _reconnectAfterAuth() async { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + try { + await sl().clear(); + await sl().initialize(); + await sl().getMyProfile(); + await sl().getChatRoomsWithLoading(); + await sl().connectToWebSocket(); + await sl().getNotifications(); + await sl().connectToWebSocket(); + await sl().registerPushToken(); + showSuccessToast('Успешный вход'); + } catch (_) {} + AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); +} diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index d0bfcce5..84df92f6 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/features/auth/models/credentials_model.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; +import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; class AuthRepository { final AuthService authService; @@ -22,4 +23,17 @@ class AuthRepository { rethrow; } } + + Future getTokenBySocial({ + required String provider, + required String accessToken, + }) async { + try { + return await authService.getTokenBySocial( + body: SocialExchangeRequest(provider: provider, accessToken: accessToken), + ); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart new file mode 100644 index 00000000..975f4196 --- /dev/null +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -0,0 +1,24 @@ +import 'dart:async'; + +class SocialAuthFacade { + static Future signInWithVk() async { + // TODO: Integrate official VK ID SDK and return access token + return null; + } + + static Future signInWithOk() async { + // TODO: Integrate official OK SDK and return access token + return null; + } + + static Future signInWithMailRu() async { + // TODO: Integrate official Mail.ru SDK and return access token + return null; + } + + static Future signInWithYandex() async { + // TODO: Integrate official Yandex ID SDK and return access token + return null; + } +} + diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 62256f8b..7dabb65b 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/features/auth/models/credentials_model.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; +import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; import 'package:retrofit/retrofit.dart'; part 'auth_service.g.dart'; @@ -13,4 +14,9 @@ abstract class AuthService { Future getTokenByCredentials({ @Body() required CredentialsModel credentials, }); + + @POST('/api/social/login') + Future getTokenBySocial({ + @Body() required SocialExchangeRequest body, + }); } From b146295996fbac0298f54ebe2d36f189880e4119 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 16 Sep 2025 19:00:53 +0300 Subject: [PATCH 05/49] fix(profile): Issue-582 --- .../auth/models/social_exchange_request.dart | 1 + lib/features/auth/sdk/social_auth_facade.dart | 1 + .../bloc/profile_cubit/profile_cubit.dart | 39 +++++-------------- 3 files changed, 12 insertions(+), 29 deletions(-) diff --git a/lib/features/auth/models/social_exchange_request.dart b/lib/features/auth/models/social_exchange_request.dart index 85bf7330..c322fab6 100644 --- a/lib/features/auth/models/social_exchange_request.dart +++ b/lib/features/auth/models/social_exchange_request.dart @@ -9,3 +9,4 @@ class SocialExchangeRequest { } } + diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 975f4196..62985d5d 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -22,3 +22,4 @@ class SocialAuthFacade { } } + diff --git a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart index d0d7a932..5ad4a948 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -18,11 +18,7 @@ class ProfileCubit extends Cubit { final ConsultantRepository consultantRepository; String _currentPeriod = '1'; - ProfileCubit({ - required this.profileRepository, - required this.chatRepository, - required this.consultantRepository, - }) : super(const ProfileState.loading()); + ProfileCubit({required this.profileRepository, required this.chatRepository, required this.consultantRepository}) : super(const ProfileState.loading()); Future getProfileBySlug(String slug) async { emit(const ProfileState.loading()); @@ -42,9 +38,7 @@ class ProfileCubit extends Cubit { } } - Future getMyProfile({ - String period = '1', - }) async { + Future getMyProfile({String period = '1'}) async { emit(const ProfileState.loading()); _currentPeriod = period; try { @@ -77,15 +71,14 @@ class ProfileCubit extends Cubit { profile: (state as ProfileStateLoaded).profile, profileDetails: (state as ProfileStateLoaded).profileDetails, blogPosts: feedBlogPostList, + weightPoints: (state as ProfileStateLoaded).weightPoints, + circumferencePoints: (state as ProfileStateLoaded).circumferencePoints, + consultantInfo: (state as ProfileStateLoaded).consultantInfo, ), ); } - Future addCircumference({ - required int chestCircumference, - required int waistCircumference, - required int hipCircumference, - }) async { + Future addCircumference({required int chestCircumference, required int waistCircumference, required int hipCircumference}) async { try { await profileRepository.addCircumference( chestCircumference: chestCircumference, @@ -98,9 +91,7 @@ class ProfileCubit extends Cubit { } } - Future addWeight({ - required double weight, - }) async { + Future addWeight({required double weight}) async { try { await profileRepository.addWeight(weight: weight); await getMyProfile(period: _currentPeriod); @@ -109,25 +100,15 @@ class ProfileCubit extends Cubit { } } - Future getChatRoomIdBySlug({ - required Author author, - }) async { + Future getChatRoomIdBySlug({required Author author}) async { try { final chatIdOrError = await chatRepository.getChatByUserSlug(author.authorSlug); if (chatIdOrError == '404') { - AppRouter.navigatorKey.currentContext?.pushNamed( - 'emptyChatScreen', - extra: author, - ); + AppRouter.navigatorKey.currentContext?.pushNamed('emptyChatScreen', extra: author); } else if (chatIdOrError == 'error') { showErrorToast('Ошибка создания чата'); } else { - AppRouter.navigatorKey.currentContext?.pushNamed( - 'chatScreen', - extra: { - 'uuid': chatIdOrError, - }, - ); + AppRouter.navigatorKey.currentContext?.pushNamed('chatScreen', extra: {'uuid': chatIdOrError}); return chatIdOrError; } } catch (e, st) { From c295c0656525e16cdbc1fa6f5cedeeb5fe5df458 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 16 Sep 2025 19:09:41 +0300 Subject: [PATCH 06/49] fix(my_plans): Issue-189 --- .../auth/presentation/pages/auth_screen.dart | 24 +-- .../presentation/my_plans_screen.dart | 41 +--- .../presentation/widgets/my_plan_widget.dart | 188 +++++------------- .../presentation/widgets/my_plans_tab.dart | 72 +------ .../presentation/pages/profile_screen.dart | 2 - 5 files changed, 77 insertions(+), 250 deletions(-) diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 61976300..c32b8a39 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -172,43 +172,43 @@ class _AuthScreenState extends State { children: [ _SocialButton( onTap: _onVkPressed, + backgroundColor: const Color(0xFF2787F5), + tooltip: 'VK ID', child: SvgPicture.asset( 'assets/icons/vk.svg', height: 24, width: 24, colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), ), - backgroundColor: const Color(0xFF2787F5), - tooltip: 'VK ID', ), _SocialButton( onTap: _onOkPressed, + backgroundColor: const Color(0xFFF58220), + tooltip: 'Одноклассники', child: SvgPicture.asset( 'assets/icons/ok.svg', height: 24, width: 24, colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), ), - backgroundColor: const Color(0xFFF58220), - tooltip: 'Одноклассники', ), - _SocialButton( + const _SocialButton( onTap: _onMailRuPressed, - child: const Text( + backgroundColor: Color(0xFF005FF9), + tooltip: 'Mail.ru', + child: Text( 'Mail', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), ), - backgroundColor: const Color(0xFF005FF9), - tooltip: 'Mail.ru', ), - _SocialButton( + const _SocialButton( onTap: _onYandexPressed, - child: const Text( + backgroundColor: Color(0xFFFFCC00), + tooltip: 'Яндекс ID', + child: Text( 'Yandex', style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), ), - backgroundColor: const Color(0xFFFFCC00), - tooltip: 'Яндекс ID', ), ], ), diff --git a/lib/features/my_plans/presentation/my_plans_screen.dart b/lib/features/my_plans/presentation/my_plans_screen.dart index 3ca9ca3e..f7bdb456 100644 --- a/lib/features/my_plans/presentation/my_plans_screen.dart +++ b/lib/features/my_plans/presentation/my_plans_screen.dart @@ -12,7 +12,6 @@ import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_botto import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plan_widget.dart'; -import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plans_tab.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -64,9 +63,7 @@ class MyPlansScreenContent extends StatelessWidget { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))), backgroundColor: appTheme.gray100, foregroundColor: appTheme.gray100, automaticallyImplyLeading: false, @@ -76,21 +73,12 @@ class MyPlansScreenContent extends StatelessWidget { snap: true, scrolledUnderElevation: 0, bottom: PreferredSize( - preferredSize: Size( - MediaQuery.sizeOf(context).width, - 64, - ), + preferredSize: Size(MediaQuery.sizeOf(context).width, 32), child: Padding( padding: const EdgeInsets.only(bottom: 0), child: Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 8), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 4), - child: MyPlansTab(), - ), - const SizedBox(height: 8), Row( children: [ const SizedBox(width: 4), @@ -104,20 +92,14 @@ class MyPlansScreenContent extends StatelessWidget { _ => activePlansSearchController, }, onChanged: (query) { - EasyDebounce.debounce( - 'searchPlans', - const Duration(milliseconds: 500), - () { - context.read().loadPlans(query: query == '' ? null : query); - }, - ); + EasyDebounce.debounce('searchPlans', const Duration(milliseconds: 500), () { + context.read().loadPlans(query: query == '' ? null : query); + }); }, ), ), const SizedBox(width: 12), - AddItemButton( - onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name), - ), + AddItemButton(onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name)), const SizedBox(width: 4), ], ), @@ -134,10 +116,7 @@ class MyPlansScreenContent extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, MyPlansState state) { if (state.isLoading) { - return Scaffold( - backgroundColor: Colors.grey.shade200, - body: const Loader(), - ); + return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader()); } else if (state.error != null) { return DataErrorWidget(onReload: () => context.read().loadPlans()); } else { @@ -162,11 +141,7 @@ class MyPlansScreenContent extends StatelessWidget { Positioned( right: 0, bottom: 74, - child: FilterBadge( - filterBottomSheet: PlansFilterBottomSheet( - myPlansCubit: context.read(), - ), - ), + child: FilterBadge(filterBottomSheet: PlansFilterBottomSheet(myPlansCubit: context.read())), ), ], ); diff --git a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart index ce91acdc..7ecdc653 100644 --- a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart +++ b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart @@ -14,16 +14,13 @@ 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/svg.dart'; +// Removed SVG icon usage after switching to text buttons import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; class MyPlanWidget extends StatefulWidget { final PlanModel plan; - const MyPlanWidget({ - super.key, - required this.plan, - }); + const MyPlanWidget({super.key, required this.plan}); @override State createState() => _MyPlanWidgetState(); @@ -34,10 +31,7 @@ class _MyPlanWidgetState extends State { Widget build(BuildContext context) { final s = S.of(context); return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -48,23 +42,14 @@ class _MyPlanWidgetState extends State { onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'navigateToComments': false}), child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), - child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.plan.image}', - fit: BoxFit.fitWidth, - width: MediaQuery.of(context).size.width, - ), + child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width), ), ), Positioned( bottom: 16, child: SizedBox( width: MediaQuery.sizeOf(context).width, - child: InfoRow( - plan: widget.plan, - itemsBackgroundColor: appTheme.whiteA700, - disableDescriptionOfTypes: true, - type: ProductType.plan, - ), + child: InfoRow(plan: widget.plan, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.plan), ), ), ], @@ -75,19 +60,16 @@ class _MyPlanWidgetState extends State { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - widget.plan.name, - style: TextStyles.textMSemibold.copyWith(color: appTheme.black), - ), + child: Text(widget.plan.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), ), if (widget.plan.details.isNotEmpty) ...[ - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Html( - data: widget.plan.details.length > 200 ? '${widget.plan.details.substring(0, 200).replaceAll('\n', '
')}...' : widget.plan.details.replaceAll('\n', '
'), + data: widget.plan.details.length > 200 + ? '${widget.plan.details.substring(0, 200).replaceAll('\n', '
')}...' + : widget.plan.details.replaceAll('\n', '
'), style: { "*": Style( margin: Margins.zero, @@ -116,26 +98,61 @@ class _MyPlanWidgetState extends State { padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ + // Buttons aligned with MyRecipeWidget: Delete (gray) and Share (blue) Row( children: [ Expanded( child: CustomOutlinedButton( - text: switch (state.selectedStateIndex) { - 1 => s.publish, - _ => s.edit, + onPressed: () async { + final result = await context.read().deletePlan(widget.plan.slug); + if (result) { + Future.delayed(const Duration(seconds: 3), () { + if (!context.mounted) return; + showBottomSheetWithBlur( + context: context, + child: Column( + children: [ + const SizedBox(height: 20), + const BottomSheetHandlerBar(), + const SizedBox(height: 20), + Text(widget.plan.name, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + Text(s.deleted, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ); + }); + } }, + text: 'Удалить', height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), buttonStyle: ButtonStyle( shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), side: const WidgetStatePropertyAll(BorderSide.none), ), ), ), - if (state.selectedStateIndex == 0) ...[ - const SizedBox(width: 6), - CustomOutlinedButton( + const SizedBox(width: 8), + Expanded( + child: CustomOutlinedButton( onPressed: () async { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( @@ -146,109 +163,14 @@ class _MyPlanWidgetState extends State { ); }, height: 52, - width: 52, - leftIcon: SvgPicture.asset( - 'assets/icons/share2.svg', - width: 20, - height: 20, - colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn), - ), - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), buttonStyle: ButtonStyle( shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), side: const WidgetStatePropertyAll(BorderSide.none), ), - text: '', + text: s.share, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), ), - ], - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: CustomOutlinedButton( - text: switch (state.selectedStateIndex) { - 0 => s.removeFromActive, - _ => s.edit, - }, - 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: 6), - CustomOutlinedButton( - onPressed: () async { - final result = await context.read().deletePlan(widget.plan.slug); - if (result) { - Future.delayed(const Duration(seconds: 3), () { - if (!context.mounted) return; - showBottomSheetWithBlur( - context: context, - child: Column( - children: [ - const SizedBox( - height: 20, - ), - const BottomSheetHandlerBar(), - const SizedBox( - height: 20, - ), - Text( - widget.plan.name, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - Text( - s.deleted, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - const SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: CustomOutlinedButton( - onPressed: () { - context.pop(); - }, - text: 'Хорошо', - height: 52, - buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: const WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - ), - ), - ), - const SizedBox( - height: 20, - ), - ], - ), - ); - }); - } - }, - height: 52, - width: 52, - leftIcon: SvgPicture.asset( - 'assets/icons/trash_can.svg', - width: 20, - height: 20, - ), - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), - buttonStyle: ButtonStyle( - shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), - backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), - side: const WidgetStatePropertyAll(BorderSide.none), - ), - text: '', ), ], ), diff --git a/lib/features/my_plans/presentation/widgets/my_plans_tab.dart b/lib/features/my_plans/presentation/widgets/my_plans_tab.dart index c681efd1..2635bef1 100644 --- a/lib/features/my_plans/presentation/widgets/my_plans_tab.dart +++ b/lib/features/my_plans/presentation/widgets/my_plans_tab.dart @@ -1,71 +1,3 @@ -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/features/my_plans/presentation/cubit/my_plans_cubit.dart'; -import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; -import 'package:eatvio_app/generated/l10n.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +// File intentionally left minimal after removing tabs for My Plans -class MyPlansTab extends StatefulWidget { - const MyPlansTab({super.key, this.additionalOnTap}); - final void Function()? additionalOnTap; - @override - State createState() => _MyPlansTabState(); -} - -class _MyPlansTabState extends State { - final List 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 BlocBuilder( - builder: (context, state) { - 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: state.selectedStateIndex == index, - text: tabTitles[index], - onTap: () { - widget.additionalOnTap?.call(); - context.read().changeSelectedStateIndex(index); - }, - ), - ), - ), - ], - ), - ), - ), - ], - ), - ); - }, - ); - } -} +// Drafts/published tab UI removed per requirements diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index cc8f7245..98966234 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,6 +1,4 @@ -import 'dart:developer'; import 'dart:io'; - import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/app_router.dart'; From 087436da1ccbb89d23fa4f16dea3ee789bedc2b1 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 17 Sep 2025 15:13:16 +0300 Subject: [PATCH 07/49] changed logic of view requests sending --- lib/core/const.dart | 2 +- lib/core/locator_service.dart | 6 + lib/core/views/cubit/views_cubit.dart | 128 ++++++++++++++++++ lib/core/views/cubit/views_state.dart | 3 + .../views/repository/views_repository.dart | 16 +++ lib/core/views/service/views_service.dart | 15 ++ .../bloc/feed_cubit/feed_cubit.dart | 7 - .../feed/presentation/pages/feed_screen.dart | 83 +----------- .../feed/presentation/widgets/diets.dart | 2 +- .../feed/presentation/widgets/plans.dart | 2 +- .../feed/presentation/widgets/recipes.dart | 2 +- .../presentation/pages/profile_screen.dart | 1 - lib/main.dart | 6 +- 13 files changed, 183 insertions(+), 90 deletions(-) create mode 100644 lib/core/views/cubit/views_cubit.dart create mode 100644 lib/core/views/cubit/views_state.dart create mode 100644 lib/core/views/repository/views_repository.dart create mode 100644 lib/core/views/service/views_service.dart diff --git a/lib/core/const.dart b/lib/core/const.dart index a5f06149..98f7bc4a 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,6 +1,6 @@ import 'package:eatvio_app/core/env_config.dart'; -String baseUrl = EnvConfig.devBaseUrl; +String baseUrl = EnvConfig.baseUrl; bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: ''); diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index e4a13866..37713f29 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -5,6 +5,9 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; +import 'package:eatvio_app/core/views/repository/views_repository.dart'; +import 'package:eatvio_app/core/views/service/views_service.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; @@ -179,6 +182,7 @@ Future init() async { sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); sl.registerFactory(() => ConsultantRatingCubit(sl())); + sl.registerFactory(() => ViewsCubit(viewsRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -212,6 +216,7 @@ Future init() async { sl.registerLazySingleton(() => CommentsRepository(service: sl())); sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); sl.registerFactory(() => ConsultantRatingRepository(sl())); + sl.registerFactory(() => ViewsRepository(viewsService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -247,6 +252,7 @@ Future init() async { sl.registerLazySingleton(() => MyRecipesService(sl())); sl.registerLazySingleton(() => ConsultantRatingService(sl())); sl.registerLazySingleton(() => NotificationSettingsService(sl())); + sl.registerLazySingleton(() => ViewsService(sl())); // External sl.registerLazySingleton( diff --git a/lib/core/views/cubit/views_cubit.dart b/lib/core/views/cubit/views_cubit.dart new file mode 100644 index 00000000..ee15af22 --- /dev/null +++ b/lib/core/views/cubit/views_cubit.dart @@ -0,0 +1,128 @@ +import 'dart:async'; +import 'package:eatvio_app/core/views/cubit/views_state.dart'; +import 'package:eatvio_app/core/views/repository/views_repository.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ViewsCubit extends Cubit { + final ViewsRepository viewsRepository; + ViewsCubit({required this.viewsRepository}) : super(ViewsCubitState()) { + _listener = AppLifecycleListener(onPause: _sendViews, onInactive: _sendViews); + } + + late final AppLifecycleListener _listener; + + final Set postsIds = {}; + final Set plansIds = {}; + final Set dietsIds = {}; + final Set recipesIds = {}; + + final Set _sentPosts = {}; + final Set _sentPlans = {}; + final Set _sentDiets = {}; + final Set _sentRecipes = {}; + + final Map> _visibilityTimers = { + ViewableContentType.posts: {}, + ViewableContentType.plans: {}, + ViewableContentType.diets: {}, + ViewableContentType.recipes: {}, + }; + + Timer? _debounceTimer; + + Set _idsForType(ViewableContentType type) { + switch (type) { + case ViewableContentType.posts: + return postsIds; + case ViewableContentType.plans: + return plansIds; + case ViewableContentType.diets: + return dietsIds; + case ViewableContentType.recipes: + return recipesIds; + } + } + + Set _sentIdsForType(ViewableContentType type) { + switch (type) { + case ViewableContentType.posts: + return _sentPosts; + case ViewableContentType.plans: + return _sentPlans; + case ViewableContentType.diets: + return _sentDiets; + case ViewableContentType.recipes: + return _sentRecipes; + } + } + + void handleVisibility({required double visibleFraction, required int id, required ViewableContentType type}) { + final timers = _visibilityTimers[type]!; + final ids = _idsForType(type); + final sent = _sentIdsForType(type); + + if (sent.contains(id)) { + timers[id]?.cancel(); + timers.remove(id); + return; + } + + if (ids.contains(id)) return; + + if (visibleFraction >= 0.5) { + if (timers.containsKey(id)) return; + timers[id] = Timer(const Duration(seconds: 1), () { + ids.add(id); + timers.remove(id); + _scheduleDebouncedSend(); + }); + } else { + timers[id]?.cancel(); + timers.remove(id); + } + } + + Future _viewContent({required ViewableContentType type, required List ids}) async { + await viewsRepository.viewContent( + viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids), + ); + } + + void _scheduleDebouncedSend() { + _debounceTimer?.cancel(); + _debounceTimer = Timer(const Duration(seconds: 4), _sendViews); + } + + void _send(Set ids, Set sent, ViewableContentType type) { + final newIds = ids.difference(sent); + if (newIds.isNotEmpty) { + _viewContent(type: type, ids: newIds.toList()); + sent.addAll(newIds); + ids.removeAll(newIds); + } + } + + void _sendViews() { + _send(postsIds, _sentPosts, ViewableContentType.posts); + _send(plansIds, _sentPlans, ViewableContentType.plans); + _send(dietsIds, _sentDiets, ViewableContentType.diets); + _send(recipesIds, _sentRecipes, ViewableContentType.recipes); + } + + @override + Future close() async { + for (final map in _visibilityTimers.values) { + for (final t in map.values) { + t.cancel(); + } + map.clear(); + } + _debounceTimer?.cancel(); + _listener.dispose(); + _sendViews(); + await super.close(); + } +} diff --git a/lib/core/views/cubit/views_state.dart b/lib/core/views/cubit/views_state.dart new file mode 100644 index 00000000..e43d3ff5 --- /dev/null +++ b/lib/core/views/cubit/views_state.dart @@ -0,0 +1,3 @@ +class ViewsCubitState { + ViewsCubitState(); +} diff --git a/lib/core/views/repository/views_repository.dart b/lib/core/views/repository/views_repository.dart new file mode 100644 index 00000000..165a4956 --- /dev/null +++ b/lib/core/views/repository/views_repository.dart @@ -0,0 +1,16 @@ +import 'package:eatvio_app/core/views/service/views_service.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; + +class ViewsRepository { + final ViewsService viewsService; + + ViewsRepository({required this.viewsService}); + + Future viewContent({required ViewModel viewModel}) async { + try { + await viewsService.viewContent(viewModel: viewModel); + } catch (e) { + rethrow; + } + } +} diff --git a/lib/core/views/service/views_service.dart b/lib/core/views/service/views_service.dart new file mode 100644 index 00000000..e89990fa --- /dev/null +++ b/lib/core/views/service/views_service.dart @@ -0,0 +1,15 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'views_service.g.dart'; + +@RestApi() +abstract class ViewsService { + factory ViewsService(Dio dio, {String baseUrl}) = _ViewsService; + + @POST('/api/views') + Future viewContent({ + @Body() required ViewModel viewModel, + }); +} diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index 39ccdc6a..947b9a5d 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -1,9 +1,7 @@ import 'dart:developer'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; -import 'package:eatvio_app/features/feed/models/view_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; -import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -84,9 +82,4 @@ class FeedCubit extends Cubit { Future viewDiet(int id) async => await feedRepository.viewDiet(id); Future viewPlan(String slug) async => await feedRepository.viewPlan(slug); - Future viewContent({required ViewableContentType type, List? ids, List? slugs}) async { - feedRepository.viewContent( - viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids, slugs: slugs), - ); - } } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 2bfaf897..844788fe 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,7 +1,6 @@ -import 'dart:async'; - import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; import 'package:eatvio_app/core/widgets/add_item_button.dart'; import 'package:eatvio_app/core/widgets/custom_search_bar.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; @@ -62,19 +61,15 @@ class _FeedScreenContentState extends State { final consultantsController = TextEditingController(); late final List chipsLabelList; - late final AppLifecycleListener _listener; int _selectedChipIndex = 0; final blogPostsScrollController = ScrollController(); final recipesScrollController = ScrollController(); final dietsScrollController = ScrollController(); final plansScrollController = ScrollController(); final consultantsScrollController = ScrollController(); - final _feedCubit = sl(); @override void initState() { - // Initialize chipsLabelList based on hidePlans setting chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; - _listener = AppLifecycleListener(onDetach: _sendViews); consultantsScrollController.addListener(_onScrollConsultants); super.initState(); } @@ -83,20 +78,7 @@ class _FeedScreenContentState extends State { if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} } - void _sendViews() { - if (postsIds.isNotEmpty) { - _feedCubit.viewContent(type: ViewableContentType.posts, ids: postsIds.toList()); - } - if (plansIds.isNotEmpty) { - _feedCubit.viewContent(type: ViewableContentType.plans, ids: plansIds.toList()); - } - if (dietsIds.isNotEmpty) { - _feedCubit.viewContent(type: ViewableContentType.diets, ids: dietsIds.toList()); - } - if (recipesIds.isNotEmpty) { - _feedCubit.viewContent(type: ViewableContentType.recipes, ids: recipesIds.toList()); - } - } + @override void dispose() { @@ -105,63 +87,10 @@ class _FeedScreenContentState extends State { dietsScrollController.dispose(); plansScrollController.dispose(); consultantsScrollController.dispose(); - for (final map in _visibilityTimers.values) { - for (final t in map.values) { - t.cancel(); - } - map.clear(); - } - _listener.dispose(); - _sendViews(); super.dispose(); } - final Set postsIds = {}; - final Set plansIds = {}; - final Set dietsIds = {}; - final Set recipesIds = {}; - final Map> _visibilityTimers = { - ViewableContentType.posts: {}, - ViewableContentType.plans: {}, - ViewableContentType.diets: {}, - ViewableContentType.recipes: {}, - }; - - Set _idsForType(ViewableContentType type) { - switch (type) { - case ViewableContentType.posts: - return postsIds; - case ViewableContentType.plans: - return plansIds; - case ViewableContentType.diets: - return dietsIds; - case ViewableContentType.recipes: - return recipesIds; - } - } - - void _handleVisibility({required double visibleFraction, required int id, required ViewableContentType type}) { - final timers = _visibilityTimers[type]!; - final ids = _idsForType(type); - - if (ids.contains(id)) { - timers[id]?.cancel(); - timers.remove(id); - return; - } - - if (visibleFraction >= 0.5) { - if (timers.containsKey(id)) return; - timers[id] = Timer(const Duration(seconds: 1), () { - ids.add(id); - timers.remove(id); - }); - } else { - timers[id]?.cancel(); - timers.remove(id); - } - } @override Widget build(BuildContext context) { @@ -301,10 +230,10 @@ class _FeedScreenContentState extends State { ]; }, body: switch (_selectedChipIndex) { - 0 => BlogPosts(handleVisibility: _handleVisibility), - 1 => Recipes(handleVisibility: _handleVisibility), - 2 => Diets(handleVisibility: _handleVisibility), - 3 when !hidePlans => Plans(handleVisibility: _handleVisibility), + 0 => BlogPosts(handleVisibility: context.read().handleVisibility), + 1 => Recipes(handleVisibility: context.read().handleVisibility), + 2 => Diets(handleVisibility: context.read().handleVisibility), + 3 when !hidePlans => Plans(handleVisibility: context.read().handleVisibility), 3 when hidePlans => const Consultants(), 4 when !hidePlans => const Consultants(), _ => Center(child: Text(chipsLabelList[_selectedChipIndex])), diff --git a/lib/features/feed/presentation/widgets/diets.dart b/lib/features/feed/presentation/widgets/diets.dart index c5e5592a..080b1a14 100644 --- a/lib/features/feed/presentation/widgets/diets.dart +++ b/lib/features/feed/presentation/widgets/diets.dart @@ -42,7 +42,7 @@ class Diets extends StatelessWidget { key: ValueKey(index), onVisibilityChanged: (info) { final id = state.dietsList[index].id; - handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.diets); }, child: DietsWidget(diet: state.dietsList[index], index: index), ); diff --git a/lib/features/feed/presentation/widgets/plans.dart b/lib/features/feed/presentation/widgets/plans.dart index 4a620945..42e107e5 100644 --- a/lib/features/feed/presentation/widgets/plans.dart +++ b/lib/features/feed/presentation/widgets/plans.dart @@ -42,7 +42,7 @@ class Plans extends StatelessWidget { key: ValueKey(index), onVisibilityChanged: (info) { final id = state.plans[index].id; - handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.plans); }, child: PlanWidget(plan: state.plans[index], index: index), ); diff --git a/lib/features/feed/presentation/widgets/recipes.dart b/lib/features/feed/presentation/widgets/recipes.dart index d286949e..1271a493 100644 --- a/lib/features/feed/presentation/widgets/recipes.dart +++ b/lib/features/feed/presentation/widgets/recipes.dart @@ -46,7 +46,7 @@ class Recipes extends StatelessWidget { key: ValueKey(index), onVisibilityChanged: (info) { final id = state.recipesList[index].id; - handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts); + handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.recipes); }, child: RecipesWidget( recipe: state.recipesList[index], diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index cc8f7245..c11cb0b0 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,4 +1,3 @@ -import 'dart:developer'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; diff --git a/lib/main.dart b/lib/main.dart index 60293ed8..d716f3fa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/locator_service.dart' as di; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; @@ -67,7 +68,10 @@ class MyApp extends StatelessWidget { child: MaterialApp.router( theme: ThemeHelper().themeData(), routerConfig: AppRouter.appRouter, - builder: (context, child) => InternetConnectionCheckerScreen(child: child!), + builder: (context, child) => BlocProvider( + create: (context) => sl(), + child: InternetConnectionCheckerScreen(child: child!), + ), debugShowCheckedModeBanner: false, localizationsDelegates: const [ S.delegate, From 2600b8cc79b01020eb19664303ec92b0a30b218b Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 17 Sep 2025 16:28:13 +0300 Subject: [PATCH 08/49] fix(auth): hide social buttons --- .../auth/presentation/pages/auth_screen.dart | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index c32b8a39..8a4e711b 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -166,52 +166,52 @@ class _AuthScreenState extends State { ), ], ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _SocialButton( - onTap: _onVkPressed, - backgroundColor: const Color(0xFF2787F5), - tooltip: 'VK ID', - child: SvgPicture.asset( - 'assets/icons/vk.svg', - height: 24, - width: 24, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - ), - _SocialButton( - onTap: _onOkPressed, - backgroundColor: const Color(0xFFF58220), - tooltip: 'Одноклассники', - child: SvgPicture.asset( - 'assets/icons/ok.svg', - height: 24, - width: 24, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - ), - const _SocialButton( - onTap: _onMailRuPressed, - backgroundColor: Color(0xFF005FF9), - tooltip: 'Mail.ru', - child: Text( - 'Mail', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), - ), - ), - const _SocialButton( - onTap: _onYandexPressed, - backgroundColor: Color(0xFFFFCC00), - tooltip: 'Яндекс ID', - child: Text( - 'Yandex', - style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), - ), - ), - ], - ), + // const SizedBox(height: 16), + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // _SocialButton( + // onTap: _onVkPressed, + // backgroundColor: const Color(0xFF2787F5), + // tooltip: 'VK ID', + // child: SvgPicture.asset( + // 'assets/icons/vk.svg', + // height: 24, + // width: 24, + // colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + // ), + // ), + // _SocialButton( + // onTap: _onOkPressed, + // backgroundColor: const Color(0xFFF58220), + // tooltip: 'Одноклассники', + // child: SvgPicture.asset( + // 'assets/icons/ok.svg', + // height: 24, + // width: 24, + // colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + // ), + // ), + // const _SocialButton( + // onTap: _onMailRuPressed, + // backgroundColor: Color(0xFF005FF9), + // tooltip: 'Mail.ru', + // child: Text( + // 'Mail', + // style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), + // ), + // ), + // const _SocialButton( + // onTap: _onYandexPressed, + // backgroundColor: Color(0xFFFFCC00), + // tooltip: 'Яндекс ID', + // child: Text( + // 'Yandex', + // style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), + // ), + // ), + // ], + // ), const Spacer(), Center( child: RichText( From fafc2aa1f74c7e744bb29a65ecdf0f34447ec1d3 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 17 Sep 2025 16:32:24 +0300 Subject: [PATCH 09/49] fix: upd ver to 73 --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- ios/Runner/Info.plist | 2 +- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 668075b4..1fee690d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = 72; + CURRENT_PROJECT_VERSION = 73; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -539,7 +539,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 72; + CURRENT_PROJECT_VERSION = 73; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -578,7 +578,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 72; + CURRENT_PROJECT_VERSION = 73; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index b05e5fac..f0f66aff 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 72 + 73 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/pubspec.yaml b/pubspec.yaml index acf68969..6861a603 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.12+72 +version: 1.2.13+73 publish_to: 'none' environment: From fef4451f4e70584782e020dc948e5bb52a1ce066 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 17 Sep 2025 17:00:44 +0300 Subject: [PATCH 10/49] fix(profile): Issue-523 --- ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/Info.plist | 2 +- lib/core/theme/theme_helper.dart | 2 + .../auth/presentation/pages/auth_screen.dart | 4 ++ .../my_finance/models/card_model.dart | 3 + .../profile/models/profile_model.dart | 1 + .../presentation/pages/profile_screen.dart | 60 +++++++++---------- pubspec.yaml | 2 +- .../services/profile_service_test.dart | 13 ++-- 9 files changed, 49 insertions(+), 44 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1fee690d..8e9edd22 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = 73; + CURRENT_PROJECT_VERSION = 74; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -539,7 +539,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 73; + CURRENT_PROJECT_VERSION = 74; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -578,7 +578,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 73; + CURRENT_PROJECT_VERSION = 74; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f0f66aff..7e82c5b2 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 73 + 74 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/core/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart index c0c0af29..c202129e 100644 --- a/lib/core/theme/theme_helper.dart +++ b/lib/core/theme/theme_helper.dart @@ -204,6 +204,8 @@ class PrimaryColors { Color get commentBg => const Color(0XFFFFFDD8); Color get cardNotificationsBG => const Color(0xFFA4A4A4); + + Color get grey7B => const Color(0xFF7B7B7B); } PrimaryColors get appTheme => ThemeHelper().themeColor(); diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 8a4e711b..ea457d6c 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -1,3 +1,5 @@ +// ignore_for_file: unused_element, duplicate_ignore + import 'dart:async'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; @@ -365,6 +367,7 @@ class _AuthScreenState extends State { } } +// ignore: unused_element class _SocialButton extends StatelessWidget { const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip}); @@ -391,6 +394,7 @@ class _SocialButton extends StatelessWidget { } } +// ignore: unused_element void _onVkPressed() async { final accessToken = await SocialAuthFacade.signInWithVk(); if (accessToken == null) return; diff --git a/lib/features/my_finance/models/card_model.dart b/lib/features/my_finance/models/card_model.dart index 0435ea56..d248cace 100644 --- a/lib/features/my_finance/models/card_model.dart +++ b/lib/features/my_finance/models/card_model.dart @@ -15,6 +15,9 @@ abstract class CardModel with _$CardModel { } bool parseBool(dynamic json) { + if (json == null) { + throw Exception('parseBool: json is null'); + } if (json == 1 || json == true || json == '1') { return true; } diff --git a/lib/features/profile/models/profile_model.dart b/lib/features/profile/models/profile_model.dart index 03fd57f3..17eaa671 100644 --- a/lib/features/profile/models/profile_model.dart +++ b/lib/features/profile/models/profile_model.dart @@ -19,6 +19,7 @@ abstract class ProfileModel with _$ProfileModel { @JsonKey(name: "updated_at") DateTime? updatedAt, @JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile, @JsonKey(name: "is_blocked_by_me", fromJson: parseBool) required bool isBlockedByMe, + @JsonKey(name: "is_blocked_by_author", fromJson: parseBool) required bool isBlockedByAuthor, @JsonKey(name: "is_blocked_by_user", fromJson: parseBool, defaultValue: false) required bool isBlockedByUser, @JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe, @JsonKey(name: "slug") required String slug, diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 98966234..11ae9b7a 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/permission_handler/permission_handler.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; @@ -93,30 +94,6 @@ class _ProfileScreenState extends State { isFirstFetch = false; } - // Проверяем, заблокировал ли пользователь меня - if (state.profile.isBlockedByUser) { - return Scaffold( - appBar: AppBar(), - body: Center( - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.block, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - 'Пользователь ограничил доступ к своей странице', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Colors.grey[600]), - ), - ], - ), - ), - ), - ); - } - return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state); } else if (state is ProfileStateError) { return Scaffold( @@ -198,11 +175,34 @@ class _ProfileScreenState extends State { ), _subscriptionsAndSubscribers(state), if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)), - if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _socialLinks(state), - if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _education(state), - if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _diplomasAndCertificates(state), - if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans && !state.profile.isBlockedByMe) _activePlans(state), - if (state.blogPosts.isNotEmpty) + if (state.profile.isBlockedByAuthor) + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.block, size: 48, color: appTheme.grey7B), + const SizedBox(height: 16), + Center( + child: Text( + 'Пользователь ограничил доступ\nк своей странице', + textAlign: TextAlign.center, + style: TextStyles.textMSemibold.copyWith(color: appTheme.grey7B), + ), + ), + ], + ), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor) + _socialLinks(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor) + _education(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor) + _diplomasAndCertificates(state), + if (state.profile.roles.contains('consultant') && + state.consultantInfo != null && + !hidePlans && + !state.profile.isBlockedByMe && + !state.profile.isBlockedByAuthor) + _activePlans(state), + if (state.blogPosts.isNotEmpty && !state.profile.isBlockedByAuthor && !state.profile.isBlockedByMe) const Padding( padding: EdgeInsets.only(left: 8, top: 16, bottom: 8), child: Text( @@ -210,7 +210,7 @@ class _ProfileScreenState extends State { style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600), ), ), - _blogPosts(state), + if (state.blogPosts.isNotEmpty && !state.profile.isBlockedByAuthor && !state.profile.isBlockedByMe) _blogPosts(state), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 6861a603..5fec1116 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.13+73 +version: 1.2.14+74 publish_to: 'none' environment: diff --git a/test/features/profile/services/profile_service_test.dart b/test/features/profile/services/profile_service_test.dart index 8c666344..5c773bc3 100644 --- a/test/features/profile/services/profile_service_test.dart +++ b/test/features/profile/services/profile_service_test.dart @@ -30,6 +30,7 @@ void main() { badges: [], subscriptions: [], subscribers: [], + isBlockedByAuthor: false, ), ), ); @@ -72,23 +73,17 @@ void main() { }); test('getBlogPostByAuthorSlug', () async { - when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer( - (_) async => [], - ); + when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer((_) async => []); expect(await mockProfileService.getBlogPostByAuthorSlug(slug: 'slug'), isA>()); }); test('getWeight', () async { - when(mockProfileService.getWeight(period: '')).thenAnswer( - (_) async => [], - ); + when(mockProfileService.getWeight(period: '')).thenAnswer((_) async => []); expect(await mockProfileService.getWeight(period: ''), isA>()); }); test('getCircumferences', () async { - when(mockProfileService.getCircumferences(period: '')).thenAnswer( - (_) async => [], - ); + when(mockProfileService.getCircumferences(period: '')).thenAnswer((_) async => []); expect(await mockProfileService.getCircumferences(period: ''), isA>()); }); }); From e6df89c98ad0587f26a2a21c869cb7a0cc1b8449 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 19 Sep 2025 15:55:07 +0300 Subject: [PATCH 11/49] fix(feed): Issue-591 --- .../presentation/cubit/blog_post_cubit.dart | 22 +++++ .../presentation/pages/blog_post_screen.dart | 90 +++++++++-------- .../widgets/consultation_card_widget.dart | 3 +- .../bloc/feed_cubit/feed_cubit.dart | 14 ++- .../feed/presentation/pages/feed_screen.dart | 5 - .../widgets/blog_post_widget.dart | 99 +++++-------------- .../my_finance/models/card_model.dart | 3 - 7 files changed, 100 insertions(+), 136 deletions(-) diff --git a/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart b/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart index 59665002..02196288 100644 --- a/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart +++ b/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart @@ -18,4 +18,26 @@ class BlogPostCubit extends Cubit { emit(state.copyWith(isLoading: false)); } } + + Future toggleLike() async { + final current = state.blogPostModel; + if (current == null) return; + + // optimistic update + final wasLiked = current.wasLikedByMe; + final updated = current.copyWith(wasLikedByMe: !wasLiked, totalLikes: wasLiked ? current.totalLikes - 1 : current.totalLikes + 1); + emit(state.copyWith(blogPostModel: updated)); + + try { + if (wasLiked) { + await feedRepository.unlikedBlogPost(id: current.id); + } else { + await feedRepository.likeBlogPost(id: current.id); + } + } catch (e, st) { + // rollback on error + log(e.toString(), stackTrace: st); + emit(state.copyWith(blogPostModel: current)); + } + } } diff --git a/lib/features/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart index 25636a57..61b64f9e 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -32,8 +32,6 @@ class _BlogPostScreenState extends State { final CommentsCubit commentsCubit = sl(); final textEditingController = TextEditingController(); int? replyId; - bool isLiked = false; - int totalLikes = 0; @override void initState() { @@ -60,26 +58,30 @@ class _BlogPostScreenState extends State { builder: (BuildContext context, CommentsState state) { if (blogPostState.isLoading) { return Scaffold( - appBar: AppBar( - leading: BackButton( - onPressed: () { - if (context.canPop()) { - GoRouter.of(context).pop(); - return; - } - GoRouter.of(context).go('/feed-screen'); - }, - ), + appBar: AppBar( + leading: BackButton( + onPressed: () { + if (context.canPop()) { + GoRouter.of(context).pop(); + return; + } + GoRouter.of(context).go('/feed-screen'); + }, ), - body: const Loader()); + ), + body: const Loader(), + ); } - isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false; - totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0; + final bool isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false; + final int totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0; return Scaffold( resizeToAvoidBottomInset: true, bottomSheet: Container( padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero), - decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1)))), + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))), + ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -114,24 +116,14 @@ class _BlogPostScreenState extends State { maxLines: 5, minLines: 1, keyboardType: TextInputType.multiline, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Colors.black, - height: 1.2, - ), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2), decoration: InputDecoration( isDense: false, // убираем true, чтобы padding учитывался корректно contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), // вертикальный паддинг увеличен hintText: 'Комментарий', - hintStyle: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - height: 1.2, - color: Color(0xFFAEAEB2), - ), + hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)), filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), @@ -196,7 +188,7 @@ class _BlogPostScreenState extends State { id: widget.id, feedBlogPostModel: blogPostState.blogPostModel, userSlug: blogPostState.blogPostModel?.author.authorSlug ?? '', - ), + ), const SizedBox(width: 16), ], ), @@ -215,9 +207,7 @@ class _BlogPostScreenState extends State { children: [ CircleAvatar( radius: 16, - foregroundImage: NetworkImage( - baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? ''), - ), + foregroundImage: NetworkImage(baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), const SizedBox(width: 8), @@ -242,30 +232,32 @@ class _BlogPostScreenState extends State { if (blogPostState.blogPostModel?.description.isNotEmpty ?? false) Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: Html(data: blogPostState.blogPostModel?.description.replaceAll('\n', '
'), style: {"*": Style(color: Colors.black87)}), + child: Html( + data: blogPostState.blogPostModel?.description.replaceAll('\n', '
'), + style: {"*": Style(color: Colors.black87)}, + ), ), const SizedBox(height: 8), if (blogPostState.blogPostModel?.image.isNotEmpty ?? false) - CachedNetworkImage( - imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}', - ), + CachedNetworkImage(imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}'), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ _iconButton( - icon: Icons.favorite_border_rounded, + icon: isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded, text: totalLikes.toString(), iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null, backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null, textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null, onTap: () async { - setState(() { - isLiked = !isLiked; - isLiked ? totalLikes++ : totalLikes--; - }); - isLiked ? await context.read().likePost(widget.id) : await context.read().unlikedPost(widget.id); - sl().updateFeedBlogPostList(blogPostState.blogPostModel, isLiked); + await context.read().toggleLike(); + // ignore: use_build_context_synchronously + final updated = context.read().state.blogPostModel; + if (updated != null) { + // ignore: use_build_context_synchronously + context.read().updateFeedBlogPostList(updated, updated.wasLikedByMe); + } }, ), const SizedBox(width: 8), @@ -284,8 +276,10 @@ class _BlogPostScreenState extends State { const Spacer(), Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500), const SizedBox(width: 4), - Text(blogPostState.blogPostModel?.totalViews.toString() ?? '', - style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400)), + Text( + blogPostState.blogPostModel?.totalViews.toString() ?? '', + style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400), + ), ], ), ), @@ -332,7 +326,11 @@ class _BlogPostScreenState extends State { children: [ Icon(icon, size: 20, color: iconColor ?? appTheme.gray600), if (text != null) const SizedBox(width: 8), - if (text != null) Text(text, style: TextStyle(color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500)), + if (text != null) + Text( + text, + style: TextStyle(color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500), + ), ], ), ), diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index 3d870e8d..f9fa4143 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -1,5 +1,4 @@ import 'package:collection/collection.dart'; -import 'package:eatvio_app/core/const.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/widgets/custom_divider.dart'; @@ -66,7 +65,7 @@ class _ConsultationCardWidgetState extends State { children: [ Row( children: [ - CircleAvatar(radius: 30, backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl)), + CircleAvatar(radius: 30, backgroundImage: NetworkImage(widget.consultationsModel.userProfilePhotoUrl)), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart index 947b9a5d..d1bf399f 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -15,13 +15,12 @@ class FeedCubit extends Cubit { FeedCubit({required this.feedRepository}) : super(const FeedState.loading()); void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) { - if (feedBlogPostModel == null || isClosed) return; - List feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList]; - final int index = feedBlogPostList.indexOf(feedBlogPostModel); - feedBlogPostList[index] = feedBlogPostModel.copyWith( - wasLikedByMe: wasLikedByMe, - totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1, - ); + if (feedBlogPostModel == null || isClosed || state is! FeedStateLoaded) return; + final List feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList]; + final int index = feedBlogPostList.indexWhere((e) => e.id == feedBlogPostModel.id); + if (index == -1) return; + final current = feedBlogPostList[index]; + feedBlogPostList[index] = current.copyWith(wasLikedByMe: wasLikedByMe, totalLikes: wasLikedByMe ? current.totalLikes + 1 : current.totalLikes - 1); if (!isClosed) { emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading)); } @@ -81,5 +80,4 @@ class FeedCubit extends Cubit { Future viewRecipe(String slug) async => await feedRepository.viewRecipe(slug); Future viewDiet(int id) async => await feedRepository.viewDiet(id); Future viewPlan(String slug) async => await feedRepository.viewPlan(slug); - } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 844788fe..796b4a3b 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -36,7 +36,6 @@ class FeedScreen extends StatelessWidget { providers: [ BlocProvider(create: (context) => sl()..loadRecipesList()), BlocProvider(create: (context) => sl()..getProfile()), - BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), BlocProvider(create: (context) => sl()..loadDiets()), if (!hidePlans) BlocProvider(create: (context) => sl()..loadPlans()), BlocProvider(create: (context) => sl()..getConsultants()), @@ -78,8 +77,6 @@ class _FeedScreenContentState extends State { if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} } - - @override void dispose() { blogPostsScrollController.dispose(); @@ -90,8 +87,6 @@ class _FeedScreenContentState extends State { super.dispose(); } - - @override Widget build(BuildContext context) { FlutterNativeSplash.remove(); diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index 23eaeded..e001a038 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -21,10 +21,7 @@ import 'package:share_plus/share_plus.dart'; class BlogPostWidget extends StatefulWidget { final int index; - const BlogPostWidget({ - super.key, - required this.index, - }); + const BlogPostWidget({super.key, required this.index}); @override State createState() => _BlogPostWidgetState(); @@ -44,10 +41,7 @@ class _BlogPostWidgetState extends State { totalLikes = state.feedBlogPostList[widget.index].totalLikes; return InkWell( borderRadius: BorderRadius.circular(12), - onTap: () => GoRouter.of(context).pushNamed( - 'blogPostScreen', - extra: state.feedBlogPostList[widget.index].id, - ), + onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.feedBlogPostList[widget.index].id), child: Material( borderRadius: BorderRadius.circular(12), color: Colors.white, @@ -95,21 +89,12 @@ class _BlogPostWidgetState extends State { Text( state.feedBlogPostList[widget.index].author.authorName, overflow: TextOverflow.ellipsis, - style: const TextStyle( - height: 1, - color: Colors.black87, - fontWeight: FontWeight.w500, - fontSize: 16, - ), + style: const TextStyle(height: 1, color: Colors.black87, fontWeight: FontWeight.w500, fontSize: 16), ), const SizedBox(height: 4), Text( '${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}', - style: const TextStyle( - height: 1, - color: Colors.black45, - fontSize: 14, - ), + style: const TextStyle(height: 1, color: Colors.black45, fontSize: 14), ), ], ), @@ -119,9 +104,7 @@ class _BlogPostWidgetState extends State { !state.feedBlogPostList[widget.index].author.authorIsMe) Padding( padding: const EdgeInsets.only(right: 8), - child: SubscribeButtonWidget( - slug: state.feedBlogPostList[widget.index].author.authorSlug, - ), + child: SubscribeButtonWidget(slug: state.feedBlogPostList[widget.index].author.authorSlug), ), // Кнопка меню CustomPopup( @@ -141,11 +124,7 @@ class _BlogPostWidgetState extends State { padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( state.feedBlogPostList[widget.index].title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black87), ), ), ], @@ -156,15 +135,12 @@ class _BlogPostWidgetState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Html( - data: (state.feedBlogPostList[widget.index].description.length > 200 - ? state.feedBlogPostList[widget.index].description.substring(0, 200) - : state.feedBlogPostList[widget.index].description).replaceAll('\n', '
'), - style: { - "*": Style( - fontSize: FontSize(16), - color: Colors.black87, - ), - }, + data: + (state.feedBlogPostList[widget.index].description.length > 200 + ? state.feedBlogPostList[widget.index].description.substring(0, 200) + : state.feedBlogPostList[widget.index].description) + .replaceAll('\n', '
'), + style: {"*": Style(fontSize: FontSize(16), color: Colors.black87)}, ), ), if (state.feedBlogPostList[widget.index].description.length > 200) @@ -172,12 +148,7 @@ class _BlogPostWidgetState extends State { padding: EdgeInsets.symmetric(horizontal: 12), child: Text( 'Показать ещё', - style: TextStyle( - height: 1, - color: Color(0xFF8B8B8B), - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600), ), ), if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty) @@ -186,10 +157,7 @@ class _BlogPostWidgetState extends State { onTap: () { context.pushTransparentRoute( rootNavigator: true, - PhotoViewScreen( - imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'], - index: 0, - ), + PhotoViewScreen(imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'], index: 0), ); }, child: Hero( @@ -218,11 +186,14 @@ class _BlogPostWidgetState extends State { isLiked = !isLiked; isLiked ? totalLikes++ : totalLikes--; }); - isLiked - ? await context.read().likePost(state.feedBlogPostList[widget.index].id) - : await context.read().unlikedPost(state.feedBlogPostList[widget.index].id); - if (!context.mounted) return; - await context.read().loadFeedBlogPostList(); + // Update feed list immediately by ID (no reload) + context.read().updateFeedBlogPostList(state.feedBlogPostList[widget.index], isLiked); + // Fire network call without introducing UI delay + if (isLiked) { + await context.read().likePost(state.feedBlogPostList[widget.index].id); + } else { + await context.read().unlikedPost(state.feedBlogPostList[widget.index].id); + } }, ), const SizedBox(width: 8), @@ -231,16 +202,10 @@ class _BlogPostWidgetState extends State { 'assets/icons/comment.svg', height: 20, width: 20, - colorFilter: ColorFilter.mode( - appTheme.gray600, - BlendMode.srcIn, - ), + colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn), ), text: state.feedBlogPostList[widget.index].totalComments.toString(), - onTap: () => GoRouter.of(context).pushNamed( - 'blogPostScreen', - extra: state.feedBlogPostList[widget.index].id, - ), + onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.feedBlogPostList[widget.index].id), ), const SizedBox(width: 8), FeedIconButton( @@ -254,28 +219,18 @@ class _BlogPostWidgetState extends State { 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, ), ); }, ), const Spacer(), - Icon( - Icons.visibility_rounded, - size: 16, - color: appTheme.gray500, - ), + Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500), const SizedBox(width: 4), Text( state.feedBlogPostList[widget.index].totalViews.toString(), - style: TextStyle( - fontSize: 12, - color: appTheme.gray500, - fontWeight: FontWeight.w400, - ), + style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400), ), ], ), diff --git a/lib/features/my_finance/models/card_model.dart b/lib/features/my_finance/models/card_model.dart index d248cace..0435ea56 100644 --- a/lib/features/my_finance/models/card_model.dart +++ b/lib/features/my_finance/models/card_model.dart @@ -15,9 +15,6 @@ abstract class CardModel with _$CardModel { } bool parseBool(dynamic json) { - if (json == null) { - throw Exception('parseBool: json is null'); - } if (json == 1 || json == true || json == '1') { return true; } From ad777c695bd38fd3a308b095c853bf4f8474e62a Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 22 Sep 2025 17:08:41 +0300 Subject: [PATCH 12/49] fix(consultations): Issue-393 --- .../widgets/mood_rating_widget.dart | 93 +++++-------------- .../widgets/satiety_rating_widget.dart | 93 +++++-------------- .../diary/repositories/diary_repository.dart | 48 +++++++++- .../presentation/user_details_screen.dart | 4 +- 4 files changed, 99 insertions(+), 139 deletions(-) diff --git a/lib/features/diary/presentation/widgets/mood_rating_widget.dart b/lib/features/diary/presentation/widgets/mood_rating_widget.dart index 41640b78..d943eac2 100644 --- a/lib/features/diary/presentation/widgets/mood_rating_widget.dart +++ b/lib/features/diary/presentation/widgets/mood_rating_widget.dart @@ -23,9 +23,12 @@ class _MoodRatingWidgetState extends State { builder: (BuildContext context, DiaryState state) { if (state is DiaryStateLoaded) { final selectedIndex = state.selectedMeal - 1; - if (isFirstFetch[selectedIndex]) { - currentSelectedRating[selectedIndex] = state.diary.meals[selectedIndex].mood.toDouble() + 1; - isFirstFetch[selectedIndex] = false; + final meals = state.diary.meals; + if (selectedIndex >= 0 && selectedIndex < meals.length) { + if (isFirstFetch[selectedIndex]) { + currentSelectedRating[selectedIndex] = meals[selectedIndex].mood.toDouble() + 1; + isFirstFetch[selectedIndex] = false; + } } return Padding( padding: const EdgeInsets.only(top: 24), @@ -35,21 +38,17 @@ class _MoodRatingWidgetState extends State { Text( state.isConsultantView ? 'Настроение пользователя после приема пищи' : 'Ваше настроение после приёма пищи', textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 16, - ), + style: const TextStyle(fontSize: 16), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SvgPicture.asset( - 'assets/icons/mood_1.svg', - height: 32, - width: 32, - ), + SvgPicture.asset('assets/icons/mood_1.svg', height: 32, width: 32), RatingBar.builder( ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented, - initialRating: state.diary.meals[selectedIndex].mood.toDouble() + 1, + initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length) + ? state.diary.meals[selectedIndex].mood.toDouble() + 1 + : 3, itemCount: 6, itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6), updateOnDrag: true, @@ -63,20 +62,11 @@ class _MoodRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFC82222), border: currentSelectedRating[selectedIndex] == 1 - ? Border.all( - width: 2, - color: const Color(0xFFAA3333), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 1 - ? const BorderRadius.horizontal( - left: Radius.circular(8), - right: Radius.circular(2), - ) - : const BorderRadius.horizontal( - left: Radius.circular(8), - ), + ? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2)) + : const BorderRadius.horizontal(left: Radius.circular(8)), ), ); case 1: @@ -87,11 +77,7 @@ class _MoodRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFFF3E3E), border: currentSelectedRating[selectedIndex] == 2 - ? Border.all( - width: 2, - color: const Color(0xFF550000), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null, ), @@ -104,11 +90,7 @@ class _MoodRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFF8C926), border: currentSelectedRating[selectedIndex] == 3 - ? Border.all( - width: 2, - color: const Color(0xFF8C5D00), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null, ), @@ -121,11 +103,7 @@ class _MoodRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFFFF3BF), border: currentSelectedRating[selectedIndex] == 4 - ? Border.all( - width: 2, - color: const Color(0xFFB5911B), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null, ), @@ -138,11 +116,7 @@ class _MoodRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFF62E833), border: currentSelectedRating[selectedIndex] == 5 - ? Border.all( - width: 2, - color: const Color(0xFF276918), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null, ), @@ -155,20 +129,11 @@ class _MoodRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFF4DCE20), border: currentSelectedRating[selectedIndex] == 6 - ? Border.all( - width: 2, - color: const Color(0xFF184D12), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 6 - ? const BorderRadius.horizontal( - left: Radius.circular(2), - right: Radius.circular(8), - ) - : const BorderRadius.horizontal( - right: Radius.circular(8), - ), + ? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8)) + : const BorderRadius.horizontal(right: Radius.circular(8)), ), ); } @@ -177,20 +142,12 @@ class _MoodRatingWidgetState extends State { setState(() { currentSelectedRating[selectedIndex] = value; }); - EasyDebounce.debounce( - 'change-satiety', - const Duration(milliseconds: 250), - () { - context.read().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1); - }, - ); + EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () { + context.read().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1); + }); }, ), - SvgPicture.asset( - 'assets/icons/mood_2.svg', - height: 32, - width: 32, - ), + SvgPicture.asset('assets/icons/mood_2.svg', height: 32, width: 32), ], ), ], diff --git a/lib/features/diary/presentation/widgets/satiety_rating_widget.dart b/lib/features/diary/presentation/widgets/satiety_rating_widget.dart index d473f691..d36ac75b 100644 --- a/lib/features/diary/presentation/widgets/satiety_rating_widget.dart +++ b/lib/features/diary/presentation/widgets/satiety_rating_widget.dart @@ -23,9 +23,12 @@ class _SatietyRatingWidgetState extends State { builder: (BuildContext context, DiaryState state) { if (state is DiaryStateLoaded) { final selectedIndex = state.selectedMeal - 1; - if (isFirstFetch[selectedIndex]) { - currentSelectedRating[selectedIndex] = state.diary.meals[selectedIndex].satiety.toDouble() + 1; - isFirstFetch[selectedIndex] = false; + final meals = state.diary.meals; + if (selectedIndex >= 0 && selectedIndex < meals.length) { + if (isFirstFetch[selectedIndex]) { + currentSelectedRating[selectedIndex] = meals[selectedIndex].satiety.toDouble() + 1; + isFirstFetch[selectedIndex] = false; + } } return Padding( padding: const EdgeInsets.only(top: 24), @@ -35,21 +38,17 @@ class _SatietyRatingWidgetState extends State { Text( state.isConsultantView ? 'Сытость пользователя после приема пищи' : 'Ваша сытость после приёма пищи', textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 16, - ), + style: const TextStyle(fontSize: 16), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SvgPicture.asset( - 'assets/icons/satiety_emoji_left.svg', - height: 32, - width: 32, - ), + SvgPicture.asset('assets/icons/satiety_emoji_left.svg', height: 32, width: 32), RatingBar.builder( ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented, - initialRating: state.diary.meals[selectedIndex].satiety.toDouble() + 1, + initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length) + ? state.diary.meals[selectedIndex].satiety.toDouble() + 1 + : 3, itemCount: 6, itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6), updateOnDrag: true, @@ -63,20 +62,11 @@ class _SatietyRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFC82222), border: currentSelectedRating[selectedIndex] == 1 - ? Border.all( - width: 2, - color: const Color(0xFFAA3333), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 1 - ? const BorderRadius.horizontal( - left: Radius.circular(8), - right: Radius.circular(2), - ) - : const BorderRadius.horizontal( - left: Radius.circular(8), - ), + ? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2)) + : const BorderRadius.horizontal(left: Radius.circular(8)), ), ); case 1: @@ -87,11 +77,7 @@ class _SatietyRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFFF3E3E), border: currentSelectedRating[selectedIndex] == 2 - ? Border.all( - width: 2, - color: const Color(0xFF550000), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null, ), @@ -104,11 +90,7 @@ class _SatietyRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFF8C926), border: currentSelectedRating[selectedIndex] == 3 - ? Border.all( - width: 2, - color: const Color(0xFF8C5D00), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null, ), @@ -121,11 +103,7 @@ class _SatietyRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFFFFF3BF), border: currentSelectedRating[selectedIndex] == 4 - ? Border.all( - width: 2, - color: const Color(0xFFB5911B), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null, ), @@ -138,11 +116,7 @@ class _SatietyRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFF62E833), border: currentSelectedRating[selectedIndex] == 5 - ? Border.all( - width: 2, - color: const Color(0xFF276918), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null, ), @@ -155,20 +129,11 @@ class _SatietyRatingWidgetState extends State { decoration: BoxDecoration( color: const Color(0xFF4DCE20), border: currentSelectedRating[selectedIndex] == 6 - ? Border.all( - width: 2, - color: const Color(0xFF184D12), - strokeAlign: BorderSide.strokeAlignOutside, - ) + ? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside) : null, borderRadius: currentSelectedRating[selectedIndex] == 6 - ? const BorderRadius.horizontal( - left: Radius.circular(2), - right: Radius.circular(8), - ) - : const BorderRadius.horizontal( - right: Radius.circular(8), - ), + ? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8)) + : const BorderRadius.horizontal(right: Radius.circular(8)), ), ); } @@ -178,20 +143,12 @@ class _SatietyRatingWidgetState extends State { setState(() { currentSelectedRating[selectedIndex] = value; }); - EasyDebounce.debounce( - 'change-satiety', - const Duration(milliseconds: 250), - () { - context.read().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1); - }, - ); + EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () { + context.read().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1); + }); }, ), - SvgPicture.asset( - 'assets/icons/satiety_emoji_right.svg', - height: 32, - width: 32, - ), + SvgPicture.asset('assets/icons/satiety_emoji_right.svg', height: 32, width: 32), ], ), ], diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index 0165ad7f..f3ff8bb5 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -82,7 +82,53 @@ class DiaryRepository { } Future getDiaryDay(int diaryId) async { - return (await diaryService.getDiaryDay(diary: diaryId)).diary; + // Align with getDiaryByDate: enrich portions with glossary nutritional info + final diaryWrapper = await diaryService.getDiaryDay(diary: diaryId); + final DiaryDayModel diaryDay = diaryWrapper.diary; + Set ids = {}; + for (var meal in diaryDay.meals) { + for (var portion in meal.portions) { + if (portion.productNutritionalValueId != null) { + ids.add(portion.productNutritionalValueId!); + } + } + } + if (ids.isNotEmpty) { + final glossary = await getGlossariesIndex(ids: ids.toList()); + final List updatedMeals = List.from(diaryDay.meals); + for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) { + final meal = updatedMeals[mealIndex]; + final updatedPortions = meal.portions.map((portion) { + if (portion.productNutritionalValueId != null) { + try { + final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId); + return portion.copyWith( + productCaloriesInfo: ProductCaloriesInfoModel( + id: glossaryItem.id, + productName: glossaryItem.name, + calories: glossaryItem.energyOn100Gr, + proteins: glossaryItem.proteinsOn100Gr, + fats: glossaryItem.fatsOn100Gr, + carbohydrates: glossaryItem.carbohydratesOn100Gr, + energyOnPortion: glossaryItem.energyOnPortion, + proteinsOnPortion: glossaryItem.proteinsOnPortion, + fatsOnPortion: glossaryItem.fatsOnPortion, + carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion, + ), + ); + } catch (e) { + return portion; + } + } + return portion; + }).toList(); + + updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions); + } + final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals); + return updatedDiaryDay; + } + return diaryDay; } Future getNutritionData(int index) async { diff --git a/lib/features/user_details/presentation/user_details_screen.dart b/lib/features/user_details/presentation/user_details_screen.dart index 4907e3dd..46e986f4 100644 --- a/lib/features/user_details/presentation/user_details_screen.dart +++ b/lib/features/user_details/presentation/user_details_screen.dart @@ -231,7 +231,7 @@ class _UserDetailsContentState extends State { children: [ Text(s.has_hypo, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHypo.toString())), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHypo ? 'да' : 'нет')), ], ), ), @@ -242,7 +242,7 @@ class _UserDetailsContentState extends State { children: [ Text(s.has_hyper, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHyper.toString())), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHyper ? 'да' : 'нет')), ], ), ), From 052c4fedca39a480f28a6fffd8b01ddc11123931 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 23 Sep 2025 23:47:38 +0300 Subject: [PATCH 13/49] feat(hms): Issue-660 --- android/build.gradle | 1 + lib/core/firebase_api.dart | 47 +++++++++++++++++++++++++++++++++++++- pubspec.yaml | 1 + 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index d2ffbffa..9db63328 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,6 +2,7 @@ allprojects { repositories { google() mavenCentral() + maven { url 'https://developer.huawei.com/repo/' } } } diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index 572b56f6..2ba89290 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -6,13 +6,24 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:huawei_push/huawei_push.dart'; class FirebaseApi { final _firebaseMessaging = FirebaseMessaging.instance; Future getToken() async { await _firebaseMessaging.requestPermission(); - return await _firebaseMessaging.getToken(); + + String? token = await _firebaseMessaging.getToken(); + + if (token == null && Platform.isAndroid) { + final isHuawei = await _isHuaweiDevice(); + if (isHuawei) { + token = await _getHuaweiPushToken(); + } + } + + return token; } Future registerPushToken() async { @@ -32,4 +43,38 @@ class FirebaseApi { } } } + + Future _isHuaweiDevice() async { + if (!Platform.isAndroid) return false; + try { + final android = await DeviceInfoPlugin().androidInfo; + final brand = android.brand.toLowerCase(); + final manufacturer = android.manufacturer.toLowerCase(); + return brand.contains('huawei') || manufacturer.contains('huawei') || brand.contains('honor') || manufacturer.contains('honor'); + } catch (_) { + return false; + } + } + + Future _getHuaweiPushToken() async { + try { + await Push.turnOnPush(); + + String? resolvedToken; + final sub = Push.getTokenStream.listen((t) => resolvedToken ??= t, onError: (_) {}); + + Push.getToken(''); + + final started = DateTime.now(); + while (resolvedToken == null && DateTime.now().difference(started).inSeconds < 5) { + await Future.delayed(const Duration(milliseconds: 100)); + } + + await sub.cancel(); + return resolvedToken; + } catch (e, st) { + log('Huawei Push token error: $e', stackTrace: st); + return null; + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index 5fec1116..3f1e036e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,6 +74,7 @@ dependencies: flutter_dotenv: ^5.2.1 uuid: ^4.5.1 appmetrica_plugin: ^3.3.0 + huawei_push: ^6.11.0+300 dev_dependencies: build_runner: ^2.7.0 From fc09fa36b8ae48c78e1d88c7e39240040b797036 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 24 Sep 2025 23:47:11 +0300 Subject: [PATCH 14/49] feat(auth): Issue-691 --- android/app/build.gradle | 3 + .../auth/presentation/pages/auth_screen.dart | 94 +++++------ lib/features/auth/sdk/social_auth_facade.dart | 152 ++++++++++++++++-- pubspec.yaml | 2 + 4 files changed, 194 insertions(+), 57 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 9ffa87c4..457960f7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -13,6 +13,8 @@ if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } +def yandexClientId = project.hasProperty('YANDEX_CLIENT_ID') ? project.property('YANDEX_CLIENT_ID') : (System.getenv('YANDEX_CLIENT_ID') ?: "") + android { namespace = "com.eatvioapp.app" compileSdk = flutter.compileSdkVersion @@ -33,6 +35,7 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + manifestPlaceholders = [YANDEX_CLIENT_ID: yandexClientId] } signingConfigs { diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index ea457d6c..f9eeddf5 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -169,51 +169,51 @@ class _AuthScreenState extends State { ], ), // const SizedBox(height: 16), - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // _SocialButton( - // onTap: _onVkPressed, - // backgroundColor: const Color(0xFF2787F5), - // tooltip: 'VK ID', - // child: SvgPicture.asset( - // 'assets/icons/vk.svg', - // height: 24, - // width: 24, - // colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - // ), - // ), - // _SocialButton( - // onTap: _onOkPressed, - // backgroundColor: const Color(0xFFF58220), - // tooltip: 'Одноклассники', - // child: SvgPicture.asset( - // 'assets/icons/ok.svg', - // height: 24, - // width: 24, - // colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - // ), - // ), - // const _SocialButton( - // onTap: _onMailRuPressed, - // backgroundColor: Color(0xFF005FF9), - // tooltip: 'Mail.ru', - // child: Text( - // 'Mail', - // style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), - // ), - // ), - // const _SocialButton( - // onTap: _onYandexPressed, - // backgroundColor: Color(0xFFFFCC00), - // tooltip: 'Яндекс ID', - // child: Text( - // 'Yandex', - // style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), - // ), - // ), - // ], - // ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _SocialButton( + onTap: () => _onVkPressed(context), + backgroundColor: const Color(0xFF2787F5), + tooltip: 'VK ID', + child: SvgPicture.asset( + 'assets/icons/vk.svg', + height: 24, + width: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + ), + _SocialButton( + onTap: _onOkPressed, + backgroundColor: const Color(0xFFF58220), + tooltip: 'Одноклассники', + child: SvgPicture.asset( + 'assets/icons/ok.svg', + height: 24, + width: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + ), + const _SocialButton( + onTap: _onMailRuPressed, + backgroundColor: Color(0xFF005FF9), + tooltip: 'Mail.ru', + child: Text( + 'Mail', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), + ), + ), + const _SocialButton( + onTap: _onYandexPressed, + backgroundColor: Color(0xFFFFCC00), + tooltip: 'Яндекс ID', + child: Text( + 'Yandex', + style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), + ), + ), + ], + ), const Spacer(), Center( child: RichText( @@ -395,8 +395,8 @@ class _SocialButton extends StatelessWidget { } // ignore: unused_element -void _onVkPressed() async { - final accessToken = await SocialAuthFacade.signInWithVk(); +void _onVkPressed(BuildContext context) async { + final accessToken = await SocialAuthFacade.signInWithVk(context: context); if (accessToken == null) return; final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); if (isAuthenticated) { diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 62985d5d..e5761498 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -1,25 +1,157 @@ import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_login_yandex/flutter_login_yandex.dart'; +import 'package:eatvio_app/core/app_router.dart'; +import 'package:vk_id/vk_id.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class VkAuthResult { + VkAuthResult({required this.accessToken, this.refreshToken, this.userId, this.firstName, this.lastName, this.email, this.phone, this.gender, this.birthDate}); + + final String accessToken; + final String? refreshToken; + final String? userId; + final String? firstName; + final String? lastName; + final String? email; + final String? phone; + final String? gender; + final String? birthDate; +} class SocialAuthFacade { - static Future signInWithVk() async { - // TODO: Integrate official VK ID SDK and return access token - return null; + static Future signInWithVk({required BuildContext context}) async { + final result = await signInWithVkAndFetchProfile(context: context); + return result?.accessToken; + } + + static Future signInWithVkAndFetchProfile({required BuildContext context}) async { + final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID']; + if (clientIdStr == null || clientIdStr.isEmpty) { + return null; + } + + try { + dynamic controller = VkIDController(clID: int.parse(clientIdStr)); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); + final redirectScheme = 'vk$clientIdStr://'; + final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme); + if (redirectParams == null) return null; + final code = redirectParams['code']; + final deviceId = redirectParams['device_id']; + if (code == null || deviceId == null) return null; + + final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId); + final accessToken = _extractAccessToken(tokenResponse); + if (accessToken == null) return null; + return VkAuthResult(accessToken: accessToken); + } catch (_) { + return null; + } } static Future signInWithOk() async { - // TODO: Integrate official OK SDK and return access token - return null; + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return null; + return _signInWithVkIdProvider(context: context, provider: 'ok'); } static Future signInWithMailRu() async { - // TODO: Integrate official Mail.ru SDK and return access token - return null; + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return null; + return _signInWithVkIdProvider(context: context, provider: 'mail'); } static Future signInWithYandex() async { - // TODO: Integrate official Yandex ID SDK and return access token + final clientId = dotenv.maybeGet('YANDEX_CLIENT_ID')?.trim() ?? Platform.environment['YANDEX_CLIENT_ID']; + final redirectUri = dotenv.maybeGet('YANDEX_REDIRECT_URI')?.trim() ?? Platform.environment['YANDEX_REDIRECT_URI']; + + if (clientId == null || clientId.isEmpty || redirectUri == null || redirectUri.isEmpty) { + return null; + } + try { + final plugin = FlutterLoginYandex(); + final response = await plugin.signIn(); + final token = (response is Map) ? response['token'] as String? : null; + return (token != null && token.isNotEmpty) ? token : null; + } catch (_) { + return null; + } + } + + static Future _signInWithVkIdProvider({required BuildContext context, required String provider}) async { + final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID']; + if (clientIdStr == null || clientIdStr.isEmpty) { + return null; + } + try { + dynamic controller = VkIDController(clID: int.parse(clientIdStr)); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); + final redirectScheme = 'vk$clientIdStr://'; + final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme); + if (redirectParams == null) return null; + final code = redirectParams['code']; + final deviceId = redirectParams['device_id']; + if (code == null || deviceId == null) return null; + final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId, provider: provider); + final accessToken = _extractAccessToken(tokenResponse); + return accessToken; + } catch (_) { + return null; + } + } + + static String? _extractAccessToken(dynamic tokenResponse) { + if (tokenResponse == null) return null; + if (tokenResponse is Map) { + return (tokenResponse['access_token'] as String?) ?? (tokenResponse['token'] as String?); + } + try { + final token = (tokenResponse as dynamic).accessToken as String?; + if (token != null && token.isNotEmpty) return token; + } catch (_) {} + try { + final token = (tokenResponse as dynamic).token as String?; + if (token != null && token.isNotEmpty) return token; + } catch (_) {} return null; } + + static Future?> _presentOAuthWebView({ + required BuildContext context, + required String initialUrl, + required String redirectSchemePrefix, + }) async { + final controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onNavigationRequest: (request) { + final url = request.url; + if (url.startsWith(redirectSchemePrefix)) { + final uri = Uri.parse(url); + final params = {}..addAll(uri.queryParameters); + Navigator.of(context, rootNavigator: true).pop(params); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse(initialUrl)); + + return showDialog>( + context: context, + barrierDismissible: false, + builder: (_) { + return Dialog( + insetPadding: const EdgeInsets.all(16), + child: SizedBox(height: 560, child: WebViewWidget(controller: controller)), + ); + }, + ); + } } - - diff --git a/pubspec.yaml b/pubspec.yaml index 3f1e036e..a62a13af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,8 @@ dependencies: uuid: ^4.5.1 appmetrica_plugin: ^3.3.0 huawei_push: ^6.11.0+300 + vk_id: ^1.0.3 + flutter_login_yandex: ^0.0.5 dev_dependencies: build_runner: ^2.7.0 From a0b29f42df50de20d6ab863bcb2c5bbcdf532b4e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 26 Sep 2025 10:31:34 +0300 Subject: [PATCH 15/49] feat(hp): Issue-660 --- android/app/agconnect-services.json | 1 + android/app/build.gradle | 17 ++++- android/app/proguard-rules.pro | 21 ++++++ android/app/src/main/AndroidManifest.xml | 10 +++ .../com/eatvioapp/app/HmsPushService.kt | 19 ++++++ .../kotlin/com/eatvioapp/app/MainActivity.kt | 65 +++++++++++++++++++ android/build.gradle | 39 +++++++++++ .../reports/problems/problems-report.html | 4 +- android/settings.gradle | 1 + lib/core/firebase_api.dart | 56 +++++++--------- pubspec.yaml | 1 - 11 files changed, 199 insertions(+), 35 deletions(-) create mode 100644 android/app/agconnect-services.json create mode 100644 android/app/proguard-rules.pro create mode 100644 android/app/src/main/kotlin/com/eatvioapp/app/HmsPushService.kt diff --git a/android/app/agconnect-services.json b/android/app/agconnect-services.json new file mode 100644 index 00000000..8ca899db --- /dev/null +++ b/android/app/agconnect-services.json @@ -0,0 +1 @@ +{"agcgw":{"url":"connect-drru.hispace.dbankcloud.ru","backurl":"connect-drru.hispace.dbankcloud.cn","websocketurl":"connect-ws-drru.hispace.dbankcloud.ru","websocketbackurl":"connect-ws-drru.hispace.dbankcloud.cn"},"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.hicloud.com","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.hispace.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-ws-dre.hispace.dbankcloud.cn","DE_back":"connect-ws-dre.hispace.dbankcloud.com"},"client":{"cp_id":"20007000036693451","product_id":"461323198429446894","client_id":"1595824543740483264","client_secret":"2A1727B0D31AA9BE1F5D9E8A5CDA1A67B9EF084AC9FB68D6C633A3CCB08ECDEE","project_id":"461323198429446894","app_id":"113238569","api_key":"DQEDAJNRITp9M3uc1rPHyT8PzLc7bnVcKJAeavZZk0TQNMx9ifvBebr+JOmZe9tEvjnLwv7c1hAHXmPGR/gWG86iABDrCVotq+NGnw==","package_name":"com.eatvioapp.app"},"oauth_client":{"client_id":"113238569","client_type":1},"app_info":{"app_id":"113238569","package_name":"com.eatvioapp.app"},"service":{"analytics":{"collector_url":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn","collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn","resource_id":"p1","channel_id":""},"ml":{"mlservice_url":"ml-api-drru.ai.dbankcloud.ru,ml-api-drru.ai.dbankcloud.cn"},"cloudstorage":{"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg":"https://ops-dra.agcstorage.link","storage_url_de":"https://ops-dre.agcstorage.link","storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia","storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu","storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn"},"search":{"url":"https://search-drru.cloud.huawei.ru"},"edukit":{"edu_url":"edukit.cloud.huawei.com.cn","dh_url":"edukit.cloud.huawei.com.cn"}},"region":"RU","configuration_version":"3.0","appInfos":[{"package_name":"com.eatvioapp.app","client":{"app_id":"113238569"},"app_info":{"package_name":"com.eatvioapp.app","app_id":"113238569"},"oauth_client":{"client_type":1,"client_id":"113238569"}}]} \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 457960f7..15095cb5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -5,6 +5,7 @@ plugins { // END: FlutterFire Configuration id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" + id 'com.huawei.agconnect' } def keystoreProperties = new Properties() @@ -35,7 +36,10 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName - manifestPlaceholders = [YANDEX_CLIENT_ID: yandexClientId] + manifestPlaceholders = [ + YANDEX_CLIENT_ID: yandexClientId, + applicationName: "io.flutter.app.FlutterApplication" + ] } signingConfigs { @@ -50,6 +54,9 @@ android { buildTypes { release { signingConfig signingConfigs.release + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } debug { signingConfig signingConfigs.debug @@ -60,6 +67,14 @@ android { } } +dependencies { + implementation 'com.huawei.hms:push:6.13.0.300' + // Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8 + implementation 'com.huawei.hms:hianalytics:6.13.0.300' + // BouncyCastle provider required by Huawei secure utils used transitively by HMS + implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1' +} + flutter { source = "../.." } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 00000000..b657fa16 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +-keep class com.huawei.** { *; } +-dontwarn com.huawei.** + +-keep class org.bouncycastle.** { *; } +-dontwarn org.bouncycastle.** + +-keep class io.flutter.** { *; } +-dontwarn io.flutter.** + +-keep class kotlinx.coroutines.** { *; } +-dontwarn kotlinx.coroutines.** + +-keep class **.R$* { *; } +-keepclassmembers class ** { @androidx.annotation.Keep *; } +-keep @androidx.annotation.Keep class * { *; } + +-keep public class * extends android.app.Application +-keep public class * extends android.content.ContentProvider + +-keep class * implements android.os.Parcelable { *; } +-keepclassmembers enum * { *; } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0d239126..a99f3deb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -11,6 +11,16 @@ android:usesCleartextTraffic="true" android:name="${applicationName}" android:icon="@mipmap/ic_launcher"> + + + + + + + when (call.method) { + "getHuaweiToken" -> { + CoroutineScope(Dispatchers.IO).launch { + try { + // Try to read appId from agconnect-services.json + var appId: String? = try { + AGConnectServicesConfig.fromContext(applicationContext) + .getString("client/app_id") + } catch (_: Exception) { + null + } + + // Fallback to manifest meta-data if needed + if (appId.isNullOrBlank()) { + try { + val pm: PackageManager = applicationContext.packageManager + val ai: ApplicationInfo = pm.getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA) + val raw = ai.metaData?.getString("com.huawei.hms.client.appid") + if (!raw.isNullOrBlank()) { + appId = raw.removePrefix("appid=") + } + } catch (_: Exception) { + // ignore and handle below + } + } + + if (appId.isNullOrBlank()) { + runOnUiThread { + result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null) + } + return@launch + } + val token = HmsInstanceId.getInstance(applicationContext) + .getToken(appId, "HCM") + runOnUiThread { + result.success(token) + } + } catch (e: Exception) { + runOnUiThread { + result.error("HMS_TOKEN_ERROR", e.message, null) + } + } + } + } + else -> result.notImplemented() + } + } + } } diff --git a/android/build.gradle b/android/build.gradle index 9db63328..03774aff 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,3 +1,16 @@ +buildscript { + repositories { + google() + mavenCentral() + maven { url 'https://developer.huawei.com/repo/' } + gradlePluginPortal() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.12.0' + classpath 'com.huawei.agconnect:agcp:1.9.1.301' + } +} + allprojects { repositories { google() @@ -14,6 +27,32 @@ subprojects { project.evaluationDependsOn(":app") } +// Temporary safeguard: set missing namespaces for Android library modules based on their manifest package +subprojects { Project subproject -> + subproject.pluginManager.withPlugin('com.android.library') { + def androidExt = subproject.extensions.findByName('android') + if (androidExt != null) { + def hasNamespaceProp = androidExt.hasProperty('namespace') + def currentNamespace = hasNamespaceProp ? androidExt.namespace : null + if (hasNamespaceProp && !currentNamespace) { + def manifestFile = file("${subproject.projectDir}/src/main/AndroidManifest.xml") + if (manifestFile.exists()) { + try { + def manifest = new groovy.xml.XmlSlurper().parse(manifestFile) + def manifestPackage = manifest.@package?.text() + if (manifestPackage) { + androidExt.namespace = manifestPackage + subproject.logger.lifecycle("[namespace-fix] Set namespace for ${subproject.path} to '${manifestPackage}' from AndroidManifest.xml") + } + } catch (Throwable t) { + subproject.logger.warn("[namespace-fix] Failed to read manifest for ${subproject.path}: ${t.message}") + } + } + } + } + } +} + tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index 4921e6e4..ce07f9e3 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,12 +650,12 @@ code + .copy-button { diff --git a/android/settings.gradle b/android/settings.gradle index e173b2c9..76846d0d 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -12,6 +12,7 @@ pluginManagement { repositories { google() mavenCentral() + maven { url 'https://developer.huawei.com/repo/' } gradlePluginPortal() } } diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index 2ba89290..2d89bd43 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -1,28 +1,27 @@ import 'dart:developer'; import 'dart:io'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:flutter/services.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:huawei_push/huawei_push.dart'; +import 'package:flutter/services.dart' show MethodChannel, PlatformException; class FirebaseApi { final _firebaseMessaging = FirebaseMessaging.instance; Future getToken() async { - await _firebaseMessaging.requestPermission(); + String? token; - String? token = await _firebaseMessaging.getToken(); - - if (token == null && Platform.isAndroid) { - final isHuawei = await _isHuaweiDevice(); - if (isHuawei) { - token = await _getHuaweiPushToken(); - } + if (await _isHuaweiDevice()) { + token = await _getHuaweiPushTokenNative(); + } else { + await _firebaseMessaging.requestPermission(); + token = await _firebaseMessaging.getToken(); } - return token; } @@ -30,10 +29,11 @@ class FirebaseApi { if (await TokenStorage.haveTokenInStorage()) { try { final deviceInfo = DeviceInfoPlugin(); + final isHuawei = Platform.isAndroid ? await _isHuaweiDevice() : false; await sl().post( '/api/profile/notification/token/register', data: { - 'platform': Platform.isIOS ? 'ios' : 'android', + 'platform': Platform.isIOS ? 'ios' : (isHuawei ? 'huawei' : 'android'), 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, 'token': await getToken(), }, @@ -47,33 +47,27 @@ class FirebaseApi { Future _isHuaweiDevice() async { if (!Platform.isAndroid) return false; try { - final android = await DeviceInfoPlugin().androidInfo; - final brand = android.brand.toLowerCase(); - final manufacturer = android.manufacturer.toLowerCase(); - return brand.contains('huawei') || manufacturer.contains('huawei') || brand.contains('honor') || manufacturer.contains('honor'); + final info = await DeviceInfoPlugin().androidInfo; + final brand = info.brand.toLowerCase(); + final manufacturer = info.manufacturer.toLowerCase(); + return brand.contains('huawei') || brand.contains('honor') || manufacturer.contains('huawei') || manufacturer.contains('honor'); } catch (_) { return false; } } - Future _getHuaweiPushToken() async { + Future _getHuaweiPushTokenNative() async { try { - await Push.turnOnPush(); - - String? resolvedToken; - final sub = Push.getTokenStream.listen((t) => resolvedToken ??= t, onError: (_) {}); - - Push.getToken(''); - - final started = DateTime.now(); - while (resolvedToken == null && DateTime.now().difference(started).inSeconds < 5) { - await Future.delayed(const Duration(milliseconds: 100)); - } - - await sub.cancel(); - return resolvedToken; + const channel = MethodChannel('com.eatvioapp/hms_push'); + final String? token = await channel.invokeMethod('getHuaweiToken'); + return token; + } on PlatformException catch (e, st) { + showErrorToast('$e $st'); + log(e.toString(), stackTrace: st); + return null; } catch (e, st) { - log('Huawei Push token error: $e', stackTrace: st); + showErrorToast('$e $st'); + log(e.toString(), stackTrace: st); return null; } } diff --git a/pubspec.yaml b/pubspec.yaml index a62a13af..98a4429f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,7 +74,6 @@ dependencies: flutter_dotenv: ^5.2.1 uuid: ^4.5.1 appmetrica_plugin: ^3.3.0 - huawei_push: ^6.11.0+300 vk_id: ^1.0.3 flutter_login_yandex: ^0.0.5 From 46d0e31f5f974b09f957eaef545b39ead9ae6bc5 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 29 Sep 2025 13:29:39 +0300 Subject: [PATCH 16/49] fix(huawei-push): Issue-660 --- .gitignore | 4 +++- .../sessions/kotlin-compiler-10039299475187077818.salive | 0 android/app/build.gradle | 5 +++-- android/build.gradle | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive diff --git a/.gitignore b/.gitignore index 06e89a5e..afeb23f8 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,6 @@ android/build/ ios/XCBuildData/ env/.env -.env \ No newline at end of file +.env +android/key.properties +app/upload-keystore.jks \ No newline at end of file diff --git a/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive b/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive new file mode 100644 index 00000000..e69de29b diff --git a/android/app/build.gradle b/android/app/build.gradle index 15095cb5..8fc22d6d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -68,9 +68,10 @@ android { } dependencies { - implementation 'com.huawei.hms:push:6.13.0.300' + implementation 'com.huawei.agconnect:agconnect-core:1.9.1.301' + implementation 'com.huawei.hms:push:6.12.0.300' // Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8 - implementation 'com.huawei.hms:hianalytics:6.13.0.300' + implementation 'com.huawei.hms:hianalytics:6.12.0.300' // BouncyCastle provider required by Huawei secure utils used transitively by HMS implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1' } diff --git a/android/build.gradle b/android/build.gradle index 03774aff..af06c34c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ buildscript { repositories { google() mavenCentral() - maven { url 'https://developer.huawei.com/repo/' } + maven {url 'https://developer.huawei.com/repo/'} gradlePluginPortal() } dependencies { @@ -15,7 +15,7 @@ allprojects { repositories { google() mavenCentral() - maven { url 'https://developer.huawei.com/repo/' } + maven {url 'https://developer.huawei.com/repo/'} } } From 24fe984e6bd63f2468fae0c8c73bb540dc351c02 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 29 Sep 2025 23:45:18 +0300 Subject: [PATCH 17/49] feat(auth): Issue-691 --- ...otlin-compiler-10039299475187077818.salive | 0 assets/icons/mail_logo.svg | 11 +++ assets/icons/ok_logo.svg | 5 + assets/icons/vk_logo.svg | 16 ++++ assets/icons/yandex_logo.svg | 16 ++++ .../auth/presentation/pages/auth_screen.dart | 92 +++++++++---------- 6 files changed, 91 insertions(+), 49 deletions(-) delete mode 100644 android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive create mode 100644 assets/icons/mail_logo.svg create mode 100644 assets/icons/ok_logo.svg create mode 100644 assets/icons/vk_logo.svg create mode 100644 assets/icons/yandex_logo.svg diff --git a/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive b/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive deleted file mode 100644 index e69de29b..00000000 diff --git a/assets/icons/mail_logo.svg b/assets/icons/mail_logo.svg new file mode 100644 index 00000000..5891b116 --- /dev/null +++ b/assets/icons/mail_logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/ok_logo.svg b/assets/icons/ok_logo.svg new file mode 100644 index 00000000..07a62fbd --- /dev/null +++ b/assets/icons/ok_logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/vk_logo.svg b/assets/icons/vk_logo.svg new file mode 100644 index 00000000..274b3486 --- /dev/null +++ b/assets/icons/vk_logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/yandex_logo.svg b/assets/icons/yandex_logo.svg new file mode 100644 index 00000000..51b51eee --- /dev/null +++ b/assets/icons/yandex_logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index f9eeddf5..970ea8d3 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -9,6 +9,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; @@ -119,6 +120,42 @@ class _AuthScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Social auth buttons at the top (outlined) + const Text('Авторизация через:', style: TextStyle(fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _SocialButton( + onTap: () => _onVkPressed(context), + backgroundColor: const Color(0xFF2787F5), + tooltip: 'VK ID', + child: SvgPicture.asset('assets/icons/vk_logo.svg', height: 24, width: 24), + ), + _SocialButton( + onTap: _onOkPressed, + backgroundColor: const Color(0xFFF58220), + tooltip: 'Одноклассники', + child: SvgPicture.asset('assets/icons/ok_logo.svg', height: 24, width: 24), + ), + _SocialButton( + onTap: _onMailRuPressed, + backgroundColor: const Color(0xFF005FF9), + tooltip: 'Mail.ru', + child: SvgPicture.asset('assets/icons/mail_logo.svg', height: 24, width: 24), + ), + _SocialButton( + onTap: _onYandexPressed, + backgroundColor: const Color(0xFFFFCC00), + tooltip: 'Яндекс ID', + child: SvgPicture.asset('assets/icons/yandex_logo.svg', height: 24, width: 24), + ), + ], + ), + const SizedBox(height: 16), + const CustomDivider(), + const SizedBox(height: 8), + const SizedBox(height: 16), Text(s.email), const SizedBox(height: 4), CustomTextFormField( @@ -168,52 +205,6 @@ class _AuthScreenState extends State { ), ], ), - // const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _SocialButton( - onTap: () => _onVkPressed(context), - backgroundColor: const Color(0xFF2787F5), - tooltip: 'VK ID', - child: SvgPicture.asset( - 'assets/icons/vk.svg', - height: 24, - width: 24, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - ), - _SocialButton( - onTap: _onOkPressed, - backgroundColor: const Color(0xFFF58220), - tooltip: 'Одноклассники', - child: SvgPicture.asset( - 'assets/icons/ok.svg', - height: 24, - width: 24, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - ), - const _SocialButton( - onTap: _onMailRuPressed, - backgroundColor: Color(0xFF005FF9), - tooltip: 'Mail.ru', - child: Text( - 'Mail', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), - ), - ), - const _SocialButton( - onTap: _onYandexPressed, - backgroundColor: Color(0xFFFFCC00), - tooltip: 'Яндекс ID', - child: Text( - 'Yandex', - style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), - ), - ), - ], - ), const Spacer(), Center( child: RichText( @@ -367,7 +358,6 @@ class _AuthScreenState extends State { } } -// ignore: unused_element class _SocialButton extends StatelessWidget { const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip}); @@ -381,12 +371,16 @@ class _SocialButton extends StatelessWidget { return Tooltip( message: tooltip, child: InkWell( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(20), onTap: onTap, child: Ink( height: 48, width: (MediaQuery.of(context).size.width - 16 * 2 - 12 * 3) / 4, - decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(12)), + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: const Color(0xFFDBDBDB), width: 1), + ), child: Center(child: child), ), ), From 27376ca54d244b13d425b5c6c0b31fddc01a1231 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 30 Sep 2025 00:39:21 +0300 Subject: [PATCH 18/49] feat(auth): Issue-691 --- ENV_SETUP.md | 9 + lib/core/env_config.dart | 8 + .../auth/presentation/pages/auth_screen.dart | 67 +++++--- lib/features/auth/sdk/social_auth_facade.dart | 158 +++++++++++++----- lib/main.dart | 2 + 5 files changed, 184 insertions(+), 60 deletions(-) diff --git a/ENV_SETUP.md b/ENV_SETUP.md index 0873fb1c..092a3bd9 100644 --- a/ENV_SETUP.md +++ b/ENV_SETUP.md @@ -54,6 +54,15 @@ cp env.example .env - `GOOGLE_CHECK_URL` - URL для проверки соединения (Google) - `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex) +### VK ID Configuration +- `VK_CLIENT_ID` - ID приложения VK (числовой) +- **Примечание**: VK_CLIENT_ID - это числовой "ID приложения" из консоли VK ID, НЕ "Защищённый ключ" или "Сервисный ключ доступа" + +### Yandex ID Configuration +- `YANDEX_CLIENT_ID` - ID приложения Yandex OAuth +- `YANDEX_CLIENT_SECRET` - секретный ключ приложения Yandex OAuth +- `YANDEX_REDIRECT_URI` - URI для перенаправления (должен совпадать с настройками в Yandex OAuth приложении) + ## Безопасность ⚠️ **ВАЖНО**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле. diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart index a3404aeb..446bc65b 100644 --- a/lib/core/env_config.dart +++ b/lib/core/env_config.dart @@ -43,4 +43,12 @@ class EnvConfig { // Connection Check URLs static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? ''; static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? ''; + + // VK ID Configuration + static String get vkClientId => dotenv.env['VK_CLIENT_ID'] ?? ''; + + // Yandex ID Configuration + static String get yandexClientId => dotenv.env['YANDEX_CLIENT_ID'] ?? ''; + static String get yandexClientSecret => dotenv.env['YANDEX_CLIENT_SECRET'] ?? ''; + static String get yandexRedirectUri => dotenv.env['YANDEX_REDIRECT_URI'] ?? ''; } diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 970ea8d3..5f9496ac 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -388,35 +388,64 @@ class _SocialButton extends StatelessWidget { } } -// ignore: unused_element void _onVkPressed(BuildContext context) async { - final accessToken = await SocialAuthFacade.signInWithVk(context: context); - if (accessToken == null) return; - final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); - if (isAuthenticated) { - if (AppRouter.navigatorKey.currentContext != null) { - // Reuse success flow - // ignore: use_build_context_synchronously - await _reconnectAfterAuth(); + print('[VK] VK button pressed'); + try { + final accessToken = await SocialAuthFacade.signInWithVk(context: context); + print('[VK] Got access token: ${accessToken != null ? 'success' : 'null'}'); + if (accessToken == null) { + print('[VK] No access token received'); + return; } + final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); + print('[VK] Authentication result: $isAuthenticated'); + if (isAuthenticated) { + if (AppRouter.navigatorKey.currentContext != null) { + // Reuse success flow + // ignore: use_build_context_synchronously + await _reconnectAfterAuth(); + } + } + } catch (e) { + print('[VK] Error during VK auth: $e'); } } void _onOkPressed() async { - final accessToken = await SocialAuthFacade.signInWithOk(); - if (accessToken == null) return; - final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); - if (isAuthenticated) { - await _reconnectAfterAuth(); + print('[OK] OK button pressed'); + try { + final accessToken = await SocialAuthFacade.signInWithOk(); + print('[OK] Got access token: ${accessToken != null ? 'success' : 'null'}'); + if (accessToken == null) { + print('[OK] No access token received'); + return; + } + final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); + print('[OK] Authentication result: $isAuthenticated'); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } + } catch (e) { + print('[OK] Error during OK auth: $e'); } } void _onMailRuPressed() async { - final accessToken = await SocialAuthFacade.signInWithMailRu(); - if (accessToken == null) return; - final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); - if (isAuthenticated) { - await _reconnectAfterAuth(); + print('[MailRu] MailRu button pressed'); + try { + final accessToken = await SocialAuthFacade.signInWithMailRu(); + print('[MailRu] Got access token: ${accessToken != null ? 'success' : 'null'}'); + if (accessToken == null) { + print('[MailRu] No access token received'); + return; + } + final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); + print('[MailRu] Authentication result: $isAuthenticated'); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } + } catch (e) { + print('[MailRu] Error during MailRu auth: $e'); } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index e5761498..90d07e95 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -1,10 +1,9 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_login_yandex/flutter_login_yandex.dart'; import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/env_config.dart'; import 'package:vk_id/vk_id.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -29,26 +28,52 @@ class SocialAuthFacade { } static Future signInWithVkAndFetchProfile({required BuildContext context}) async { - final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID']; - if (clientIdStr == null || clientIdStr.isEmpty) { + final clientIdStr = EnvConfig.vkClientId.trim(); + print('[VK] VK_CLIENT_ID from EnvConfig: $clientIdStr'); + if (clientIdStr.isEmpty) { + print('[VK] VK_CLIENT_ID is not set. Configure it in .env'); return null; } try { + print('[VK] Creating VkIDController with clientId: $clientIdStr'); dynamic controller = VkIDController(clID: int.parse(clientIdStr)); final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); + print('[VK] Generated auth URI: $authUri'); + + // Extract URL from MapEntry + String authUrl; + if (authUri is MapEntry) { + authUrl = authUri.value.toString(); + } else { + authUrl = authUri.toString(); + } + print('[VK] Extracted auth URL: $authUrl'); + final redirectScheme = 'vk$clientIdStr://'; - final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme); - if (redirectParams == null) return null; + print('[VK] Redirect scheme: $redirectScheme'); + + final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); + print('[VK] WebView returned params: $redirectParams'); + if (redirectParams == null) { + print('[VK] No redirect params received'); + return null; + } + final code = redirectParams['code']; final deviceId = redirectParams['device_id']; - if (code == null || deviceId == null) return null; + print('[VK] Code: $code, DeviceId: $deviceId'); + if (code == null || deviceId == null) { + print('[VK] Missing code or deviceId'); + return null; + } - final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId); - final accessToken = _extractAccessToken(tokenResponse); - if (accessToken == null) return null; - return VkAuthResult(accessToken: accessToken); - } catch (_) { + // Instead of exchanging the code on-device (the sdk method is not available), + // return the authorization code so the backend can exchange it securely. + print('[VK] Returning authorization code to backend for exchange'); + return VkAuthResult(accessToken: code); + } catch (e) { + print('[VK] Error in signInWithVkAndFetchProfile: $e'); return null; } } @@ -66,10 +91,14 @@ class SocialAuthFacade { } static Future signInWithYandex() async { - final clientId = dotenv.maybeGet('YANDEX_CLIENT_ID')?.trim() ?? Platform.environment['YANDEX_CLIENT_ID']; - final redirectUri = dotenv.maybeGet('YANDEX_REDIRECT_URI')?.trim() ?? Platform.environment['YANDEX_REDIRECT_URI']; + final clientId = EnvConfig.yandexClientId.trim(); + final redirectUri = EnvConfig.yandexRedirectUri.trim(); - if (clientId == null || clientId.isEmpty || redirectUri == null || redirectUri.isEmpty) { + print('[Yandex] Client ID: $clientId'); + print('[Yandex] Redirect URI: $redirectUri'); + + if (clientId.isEmpty || redirectUri.isEmpty) { + print('[Yandex] Missing YANDEX_CLIENT_ID or YANDEX_REDIRECT_URI in .env'); return null; } try { @@ -83,73 +112,120 @@ class SocialAuthFacade { } static Future _signInWithVkIdProvider({required BuildContext context, required String provider}) async { - final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID']; - if (clientIdStr == null || clientIdStr.isEmpty) { + final clientIdStr = EnvConfig.vkClientId.trim(); + if (clientIdStr.isEmpty) { return null; } try { + print('[VK] Provider flow start: $provider, clientId: $clientIdStr'); dynamic controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); + + // Try different approaches for provider parameter + dynamic authUri; + try { + // First try with provider as string + authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); + print('[VK] [$provider] Generated auth URI with string provider: $authUri'); + } catch (e) { + print('[VK] [$provider] Failed with string provider, trying null: $e'); + try { + // Try with null provider + authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: null); + print('[VK] [$provider] Generated auth URI with null provider: $authUri'); + } catch (e2) { + print('[VK] [$provider] Failed with null provider, trying without provider: $e2'); + // Try without provider parameter + authUri = controller.generateAuthorizeLinkWithCodeVerifier(); + print('[VK] [$provider] Generated auth URI without provider: $authUri'); + } + } + + // Extract URL from MapEntry + String authUrl; + if (authUri is MapEntry) { + authUrl = authUri.value.toString(); + } else { + authUrl = authUri.toString(); + } + print('[VK] [$provider] Extracted auth URL: $authUrl'); + final redirectScheme = 'vk$clientIdStr://'; - final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme); + print('[VK] [$provider] Redirect scheme: $redirectScheme'); + final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); + print('[VK] [$provider] WebView returned params: $redirectParams'); if (redirectParams == null) return null; final code = redirectParams['code']; final deviceId = redirectParams['device_id']; + print('[VK] [$provider] Code: $code, DeviceId: $deviceId'); if (code == null || deviceId == null) return null; - final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId, provider: provider); - final accessToken = _extractAccessToken(tokenResponse); - return accessToken; - } catch (_) { + // Return the authorization code to be exchanged by the backend for provider flow + return code; + } catch (e) { + print('[VK] [$provider] Error in provider flow: $e'); return null; } } - static String? _extractAccessToken(dynamic tokenResponse) { - if (tokenResponse == null) return null; - if (tokenResponse is Map) { - return (tokenResponse['access_token'] as String?) ?? (tokenResponse['token'] as String?); - } - try { - final token = (tokenResponse as dynamic).accessToken as String?; - if (token != null && token.isNotEmpty) return token; - } catch (_) {} - try { - final token = (tokenResponse as dynamic).token as String?; - if (token != null && token.isNotEmpty) return token; - } catch (_) {} - return null; - } + // Token extraction is no longer needed because we return the authorization code + // to the backend for exchange. static Future?> _presentOAuthWebView({ required BuildContext context, required String initialUrl, required String redirectSchemePrefix, }) async { + print('[VK] WebView: Loading URL: $initialUrl'); + print('[VK] WebView: Redirect scheme: $redirectSchemePrefix'); + final controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onNavigationRequest: (request) { final url = request.url; + print('[VK] WebView: Navigation to: $url'); if (url.startsWith(redirectSchemePrefix)) { + print('[VK] WebView: Redirect detected, parsing params'); final uri = Uri.parse(url); final params = {}..addAll(uri.queryParameters); + print('[VK] WebView: Parsed params: $params'); Navigator.of(context, rootNavigator: true).pop(params); return NavigationDecision.prevent; } return NavigationDecision.navigate; }, + onPageStarted: (url) { + print('[VK] WebView: Page started loading: $url'); + }, + onPageFinished: (url) { + print('[VK] WebView: Page finished loading: $url'); + }, + onWebResourceError: (error) { + print('[VK] WebView: Error: ${error.description}'); + }, ), - ) - ..loadRequest(Uri.parse(initialUrl)); + ); + + try { + await controller.loadRequest(Uri.parse(initialUrl)); + } catch (e) { + print('[VK] WebView: Failed to load URL: $e'); + return null; + } return showDialog>( context: context, barrierDismissible: false, builder: (_) { + const borderRadius = BorderRadius.all(Radius.circular(16)); return Dialog( insetPadding: const EdgeInsets.all(16), - child: SizedBox(height: 560, child: WebViewWidget(controller: controller)), + shape: const RoundedRectangleBorder(borderRadius: borderRadius), + elevation: 0, + child: ClipRRect( + borderRadius: borderRadius, + child: SizedBox(height: 560, child: WebViewWidget(controller: controller)), + ), ); }, ); diff --git a/lib/main.dart b/lib/main.dart index d716f3fa..e0492219 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,6 +31,8 @@ Future main() async { FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); await dotenv.load(fileName: ".env"); + print('[ENV] Loaded .env file'); + print('[ENV] VK_CLIENT_ID: ${dotenv.maybeGet('VK_CLIENT_ID')}'); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await FirebaseMessaging.instance.requestPermission(); From b65fca8116a90fde2241ae69f209df84e292ca14 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 30 Sep 2025 16:11:30 +0300 Subject: [PATCH 19/49] feat(auth): Issue-691 --- .../auth/presentation/pages/auth_screen.dart | 17 --- lib/features/auth/sdk/social_auth_facade.dart | 102 ++++++++---------- lib/main.dart | 2 - 3 files changed, 47 insertions(+), 74 deletions(-) diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 5f9496ac..30ef29cf 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -389,63 +389,46 @@ class _SocialButton extends StatelessWidget { } void _onVkPressed(BuildContext context) async { - print('[VK] VK button pressed'); try { final accessToken = await SocialAuthFacade.signInWithVk(context: context); - print('[VK] Got access token: ${accessToken != null ? 'success' : 'null'}'); if (accessToken == null) { - print('[VK] No access token received'); return; } final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); - print('[VK] Authentication result: $isAuthenticated'); if (isAuthenticated) { if (AppRouter.navigatorKey.currentContext != null) { - // Reuse success flow - // ignore: use_build_context_synchronously await _reconnectAfterAuth(); } } } catch (e) { - print('[VK] Error during VK auth: $e'); } } void _onOkPressed() async { - print('[OK] OK button pressed'); try { final accessToken = await SocialAuthFacade.signInWithOk(); - print('[OK] Got access token: ${accessToken != null ? 'success' : 'null'}'); if (accessToken == null) { - print('[OK] No access token received'); return; } final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); - print('[OK] Authentication result: $isAuthenticated'); if (isAuthenticated) { await _reconnectAfterAuth(); } } catch (e) { - print('[OK] Error during OK auth: $e'); } } void _onMailRuPressed() async { - print('[MailRu] MailRu button pressed'); try { final accessToken = await SocialAuthFacade.signInWithMailRu(); - print('[MailRu] Got access token: ${accessToken != null ? 'success' : 'null'}'); if (accessToken == null) { - print('[MailRu] No access token received'); return; } final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); - print('[MailRu] Authentication result: $isAuthenticated'); if (isAuthenticated) { await _reconnectAfterAuth(); } } catch (e) { - print('[MailRu] Error during MailRu auth: $e'); } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 90d07e95..74959498 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -29,51 +29,35 @@ class SocialAuthFacade { static Future signInWithVkAndFetchProfile({required BuildContext context}) async { final clientIdStr = EnvConfig.vkClientId.trim(); - print('[VK] VK_CLIENT_ID from EnvConfig: $clientIdStr'); if (clientIdStr.isEmpty) { - print('[VK] VK_CLIENT_ID is not set. Configure it in .env'); return null; } try { - print('[VK] Creating VkIDController with clientId: $clientIdStr'); dynamic controller = VkIDController(clID: int.parse(clientIdStr)); final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); - print('[VK] Generated auth URI: $authUri'); - // Extract URL from MapEntry String authUrl; if (authUri is MapEntry) { authUrl = authUri.value.toString(); } else { authUrl = authUri.toString(); } - print('[VK] Extracted auth URL: $authUrl'); final redirectScheme = 'vk$clientIdStr://'; - print('[VK] Redirect scheme: $redirectScheme'); final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); - print('[VK] WebView returned params: $redirectParams'); if (redirectParams == null) { - print('[VK] No redirect params received'); return null; } final code = redirectParams['code']; final deviceId = redirectParams['device_id']; - print('[VK] Code: $code, DeviceId: $deviceId'); if (code == null || deviceId == null) { - print('[VK] Missing code or deviceId'); return null; } - - // Instead of exchanging the code on-device (the sdk method is not available), - // return the authorization code so the backend can exchange it securely. - print('[VK] Returning authorization code to backend for exchange'); return VkAuthResult(accessToken: code); } catch (e) { - print('[VK] Error in signInWithVkAndFetchProfile: $e'); return null; } } @@ -94,20 +78,47 @@ class SocialAuthFacade { final clientId = EnvConfig.yandexClientId.trim(); final redirectUri = EnvConfig.yandexRedirectUri.trim(); - print('[Yandex] Client ID: $clientId'); - print('[Yandex] Redirect URI: $redirectUri'); - if (clientId.isEmpty || redirectUri.isEmpty) { - print('[Yandex] Missing YANDEX_CLIENT_ID or YANDEX_REDIRECT_URI in .env'); return null; } + try { - final plugin = FlutterLoginYandex(); - final response = await plugin.signIn(); - final token = (response is Map) ? response['token'] as String? : null; - return (token != null && token.isNotEmpty) ? token : null; - } catch (_) { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return null; + + final authorizeUrl = Uri.https('oauth.yandex.ru', '/authorize', { + 'response_type': 'token', + 'client_id': clientId, + 'redirect_uri': redirectUri, + 'force_confirm': 'yes', + }).toString(); + + final redirectSchemePrefix = redirectUri; + + final params = await _presentOAuthWebView(context: context, initialUrl: authorizeUrl, redirectSchemePrefix: redirectSchemePrefix); + + if (params == null) return null; + + final token = params['access_token']; + if (token != null && token.isNotEmpty) { + return token; + } + + final code = params['code']; + if (code != null && code.isNotEmpty) { + return code; + } + return null; + } catch (e) { + try { + final plugin = FlutterLoginYandex(); + final response = await plugin.signIn(); + final token = (response is Map) ? response['token'] as String? : null; + return (token != null && token.isNotEmpty) ? token : null; + } catch (_) { + return null; + } } } @@ -117,26 +128,16 @@ class SocialAuthFacade { return null; } try { - print('[VK] Provider flow start: $provider, clientId: $clientIdStr'); dynamic controller = VkIDController(clID: int.parse(clientIdStr)); - // Try different approaches for provider parameter dynamic authUri; try { - // First try with provider as string authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); - print('[VK] [$provider] Generated auth URI with string provider: $authUri'); } catch (e) { - print('[VK] [$provider] Failed with string provider, trying null: $e'); try { - // Try with null provider authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: null); - print('[VK] [$provider] Generated auth URI with null provider: $authUri'); } catch (e2) { - print('[VK] [$provider] Failed with null provider, trying without provider: $e2'); - // Try without provider parameter authUri = controller.generateAuthorizeLinkWithCodeVerifier(); - print('[VK] [$provider] Generated auth URI without provider: $authUri'); } } @@ -147,61 +148,53 @@ class SocialAuthFacade { } else { authUrl = authUri.toString(); } - print('[VK] [$provider] Extracted auth URL: $authUrl'); - final redirectScheme = 'vk$clientIdStr://'; - print('[VK] [$provider] Redirect scheme: $redirectScheme'); final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); - print('[VK] [$provider] WebView returned params: $redirectParams'); if (redirectParams == null) return null; final code = redirectParams['code']; final deviceId = redirectParams['device_id']; - print('[VK] [$provider] Code: $code, DeviceId: $deviceId'); if (code == null || deviceId == null) return null; - // Return the authorization code to be exchanged by the backend for provider flow return code; } catch (e) { - print('[VK] [$provider] Error in provider flow: $e'); return null; } } - // Token extraction is no longer needed because we return the authorization code - // to the backend for exchange. - static Future?> _presentOAuthWebView({ required BuildContext context, required String initialUrl, required String redirectSchemePrefix, }) async { - print('[VK] WebView: Loading URL: $initialUrl'); - print('[VK] WebView: Redirect scheme: $redirectSchemePrefix'); - final controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onNavigationRequest: (request) { final url = request.url; - print('[VK] WebView: Navigation to: $url'); if (url.startsWith(redirectSchemePrefix)) { - print('[VK] WebView: Redirect detected, parsing params'); final uri = Uri.parse(url); final params = {}..addAll(uri.queryParameters); - print('[VK] WebView: Parsed params: $params'); + if (uri.fragment.isNotEmpty) { + final fragPairs = uri.fragment.split('&'); + for (final pair in fragPairs) { + final eq = pair.indexOf('='); + if (eq > 0 && eq < pair.length - 1) { + final key = Uri.decodeComponent(pair.substring(0, eq)); + final value = Uri.decodeComponent(pair.substring(eq + 1)); + params[key] = value; + } + } + } Navigator.of(context, rootNavigator: true).pop(params); return NavigationDecision.prevent; } return NavigationDecision.navigate; }, onPageStarted: (url) { - print('[VK] WebView: Page started loading: $url'); }, onPageFinished: (url) { - print('[VK] WebView: Page finished loading: $url'); }, onWebResourceError: (error) { - print('[VK] WebView: Error: ${error.description}'); }, ), ); @@ -209,7 +202,6 @@ class SocialAuthFacade { try { await controller.loadRequest(Uri.parse(initialUrl)); } catch (e) { - print('[VK] WebView: Failed to load URL: $e'); return null; } diff --git a/lib/main.dart b/lib/main.dart index e0492219..d716f3fa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,8 +31,6 @@ Future main() async { FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); await dotenv.load(fileName: ".env"); - print('[ENV] Loaded .env file'); - print('[ENV] VK_CLIENT_ID: ${dotenv.maybeGet('VK_CLIENT_ID')}'); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await FirebaseMessaging.instance.requestPermission(); From f1945202fe7b476e21932deef5114b377c65c798 Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 30 Sep 2025 17:33:30 +0300 Subject: [PATCH 20/49] feat(internetConnection): changed logic for internet connection checking and displaying state --- .../internet_connection_cubit.dart | 73 +++++++++++++++++++ .../internet_connection_state.dart | 13 ++++ lib/core/locator_service.dart | 3 + lib/core/utils/toasts.dart | 14 +++- lib/main.dart | 8 +- 5 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 lib/core/internet_connection_cubit/internet_connection_cubit.dart create mode 100644 lib/core/internet_connection_cubit/internet_connection_state.dart diff --git a/lib/core/internet_connection_cubit/internet_connection_cubit.dart b/lib/core/internet_connection_cubit/internet_connection_cubit.dart new file mode 100644 index 00000000..fa9a3527 --- /dev/null +++ b/lib/core/internet_connection_cubit/internet_connection_cubit.dart @@ -0,0 +1,73 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:eatvio_app/core/env_config.dart'; +import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_state.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:http/http.dart' as http; + +class InternetConnectionCubit extends Cubit { + InternetConnectionCubit() : super(InternetConnectionState()) { + checkConnection(); + _connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateAccessStatus); + _listener = AppLifecycleListener( + onResume: () async { + _connectivitySubscription.resume(); + await checkConnection(); + }, + onHide: _connectivitySubscription.pause, + onPause: _connectivitySubscription.pause, + ); + } + + final Connectivity _connectivity = Connectivity(); + late StreamSubscription> _connectivitySubscription; + late final AppLifecycleListener _listener; + + Future checkConnection() async { + late List result; + + try { + result = await _connectivity.checkConnectivity(); + } on PlatformException catch (e) { + log('Couldn\'t check connectivity status', error: e); + return; + } + + return _updateAccessStatus(result); + } + + Future _updateAccessStatus(List result) async { + final hasInternet = await _checkInternetAccess(result); + showInternetConnectionToast(); + emit(state.copyWith(isInternetAvailable: hasInternet)); + } + + Future _checkInternetAccess(List result) async { + if (result.contains(ConnectivityResult.none)) { + return false; + } + + final uris = [Uri.parse(EnvConfig.googleCheckUrl), Uri.parse(EnvConfig.yandexCheckUrl), Uri.parse(EnvConfig.devBaseUrl), Uri.parse(EnvConfig.baseUrl)]; + for (final uri in uris) { + try { + final response = await http.get(uri).timeout(const Duration(seconds: 3)); + return response.statusCode >= 100 && response.statusCode < 600; + } catch (e) { + continue; + } + } + return false; + } + + @override + Future close() { + _connectivitySubscription.cancel(); + _listener.dispose(); + return super.close(); + } +} diff --git a/lib/core/internet_connection_cubit/internet_connection_state.dart b/lib/core/internet_connection_cubit/internet_connection_state.dart new file mode 100644 index 00000000..95c831e1 --- /dev/null +++ b/lib/core/internet_connection_cubit/internet_connection_state.dart @@ -0,0 +1,13 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'internet_connection_state.freezed.dart'; + + +@freezed +abstract class InternetConnectionState with _$InternetConnectionState { + + factory InternetConnectionState({ + @Default(true) bool isInternetAvailable, + }) = _InternetConnectionState; + +} \ No newline at end of file diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 37713f29..ffe747a9 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; +import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart'; import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; import 'package:eatvio_app/core/views/repository/views_repository.dart'; import 'package:eatvio_app/core/views/service/views_service.dart'; @@ -45,6 +46,7 @@ import 'package:eatvio_app/features/blog_post_create/services/blog_post_service. import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; +import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; @@ -128,6 +130,7 @@ Future init() async { sl.registerLazySingleton(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl())); sl.registerLazySingleton(() => ProfileAvatarCubit(profileRepository: sl())); sl.registerLazySingleton(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl())); + sl.registerLazySingleton(() => InternetConnectionCubit()); // Cubit factories sl.registerFactory(() => FeedCubit(feedRepository: sl())); diff --git a/lib/core/utils/toasts.dart b/lib/core/utils/toasts.dart index 3d4fda0d..4683cb86 100644 --- a/lib/core/utils/toasts.dart +++ b/lib/core/utils/toasts.dart @@ -38,4 +38,16 @@ void showSuccessToast(String message) { textColor: appTheme.whiteA700, fontSize: 16.0, ); -} \ No newline at end of file +} + +void showInternetConnectionToast() { + Fluttertoast.showToast( + msg: 'Проверьте подключение к интернету', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.TOP, + timeInSecForIosWeb: 3, + backgroundColor: appTheme.red, + textColor: Colors.white, + fontSize: 16.0, + ); +} diff --git a/lib/main.dart b/lib/main.dart index d716f3fa..c9e9a3d6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:io' as io; import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart'; import 'package:eatvio_app/core/locator_service.dart' as di; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -9,7 +10,6 @@ import 'package:eatvio_app/features/notifications/presentation/notifications_cub import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; -import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; import 'package:eatvio_app/firebase_options.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:firebase_core/firebase_core.dart'; @@ -58,20 +58,18 @@ class MyApp extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider(create: (context) => sl()), + BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()..getNotifications()), BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), BlocProvider(create: (context) => sl()..getMyProfile()), BlocProvider(create: (context) => sl()..getChatRoomsWithLoading()), + BlocProvider(create: (context) => sl()), ], child: Portal( child: KeyboardDismisser( child: MaterialApp.router( theme: ThemeHelper().themeData(), routerConfig: AppRouter.appRouter, - builder: (context, child) => BlocProvider( - create: (context) => sl(), - child: InternetConnectionCheckerScreen(child: child!), - ), debugShowCheckedModeBanner: false, localizationsDelegates: const [ S.delegate, From d18ad865909c556a7807053e680a1de484141bff Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 1 Oct 2025 17:24:48 +0300 Subject: [PATCH 21/49] feat(internetConnection): fixed yandex sign in compilation --- ...otlin-compiler-10039299475187077818.salive | 0 ios/Runner/AppDelegate.swift | 19 ++++++++++++++++++- ios/Runner/Info.plist | 19 +++++++++++++++++++ ios/Runner/Runner.entitlements | 1 + lib/core/const.dart | 2 +- .../internet_connection_cubit.dart | 2 +- lib/main.dart | 2 +- 7 files changed, 41 insertions(+), 4 deletions(-) delete mode 100644 android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive diff --git a/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive b/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive deleted file mode 100644 index e69de29b..00000000 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index b6363034..fc70b0b0 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ -import UIKit import Flutter +import UIKit +import YandexLoginSDK @main @objc class AppDelegate: FlutterAppDelegate { @@ -10,4 +11,20 @@ import Flutter GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + @available(iOS 8.0, *) + func application( + _ application: UIApplication, continue userActivity: NSUserActivity, + restorationHandler: @escaping ([Any]?) -> Void + ) -> Bool { + YXLSdk.shared.processUserActivity(userActivity) + return true + } + + override public func application( + _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return YXLSdk.shared.handleOpen( + url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String) + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7e82c5b2..3275dbdb 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -67,5 +67,24 @@ UIViewControllerBasedStatusBarAppearance + LSApplicationQueriesSchemes + + yandexauth + yandexauth2 + yandexauth4 + + YAClientId + 5399b03636104c6d920b87c1ad329254 + CFBundleURLTypes + + + CFBundleURLName + YandexLoginSDK + CFBundleURLSchemes + + yx5399b03636104c6d920b87c1ad329254 + + + diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 7b8ca2a8..4ed4ad00 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -8,6 +8,7 @@ applinks:eatvio.ru applinks:www.eatvio.ru + applinks:5399b03636104c6d920b87c1ad329254.oauth.yandex.ru diff --git a/lib/core/const.dart b/lib/core/const.dart index 98f7bc4a..a5f06149 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,6 +1,6 @@ import 'package:eatvio_app/core/env_config.dart'; -String baseUrl = EnvConfig.baseUrl; +String baseUrl = EnvConfig.devBaseUrl; bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: ''); diff --git a/lib/core/internet_connection_cubit/internet_connection_cubit.dart b/lib/core/internet_connection_cubit/internet_connection_cubit.dart index fa9a3527..67427c6c 100644 --- a/lib/core/internet_connection_cubit/internet_connection_cubit.dart +++ b/lib/core/internet_connection_cubit/internet_connection_cubit.dart @@ -43,7 +43,7 @@ class InternetConnectionCubit extends Cubit { Future _updateAccessStatus(List result) async { final hasInternet = await _checkInternetAccess(result); - showInternetConnectionToast(); + if (!hasInternet) showInternetConnectionToast(); emit(state.copyWith(isInternetAvailable: hasInternet)); } diff --git a/lib/main.dart b/lib/main.dart index c9e9a3d6..0343afd2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -63,7 +63,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), BlocProvider(create: (context) => sl()..getMyProfile()), BlocProvider(create: (context) => sl()..getChatRoomsWithLoading()), - BlocProvider(create: (context) => sl()), + BlocProvider(create: (context) => sl(), lazy: false), ], child: Portal( child: KeyboardDismisser( From 28658521240bcef909570070b18019bbb6568d29 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 1 Oct 2025 23:59:19 +0300 Subject: [PATCH 22/49] fix: refactor --- lib/core/app_config.dart | 53 +++++ lib/core/app_initializer.dart | 68 ++++++ lib/core/di/cubit_registrations.dart | 162 +++++++++++++ lib/core/di/external_registrations.dart | 33 +++ lib/core/di/repository_registrations.dart | 88 +++++++ lib/core/di/service_registrations.dart | 88 +++++++ lib/core/locator_service.dart | 276 ++-------------------- lib/main.dart | 68 +----- 8 files changed, 520 insertions(+), 316 deletions(-) create mode 100644 lib/core/app_config.dart create mode 100644 lib/core/app_initializer.dart create mode 100644 lib/core/di/cubit_registrations.dart create mode 100644 lib/core/di/external_registrations.dart create mode 100644 lib/core/di/repository_registrations.dart create mode 100644 lib/core/di/service_registrations.dart diff --git a/lib/core/app_config.dart b/lib/core/app_config.dart new file mode 100644 index 00000000..554a326f --- /dev/null +++ b/lib/core/app_config.dart @@ -0,0 +1,53 @@ +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +/// Конфигурация приложения +class AppConfig { + /// Список глобальных BlocProvider'ов + static List get globalBlocProviders => [ + BlocProvider(create: (context) => sl()), + BlocProvider( + create: (context) => sl()..getNotifications(), + ), + BlocProvider( + create: (context) => sl()..loadFeedBlogPostList(), + ), + BlocProvider( + create: (context) => sl()..getMyProfile(), + ), + BlocProvider( + create: (context) => sl()..getChatRoomsWithLoading(), + ), + ]; + + /// Делегаты локализации + static const List localizationDelegates = [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ]; + + /// Поддерживаемые локали + static const List supportedLocales = [ + Locale.fromSubtags(languageCode: 'ru'), + ]; + + /// Создание обертки для проверки интернет-соединения + static Widget createInternetConnectionWrapper(Widget child) { + return BlocProvider( + create: (context) => sl(), + child: InternetConnectionCheckerScreen(child: child), + ); + } +} diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart new file mode 100644 index 00000000..a3798a34 --- /dev/null +++ b/lib/core/app_initializer.dart @@ -0,0 +1,68 @@ +import 'dart:io' as io; +import 'package:eatvio_app/core/env_config.dart'; +import 'package:eatvio_app/core/locator_service.dart' as di; +import 'package:eatvio_app/firebase_options.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:appmetrica_plugin/appmetrica_plugin.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +/// Класс для инициализации приложения +class AppInitializer { + /// Инициализация всех сервисов приложения + static Future initialize() async { + await _initializeSentry(); + await _initializeEnvironment(); + await _initializeFirebase(); + await _initializeDependencyInjection(); + await _initializeDebugMode(); + await _initializeAnalytics(); + } + + /// Инициализация Sentry + static Future _initializeSentry() async { + final widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + } + + /// Загрузка переменных окружения + static Future _initializeEnvironment() async { + await dotenv.load(fileName: ".env"); + } + + /// Инициализация Firebase + static Future _initializeFirebase() async { + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await FirebaseMessaging.instance.requestPermission(); + } + + /// Инициализация системы внедрения зависимостей + static Future _initializeDependencyInjection() async { + await di.init(); + } + + /// Настройка режима отладки + static Future _initializeDebugMode() async { + if (kDebugMode) { + io.HttpClient.enableTimelineLogging = true; + } + } + + /// Инициализация аналитики + static Future _initializeAnalytics() async { + AppMetrica.activate(AppMetricaConfig(EnvConfig.appMetricaApiKey)); + AppMetrica.reportEvent('Application started'); + } + + /// Запуск приложения с Sentry + static Future runAppWithSentry(VoidCallback appRunner) async { + await SentryFlutter.init((options) { + options.dsn = EnvConfig.sentryDsn; + options.tracesSampleRate = 1.0; + options.debug = false; + }, appRunner: appRunner); + } +} diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart new file mode 100644 index 00000000..efd0dbd2 --- /dev/null +++ b/lib/core/di/cubit_registrations.dart @@ -0,0 +1,162 @@ +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; +import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart'; +import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; +import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; +import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart'; +import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans; +import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.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/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/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; +import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; +import 'package:get_it/get_it.dart'; + +/// Регистрация всех Cubit'ов в системе внедрения зависимостей +class CubitRegistrations { + static final sl = GetIt.instance; + + /// Регистрация всех Cubit'ов + static void registerAll() { + _registerSingletonCubits(); + _registerFactoryCubits(); + } + + /// Регистрация Cubit'ов как синглтонов + static void _registerSingletonCubits() { + sl.registerLazySingleton(() => NewMessagesCubit()); + sl.registerLazySingleton( + () => NotificationsCubit(wsManager: sl(), notificationsRepository: sl()), + ); + sl.registerLazySingleton( + () => ProfileAvatarCubit(profileRepository: sl()), + ); + sl.registerLazySingleton( + () => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl()), + ); + } + + /// Регистрация Cubit'ов как фабрик + static void _registerFactoryCubits() { + // Auth related cubits + sl.registerFactory(() => AuthCubit(authRepository: sl())); + sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); + sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); + sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); + + // Feed related cubits + sl.registerFactory(() => FeedCubit(feedRepository: sl())); + sl.registerFactory( + () => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()), + ); + sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); + sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); + sl.registerFactory(() => ConsultantsCubit(feedRepository: sl())); + sl.registerFactory(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl())); + + // Chat related cubits + sl.registerFactory( + () => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl()), + ); + + // Profile related cubits + sl.registerFactory( + () => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()), + ); + sl.registerFactory(() => profile_plans.PlansCubit(plansRepository: sl())); + sl.registerFactory(() => SubCubit(profileRepository: sl())); + + // Content creation cubits + sl.registerFactory( + () => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl()), + ); + sl.registerFactory(() => CreatePlanCubit(createPlanRepository: sl())); + sl.registerFactory( + () => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl()), + ); + sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); + + // Content viewing cubits + sl.registerFactory(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl())); + sl.registerFactory(() => AboutPlanCubit(planRepository: sl())); + sl.registerFactory(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl())); + sl.registerFactory(() => BlogPostCubit(sl())); + + // User content cubits + sl.registerFactory(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl())); + sl.registerFactory(() => MyPlansCubit(myPlansRepository: sl())); + sl.registerFactory(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl())); + + // Other feature cubits + sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); + sl.registerFactory( + () => DiaryCubit( + chatRepository: sl(), + diaryRepository: sl(), + glossaryRepository: sl(), + profileRepository: sl(), + dietRepository: sl(), + settingsRepository: sl(), + ), + ); + sl.registerFactory( + () => SettingsCubit( + profileRepository: sl(), + settingsRepository: sl(), + consultantRepository: sl(), + consultationsRepository: sl(), + ), + ); + sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); + sl.registerFactory(() => ComplainingCubit(repository: sl())); + sl.registerFactory(() => UserCardsCubit(cardsRepository: sl())); + sl.registerFactory(() => OrderInfoCubit(chatRepository: sl())); + sl.registerFactory(() => ConsultationsCubit(consultationsRepository: sl())); + sl.registerFactory(() => ConsultantCommentsCubit(diaryRepository: sl())); + sl.registerFactory(() => CancelPlanCubit(cancelPlanRepository: sl())); + sl.registerFactory(() => ConsultationsArchiveCubit(consultationsRepository: sl())); + sl.registerFactory(() => ConsultantFinancesCubit(consultationsRepository: sl())); + sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); + sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); + sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); + sl.registerFactory(() => ConsultantRatingCubit(sl())); + sl.registerFactory(() => ViewsCubit(viewsRepository: sl())); + sl.registerFactory(() => UserDetailsCubit(profileRepository: sl())); + } +} diff --git a/lib/core/di/external_registrations.dart b/lib/core/di/external_registrations.dart new file mode 100644 index 00000000..38b3caa0 --- /dev/null +++ b/lib/core/di/external_registrations.dart @@ -0,0 +1,33 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart'; +import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; +import 'package:get_it/get_it.dart'; + +/// Регистрация внешних зависимостей в системе внедрения зависимостей +class ExternalRegistrations { + static final sl = GetIt.instance; + + /// Регистрация всех внешних зависимостей + static void registerAll() { + _registerDio(); + } + + /// Регистрация Dio HTTP клиента + static void _registerDio() { + sl.registerLazySingleton( + () => Dio( + BaseOptions( + baseUrl: baseUrl, + headers: { + 'accept': 'application/json', + 'content-type': 'application/json', + }, + ), + )..interceptors.addAll([ + TokenInterceptor(), + PhoneVerifyErrorInterceptor(), + ]), + ); + } +} diff --git a/lib/core/di/repository_registrations.dart b/lib/core/di/repository_registrations.dart new file mode 100644 index 00000000..6ce258aa --- /dev/null +++ b/lib/core/di/repository_registrations.dart @@ -0,0 +1,88 @@ +import 'package:eatvio_app/core/comments/repositories/comments_repository.dart'; +import 'package:eatvio_app/core/views/repository/views_repository.dart'; +import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; +import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; +import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; +import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart'; +import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart'; +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; +import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; +import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart'; +import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; +import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; +import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; +import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; +import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; +import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; +import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; +import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; +import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; +import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; +import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; +import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart'; +import 'package:eatvio_app/features/plans/repositories/plan_repository.dart'; +import 'package:eatvio_app/features/plans/repositories/plans_repository.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.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/registration/repositories/registration_repository.dart'; +import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; +import 'package:get_it/get_it.dart'; + +/// Регистрация всех Repository в системе внедрения зависимостей +class RepositoryRegistrations { + static final sl = GetIt.instance; + + /// Регистрация всех Repository + static void registerAll() { + _registerSingletonRepositories(); + _registerFactoryRepositories(); + } + + /// Регистрация Repository как синглтонов + static void _registerSingletonRepositories() { + // Core repositories + sl.registerLazySingleton(() => ChatRepository(chatService: sl())); + sl.registerLazySingleton(() => FeedRepository(feedService: sl())); + sl.registerLazySingleton(() => ProfileRepository(profileService: sl())); + sl.registerLazySingleton(() => CalendarRepository(calendarService: sl())); + sl.registerLazySingleton(() => SettingsRepository(settingsService: sl())); + sl.registerLazySingleton(() => ConsultantRepository(consultantService: sl())); + sl.registerLazySingleton(() => DiaryRepository(diaryService: sl())); + sl.registerLazySingleton(() => GlossaryRepository(glossaryService: sl())); + sl.registerLazySingleton(() => DietRepository(dietsService: sl())); + sl.registerLazySingleton(() => PlansRepository(plansService: sl())); + sl.registerLazySingleton(() => PlanRepository(planService: sl())); + sl.registerLazySingleton(() => AboutRecipeRepository(recipeService: sl())); + sl.registerLazySingleton(() => ComplainingRepository(service: sl())); + sl.registerLazySingleton(() => AuthRepository(authService: sl())); + sl.registerLazySingleton(() => CardsRepository(cardsService: sl())); + sl.registerLazySingleton(() => ConsultationsRepository(consultationService: sl())); + sl.registerLazySingleton(() => CancelPlanRepository(cancelService: sl())); + sl.registerLazySingleton(() => CreatePlanRepository(createPlanService: sl())); + sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl())); + sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl())); + sl.registerLazySingleton(() => CommentsRepository(service: sl())); + sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); + + // Content repositories + sl.registerLazySingleton(() => BlogPostRepository(blogPostService: sl())); + sl.registerLazySingleton(() => CreateDietRepository(createDietService: sl())); + sl.registerLazySingleton(() => MyDietsRepository(myDietsService: sl())); + sl.registerLazySingleton(() => MyPlansRepository(myPlansService: sl())); + + // Auth repositories + sl.registerLazySingleton(() => RegistrationRepository(registrationService: sl())); + } + + /// Регистрация Repository как фабрик + static void _registerFactoryRepositories() { + sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); + sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); + sl.registerFactory(() => ConsultantRatingRepository(sl())); + sl.registerFactory(() => ViewsRepository(viewsService: sl())); + } +} diff --git a/lib/core/di/service_registrations.dart b/lib/core/di/service_registrations.dart new file mode 100644 index 00000000..454d9064 --- /dev/null +++ b/lib/core/di/service_registrations.dart @@ -0,0 +1,88 @@ +import 'package:eatvio_app/core/comments/services/comments_service.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/views/service/views_service.dart'; +import 'package:eatvio_app/core/websockets/ws_service.dart'; +import 'package:eatvio_app/features/auth/services/auth_service.dart'; +import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; +import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; +import 'package:eatvio_app/features/calendar/services/calendar_service.dart'; +import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart'; +import 'package:eatvio_app/features/chat/services/chat_service.dart'; +import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart'; +import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart'; +import 'package:eatvio_app/features/consultations/services/consultation_service.dart'; +import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; +import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; +import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; +import 'package:eatvio_app/features/diary/services/diary_service.dart'; +import 'package:eatvio_app/features/diets/services/diet_service.dart'; +import 'package:eatvio_app/features/feed/services/feed_service.dart'; +import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; +import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; +import 'package:eatvio_app/features/menu/services/notification_settings_service.dart'; +import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; +import 'package:eatvio_app/features/my_finance/services/cards_service.dart'; +import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; +import 'package:eatvio_app/features/notifications/service/notifications_service.dart'; +import 'package:eatvio_app/features/plans/services/plan_service.dart'; +import 'package:eatvio_app/features/plans/services/plans_service.dart'; +import 'package:eatvio_app/features/profile/services/profile_service.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/services/registration_service.dart'; +import 'package:eatvio_app/features/settings/services/settings_service.dart'; +import 'package:get_it/get_it.dart'; + +/// Регистрация всех Service в системе внедрения зависимостей +class ServiceRegistrations { + static final sl = GetIt.instance; + + /// Регистрация всех Service + static void registerAll() { + _registerSingletonServices(); + } + + /// Регистрация Service как синглтонов + static void _registerSingletonServices() { + // Core services + sl.registerLazySingleton(() => ChatService(sl())); + sl.registerLazySingleton(() => FeedService(sl())); + sl.registerLazySingleton(() => ProfileService(sl())); + sl.registerLazySingleton(() => CalendarService(sl())); + sl.registerLazySingleton(() => SettingsService(sl())); + sl.registerLazySingleton(() => ConsultantService(sl())); + sl.registerLazySingleton(() => DiaryService(sl())); + sl.registerLazySingleton(() => GlossaryService(sl())); + sl.registerLazySingleton(() => DietService(sl())); + sl.registerLazySingleton(() => PlansService(sl())); + sl.registerLazySingleton(() => PlanService(sl())); + sl.registerLazySingleton(() => MyDietsService(sl())); + sl.registerLazySingleton(() => MyPlansService(sl())); + sl.registerLazySingleton(() => WsService(sl())); + sl.registerLazySingleton(() => AboutRecipeService(sl())); + sl.registerLazySingleton(() => ComplainingService(sl())); + sl.registerLazySingleton(() => AuthService(sl())); + sl.registerLazySingleton(() => CardsService(sl())); + sl.registerLazySingleton(() => ConsultationService(sl())); + sl.registerLazySingleton(() => CancelPlanService(sl())); + sl.registerLazySingleton(() => CreatePlanService(sl())); + sl.registerLazySingleton(() => NotificationsService(sl())); + sl.registerLazySingleton(() => FirebaseApi()); + sl.registerLazySingleton(() => ResetPasswordService(sl())); + sl.registerLazySingleton(() => BlockedUserService(sl())); + sl.registerLazySingleton(() => CommentsService(sl())); + sl.registerLazySingleton(() => MyRecipesService(sl())); + sl.registerLazySingleton(() => ConsultantRatingService(sl())); + sl.registerLazySingleton(() => NotificationSettingsService(sl())); + sl.registerLazySingleton(() => ViewsService(sl())); + + // Content services + sl.registerLazySingleton(() => CreateRecipeService(sl())); + sl.registerLazySingleton(() => BlogPostService(sl())); + sl.registerLazySingleton(() => CreateDietService(sl())); + + // Auth services + sl.registerLazySingleton(() => RegistrationService(sl())); + } +} diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 37713f29..8299fea2 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -1,263 +1,27 @@ -import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/comments/repositories/comments_repository.dart'; -import 'package:eatvio_app/core/comments/services/comments_service.dart'; -import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart'; -import 'package:eatvio_app/core/firebase_api.dart'; -import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; -import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; -import 'package:eatvio_app/core/views/repository/views_repository.dart'; -import 'package:eatvio_app/core/views/service/views_service.dart'; +import 'package:eatvio_app/core/di/cubit_registrations.dart'; +import 'package:eatvio_app/core/di/external_registrations.dart'; +import 'package:eatvio_app/core/di/repository_registrations.dart'; +import 'package:eatvio_app/core/di/service_registrations.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; -import 'package:eatvio_app/core/websockets/ws_service.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; -import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; -import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; -import 'package:eatvio_app/features/auth/services/auth_service.dart'; -import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; -import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart'; -import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart'; -import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart'; -import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart'; -import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; -import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; -import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; -import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; -import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart'; -import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart'; -import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; -import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; -import 'package:eatvio_app/features/consultations/services/consultation_service.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart'; -import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; -import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; -import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; -import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; -import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; -import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; -import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; -import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; -import 'package:eatvio_app/features/diets/services/diet_service.dart'; -import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; -import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; -import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; -import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; -import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; -import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; -import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; -import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; -import 'package:eatvio_app/features/menu/services/notification_settings_service.dart'; -import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; -import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; -import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart'; -import 'package:eatvio_app/features/calendar/services/calendar_service.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; -import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; -import 'package:eatvio_app/features/chat/services/chat_service.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart'; -import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; -import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; -import 'package:eatvio_app/features/diary/services/diary_service.dart'; -import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart'; -import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; -import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; -import 'package:eatvio_app/features/feed/services/feed_service.dart'; -import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; -import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; -import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; -import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; -import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; -import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; -import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; -import 'package:eatvio_app/features/my_finance/services/cards_service.dart'; -import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; -import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; -import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; -import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart'; -import 'package:eatvio_app/features/notifications/service/notifications_service.dart'; -import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart'; -import 'package:eatvio_app/features/plans/repositories/plans_repository.dart'; -import 'package:eatvio_app/features/plans/services/plans_service.dart'; -import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; -import 'package:eatvio_app/features/plans/repositories/plan_repository.dart'; -import 'package:eatvio_app/features/plans/services/plan_service.dart'; -import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; -import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; -import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; -import 'package:eatvio_app/features/profile/services/profile_service.dart'; -import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; -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'; -import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; -import 'package:eatvio_app/features/settings/services/settings_service.dart'; -import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart'; import 'package:get_it/get_it.dart'; -import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans; +/// Глобальный экземпляр GetIt для внедрения зависимостей final sl = GetIt.instance; +/// Инициализация системы внедрения зависимостей Future init() async { - // Cubit singletons + // Регистрация внешних зависимостей (Dio, etc.) + ExternalRegistrations.registerAll(); + + // Регистрация сервисов + ServiceRegistrations.registerAll(); + + // Регистрация репозиториев + RepositoryRegistrations.registerAll(); + + // Регистрация Cubit'ов + CubitRegistrations.registerAll(); + + // Регистрация WebSocket менеджера sl.registerLazySingleton(() => WsManager(wsService: sl())); - sl.registerLazySingleton(() => NewMessagesCubit()); - sl.registerLazySingleton(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl())); - sl.registerLazySingleton(() => ProfileAvatarCubit(profileRepository: sl())); - sl.registerLazySingleton(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl())); - - // Cubit factories - sl.registerFactory(() => FeedCubit(feedRepository: sl())); - sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); - sl.registerFactory(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl())); - sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl())); - sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); - sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl())); - sl.registerFactory(() => profile_plans.PlansCubit(plansRepository: sl())); - sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); - sl.registerFactory(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl())); - sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); - sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl())); - sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); - sl.registerFactory( - () => DiaryCubit( - chatRepository: sl(), - diaryRepository: sl(), - glossaryRepository: sl(), - profileRepository: sl(), - dietRepository: sl(), - settingsRepository: sl(), - ), - ); - sl.registerFactory(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl())); - sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); - sl.registerFactory(() => SubCubit(profileRepository: sl())); - sl.registerFactory(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl())); - sl.registerFactory(() => CreatePlanCubit(createPlanRepository: sl())); - sl.registerFactory(() => FeedProfileCubit(profileRepository: sl())); - sl.registerFactory(() => AboutPlanCubit(planRepository: sl())); - sl.registerFactory(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl())); - sl.registerFactory(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl())); - sl.registerFactory(() => MyPlansCubit(myPlansRepository: sl())); - sl.registerFactory(() => UserDetailsCubit(profileRepository: sl())); - sl.registerFactory(() => ConsultantsCubit(feedRepository: sl())); - sl.registerFactory(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl())); - sl.registerFactory(() => AuthCubit(authRepository: sl())); - sl.registerFactory(() => ComplainingCubit(repository: sl())); - sl.registerFactory(() => UserCardsCubit(cardsRepository: sl())); - sl.registerFactory(() => OrderInfoCubit(chatRepository: sl())); - sl.registerFactory(() => ConsultationsCubit(consultationsRepository: sl())); - sl.registerFactory(() => ConsultantCommentsCubit(diaryRepository: sl())); - sl.registerFactory(() => CancelPlanCubit(cancelPlanRepository: sl())); - sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); - sl.registerFactory(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl())); - sl.registerFactory(() => ConsultationsArchiveCubit(consultationsRepository: sl())); - sl.registerFactory(() => ConsultantFinancesCubit(consultationsRepository: sl())); - sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); - sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); - sl.registerFactory(() => BlogPostCubit(sl())); - sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); - sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); - sl.registerFactory(() => ConsultantRatingCubit(sl())); - sl.registerFactory(() => ViewsCubit(viewsRepository: sl())); - - // Repository - sl.registerLazySingleton(() => ChatRepository(chatService: sl())); - sl.registerLazySingleton(() => FeedRepository(feedService: sl())); - sl.registerLazySingleton(() => RegistrationRepository(registrationService: sl())); - sl.registerLazySingleton(() => CalendarRepository(calendarService: sl())); - sl.registerLazySingleton(() => ProfileRepository(profileService: sl())); - sl.registerLazySingleton(() => RecipeRepository(recipeService: sl())); - sl.registerLazySingleton(() => SettingsRepository(settingsService: sl())); - sl.registerLazySingleton(() => ConsultantRepository(consultantService: sl())); - sl.registerLazySingleton(() => DiaryRepository(diaryService: sl())); - sl.registerLazySingleton(() => GlossaryRepository(glossaryService: sl())); - sl.registerLazySingleton(() => DietRepository(dietsService: sl())); - sl.registerLazySingleton(() => BlogPostRepository(blogPostService: sl())); - sl.registerLazySingleton(() => CreateDietRepository(createDietService: sl())); - sl.registerLazySingleton(() => PlansRepository(plansService: sl())); - sl.registerLazySingleton(() => PlanRepository(planService: sl())); - sl.registerLazySingleton(() => MyDietsRepository(myDietsService: sl())); - sl.registerLazySingleton(() => MyPlansRepository(myPlansService: sl())); - sl.registerLazySingleton(() => AboutRecipeRepository(recipeService: sl())); - sl.registerLazySingleton(() => ComplainingRepository(service: sl())); - sl.registerLazySingleton(() => AuthRepository(authService: sl())); - sl.registerLazySingleton(() => CardsRepository(cardsService: sl())); - sl.registerLazySingleton(() => ConsultationsRepository(consultationService: sl())); - sl.registerLazySingleton(() => CancelPlanRepository(cancelService: sl())); - sl.registerLazySingleton(() => CreatePlanRepository(createPlanService: sl())); - sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl())); - sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); - sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); - sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl())); - sl.registerLazySingleton(() => CommentsRepository(service: sl())); - sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); - sl.registerFactory(() => ConsultantRatingRepository(sl())); - sl.registerFactory(() => ViewsRepository(viewsService: sl())); - - // Services - sl.registerLazySingleton(() => ChatService(sl())); - sl.registerLazySingleton(() => FeedService(sl())); - sl.registerLazySingleton(() => RegistrationService(sl())); - sl.registerLazySingleton(() => CalendarService(sl())); - sl.registerLazySingleton(() => ProfileService(sl())); - sl.registerLazySingleton(() => CreateRecipeService(sl())); - sl.registerLazySingleton(() => SettingsService(sl())); - sl.registerLazySingleton(() => ConsultantService(sl())); - sl.registerLazySingleton(() => DiaryService(sl())); - sl.registerLazySingleton(() => GlossaryService(sl())); - sl.registerLazySingleton(() => DietService(sl())); - sl.registerLazySingleton(() => BlogPostService(sl())); - sl.registerLazySingleton(() => CreateDietService(sl())); - sl.registerLazySingleton(() => PlansService(sl())); - sl.registerLazySingleton(() => PlanService(sl())); - sl.registerLazySingleton(() => MyDietsService(sl())); - sl.registerLazySingleton(() => MyPlansService(sl())); - sl.registerLazySingleton(() => WsService(sl())); - sl.registerLazySingleton(() => AboutRecipeService(sl())); - sl.registerLazySingleton(() => ComplainingService(sl())); - sl.registerLazySingleton(() => AuthService(sl())); - sl.registerLazySingleton(() => CardsService(sl())); - sl.registerLazySingleton(() => ConsultationService(sl())); - sl.registerLazySingleton(() => CancelPlanService(sl())); - sl.registerLazySingleton(() => CreatePlanService(sl())); - sl.registerLazySingleton(() => NotificationsService(sl())); - sl.registerLazySingleton(() => FirebaseApi()); - sl.registerLazySingleton(() => ResetPasswordService(sl())); - sl.registerLazySingleton(() => BlockedUserService(sl())); - sl.registerLazySingleton(() => CommentsService(sl())); - sl.registerLazySingleton(() => MyRecipesService(sl())); - sl.registerLazySingleton(() => ConsultantRatingService(sl())); - sl.registerLazySingleton(() => NotificationSettingsService(sl())); - sl.registerLazySingleton(() => ViewsService(sl())); - - // External - sl.registerLazySingleton( - () => - Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'})) - ..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]), - ); -} +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index d716f3fa..c6501d49 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,53 +1,15 @@ -import 'dart:io' as io; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/locator_service.dart' as di; -import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; -import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; -import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; -import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; -import 'package:eatvio_app/firebase_options.dart'; -import 'package:eatvio_app/generated/l10n.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:eatvio_app/core/env_config.dart'; -import 'package:appmetrica_plugin/appmetrica_plugin.dart'; Future main() async { - WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); - FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - - await dotenv.load(fileName: ".env"); - - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - await FirebaseMessaging.instance.requestPermission(); - await di.init(); - - if (kDebugMode) { - io.HttpClient.enableTimelineLogging = true; - } - - AppMetrica.activate(AppMetricaConfig(EnvConfig.appMetricaApiKey)); - AppMetrica.reportEvent('Application started'); - - await SentryFlutter.init((options) { - options.dsn = EnvConfig.sentryDsn; - options.tracesSampleRate = 1.0; - options.debug = false; - }, appRunner: () => runApp(const MyApp())); + await AppInitializer.initialize(); + await AppInitializer.runAppWithSentry(() => runApp(const MyApp())); } class MyApp extends StatelessWidget { @@ -56,30 +18,16 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiBlocProvider( - providers: [ - BlocProvider(create: (context) => sl()), - BlocProvider(create: (context) => sl()..getNotifications()), - BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), - BlocProvider(create: (context) => sl()..getMyProfile()), - BlocProvider(create: (context) => sl()..getChatRoomsWithLoading()), - ], + providers: AppConfig.globalBlocProviders, child: Portal( child: KeyboardDismisser( child: MaterialApp.router( theme: ThemeHelper().themeData(), routerConfig: AppRouter.appRouter, - builder: (context, child) => BlocProvider( - create: (context) => sl(), - child: InternetConnectionCheckerScreen(child: child!), - ), + builder: (context, child) => AppConfig.createInternetConnectionWrapper(child!), debugShowCheckedModeBanner: false, - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [Locale.fromSubtags(languageCode: 'ru')], + localizationsDelegates: AppConfig.localizationDelegates, + supportedLocales: AppConfig.supportedLocales, ), ), ), From 7861cdce3cdb74b2471e61c5ab024ffc73c3eb2d Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 1 Oct 2025 23:59:34 +0300 Subject: [PATCH 23/49] fix: refactor --- lib/core/di/cubit_registrations.dart | 1 - lib/core/di/repository_registrations.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index efd0dbd2..156e43b1 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -23,7 +23,6 @@ import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/con import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; diff --git a/lib/core/di/repository_registrations.dart b/lib/core/di/repository_registrations.dart index 6ce258aa..71d1523d 100644 --- a/lib/core/di/repository_registrations.dart +++ b/lib/core/di/repository_registrations.dart @@ -12,7 +12,6 @@ import 'package:eatvio_app/features/consultant_rating/repositories/consultant_ra import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; -import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; From e7ce9d8f909aab9841aaa3424804df3432345174 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 2 Oct 2025 00:05:25 +0300 Subject: [PATCH 24/49] fix: refactor --- lib/core/app_router.dart | 482 ++---------------- lib/core/constants/app_constants.dart | 72 +++ lib/core/cubit/base_cubit.dart | 42 ++ lib/core/cubit/state_utils.dart | 28 + lib/core/routing/auth_routes.dart | 37 ++ lib/core/routing/content_routes.dart | 73 +++ lib/core/routing/main_navigation_routes.dart | 94 ++++ lib/core/routing/other_routes.dart | 229 +++++++++ lib/core/routing/route_constants.dart | 84 +++ lib/core/routing/settings_routes.dart | 62 +++ lib/core/utils/validation_utils.dart | 87 ++++ .../blog_post_create_cubit.dart | 55 +- 12 files changed, 862 insertions(+), 483 deletions(-) create mode 100644 lib/core/constants/app_constants.dart create mode 100644 lib/core/cubit/base_cubit.dart create mode 100644 lib/core/cubit/state_utils.dart create mode 100644 lib/core/routing/auth_routes.dart create mode 100644 lib/core/routing/content_routes.dart create mode 100644 lib/core/routing/main_navigation_routes.dart create mode 100644 lib/core/routing/other_routes.dart create mode 100644 lib/core/routing/route_constants.dart create mode 100644 lib/core/routing/settings_routes.dart create mode 100644 lib/core/utils/validation_utils.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index cc028255..f3715464 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,455 +1,53 @@ -import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/firebase_api.dart'; -import 'package:eatvio_app/core/token_storage.dart'; -import 'package:eatvio_app/core/websockets/ws_manager.dart'; -import 'package:eatvio_app/core/locator_service.dart'; -import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; -import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; -import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart'; -import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; -import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.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/chat/presentation/pages/chats_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart'; -import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; -import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart'; -import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart'; -import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; -import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; -import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; -import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; -import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; -import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; -import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; -import 'package:eatvio_app/features/diets/presentation/pages/about_diet_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/pages/feed_screen.dart'; -import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.dart'; -import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart'; -import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; -import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart'; -import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart'; -import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; -import 'package:eatvio_app/features/notifications/presentation/notifications_screen.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/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'; -import 'package:eatvio_app/features/settings/presentation/pages/change_password_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart'; -import 'package:eatvio_app/features/support/presentation/pages/support_screen.dart'; -import 'package:eatvio_app/features/update_screen/presentation/update_screen.dart'; -import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; -import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart'; +import 'package:eatvio_app/core/routing/auth_routes.dart'; +import 'package:eatvio_app/core/routing/content_routes.dart'; +import 'package:eatvio_app/core/routing/main_navigation_routes.dart'; +import 'package:eatvio_app/core/routing/other_routes.dart'; +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/core/routing/settings_routes.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:go_router/go_router.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:version/version.dart'; -import 'dart:developer'; +/// Главный роутер приложения class AppRouter { static GlobalKey navigatorKey = GlobalKey(); static GoRouter appRouter = GoRouter( - initialLocation: '/update-screen', + initialLocation: RouteConstants.updateScreen, navigatorKey: navigatorKey, - redirect: (context, state) { - final segments = state.uri.pathSegments; - if (segments.length >= 3) { - final type = segments[1]; - final contentIdentifier = segments[2]; - - switch (type) { - case 'recipe': - FlutterNativeSplash.remove(); - return '/about-recipes-screen?slug=$contentIdentifier'; - case 'blog_post': - FlutterNativeSplash.remove(); - return '/blog-post-screen?id=$contentIdentifier'; - case 'plan': - FlutterNativeSplash.remove(); - return '/about-plan-screen?slug=$contentIdentifier'; - case 'diet': - FlutterNativeSplash.remove(); - return '/about-diet-screen?slug=$contentIdentifier'; - } - } - - return null; - }, + redirect: _handleRedirect, routes: [ - GoRoute( - path: '/auth-screen', - name: 'authScreen', - builder: (context, state) => const AuthScreen(), - routes: [ - GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), - GoRoute( - path: 'forget-password-screen', - name: 'forgetPasswordScreen', - builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool), - ), - GoRoute(path: 'scan-qr-code-screen', name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()), - ], - ), - ShellRoute( - builder: (context, state, child) { - return BlocBuilder( - builder: (BuildContext context, ProfileAvatarState state) { - if (state is ProfileAvatarStateLoaded) { - return BottomNavigationBarWidget(profileModel: state.profile, child: child); - } - return const SizedBox.shrink(); - }, - ); - }, - routes: [ - GoRoute( - path: '/my-profile-screen', - name: 'myProfileScreen', - pageBuilder: (context, state) { - return const NoTransitionPage(child: ProfileScreen()); - }, - ), - GoRoute( - path: '/feed-screen', - name: 'feedScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()), - redirect: (context, state) async { - try { - await sl().initialize(); - } catch (e, st) { - log(e.toString(), stackTrace: st); - } - return null; - }, - ), - GoRoute( - path: '/diary-screen', - name: 'diaryScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), - ), - GoRoute( - path: ConsultationsScreen.path, - name: ConsultationsScreen.name, - pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()), - ), - GoRoute( - path: '/chat-select-screen', - name: 'chatSelectScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), - ), - GoRoute( - path: '/notifications-screen', - name: 'notificationsScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()), - ), - GoRoute( - path: '/menu-screen', - name: 'menuScreen', - pageBuilder: (context, state) => NoTransitionPage( - child: BlocBuilder( - builder: (BuildContext context, ProfileAvatarState state) { - if (state is ProfileAvatarStateLoaded) { - return MenuScreen(profileModel: state.profile); - } - return const SizedBox.shrink(); - }, - ), - ), - ), - ], - ), - GoRoute(name: 'blockedUsersScreen', path: '/blocked-users-screen', builder: (context, state) => const BlockedUsersScreen()), - GoRoute( - path: '/welcome-screen', - name: 'welcomeScreen', - builder: (context, state) => const WelcomeScreen(), - redirect: (context, state) async { - try { - final sharedPreferences = await SharedPreferences.getInstance(); - final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; - await sl().registerPushToken(); - if (welcomeScreenWasShowed == false) { - return null; - } else if (await TokenStorage.haveTokenInStorage()) { - return '/feed-screen'; - } else { - return '/auth-screen'; - } - } catch (e) { - return '/feed-screen'; - } - }, - ), - GoRoute( - path: '/update-screen', - name: 'updateScreen', - builder: (context, state) => const UpdateScreen(), - redirect: (context, state) async { - try { - final versions = (await sl().get('$baseUrl/api/app_versions')).data; - PackageInfo packageInfo = await PackageInfo.fromPlatform(); - String version = packageInfo.version; - Version currentVersion = Version.parse(version); - Version latestVersion = Version.parse(versions['min_version']); - if (latestVersion > currentVersion) { - return null; - } else { - return '/welcome-screen'; - } - } catch (e) { - return '/welcome-screen'; - } - }, - ), - GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()), - GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()), - GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()), - GoRoute(path: '/user-finance-screen', name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()), - GoRoute( - path: CardSettingsScreen.path, - name: CardSettingsScreen.name, - builder: (context, state) { - final chosenPlanId = state.extra as int?; - return CardSettingsScreen(chosenPlanId: chosenPlanId); - }, - ), - GoRoute( - path: '/about-plan-screen', - name: 'aboutPlanScreen', - builder: (context, state) { - final slug = state.uri.queryParameters['slug']; - if (slug != null) { - return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); - } - - final data = state.extra as Map; - return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); - }, - ), - GoRoute(path: '/glossary-screen', name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()), - GoRoute( - path: '/about-recipes-screen', - name: 'aboutRecipesScreen', - builder: (context, state) { - final slug = state.uri.queryParameters['slug']; - if (slug != null) { - return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null); - } - - final data = state.extra as Map; - return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); - }, - ), - GoRoute( - path: RecipeCreateScreen.path, - name: RecipeCreateScreen.name, - builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null), - ), - GoRoute( - path: '/profile-screen', - name: 'profileScreen', - builder: (context, state) => ProfileScreen(author: state.extra as Author), - ), - GoRoute( - path: '/subs-screen/:slug', - name: 'subsScreen', - builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType), - ), - GoRoute( - path: '/about-diet-screen', - name: 'aboutDietScreen', - builder: (context, state) { - final slug = state.uri.queryParameters['slug']; - if (slug != null) { - return AboutDietScreen(slug: slug, index: null, navigateToComments: false); - } - final data = state.extra as Map; - return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); - }, - ), - GoRoute( - path: '/blog-post-create-screen', - name: 'blogPostCreateScreen', - builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null), - ), - GoRoute( - path: '/blog-post-screen', - name: 'blogPostScreen', - builder: (context, state) { - final id = state.uri.queryParameters['id']; - final parsedId = int.tryParse(id ?? ''); - if (parsedId != null) { - return BlogPostScreen(id: parsedId); - } - - return BlogPostScreen(id: state.extra as int); - }, - ), - GoRoute( - path: '/chat-screen', - name: 'chatScreen', - builder: (context, state) { - final data = state.extra as Map; - final uuid = data['uuid'] as String; - final sendInitialMessage = data['sendInitialMessage'] as bool?; - return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); - }, - ), - GoRoute( - path: '/empty-chat-screen', - name: 'emptyChatScreen', - builder: (context, state) => EmptyChatScreen(author: state.extra as Author), - ), - GoRoute( - path: '/settings-screen', - name: 'settingsScreen', - builder: (context, state) => const SettingsScreen(), - routes: [ - GoRoute( - path: 'general-info-screen', - name: 'generalInfoScreen', - builder: (context, state) => GeneralInfoScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'other-info-screen', - name: 'otherInfoScreen', - builder: (context, state) => OtherInfoScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'physical-activity-screen', - name: 'physicalActivityScreen', - builder: (context, state) => PhysicalActivityScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'physical-form-screen', - name: 'physicalFormScreen', - builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'pressure-screen', - name: 'pressureScreen', - builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'change-password-screen', - name: 'changePasswordScreen', - builder: (context, state) => ChangePasswordScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'change-email-screen', - name: 'changeEmailScreen', - builder: (context, state) => ChangeEmailScreen(settingsEntity: state.extra as SettingsEntity), - ), - ], - ), - GoRoute(path: '/become-consultant-screen', name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()), - GoRoute( - path: '/stepper-become-consultant-screen', - name: 'stepperBecomeConsultantScreen', - builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), - ), - GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), - GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()), - GoRoute( - path: '/photo-view-screen/:index', - name: 'photoViewScreen', - builder: (context, state) => PhotoViewScreen(imageUrls: state.extra as List, index: int.parse(state.pathParameters['index']!)), - ), - GoRoute( - path: CreateDietScreen.path, - name: CreateDietScreen.name, - builder: (context, state) { - final diet = state.extra as Diet?; - return CreateDietScreen(diet: diet); - }, - ), - GoRoute( - path: DietPreviewScreen.path, - name: DietPreviewScreen.name, - builder: (context, state) { - final data = state.extra as Diet; - return DietPreviewScreen(diet: data); - }, - ), - GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()), - GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()), - GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()), - GoRoute( - path: UserDetailsScreen.path, - name: UserDetailsScreen.name, - builder: (context, state) => UserDetailsScreen(slug: state.extra as String), - ), - GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()), - GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()), - GoRoute( - path: InfoByOrderScreen.path, - name: InfoByOrderScreen.name, - builder: (context, state) { - final data = state.extra as Map; - final planId = data['planId'] as int; - final successfulPayment = data['successfulPayment'] as bool?; - return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment); - }, - ), - GoRoute( - path: PaymentScreen.path, - name: PaymentScreen.name, - builder: (context, state) { - final data = state.extra as Map; - final planId = data['planId'] as int; - final url = data['url'] as String; - return PaymentScreen(url: url, planId: planId); - }, - ), - GoRoute( - path: UserDiaryScreen.path, - name: UserDiaryScreen.name, - builder: (context, state) { - final data = state.extra as Map; - final diaryId = data['diaryId'] as int; - final currentPlanDay = data['currentDiaryDay'] as int; - return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay); - }, - ), - GoRoute( - path: '/verify-screen', - 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()), - GoRoute(path: '/notifications-settings-screen', name: 'notificationsSettingsScreen', builder: (context, state) => const NotificationsSettingsScreen()), + ...AuthRoutes.createRoutes(), + ...MainNavigationRoutes.createRoutes(), + ...ContentRoutes.createRoutes(), + ...SettingsRoutes.createRoutes(), + ...OtherRoutes.createRoutes(), ], ); + + /// Обработка редиректов для deep links + static String? _handleRedirect(BuildContext context, GoRouterState state) { + final segments = state.uri.pathSegments; + if (segments.length >= 3) { + final type = segments[1]; + final contentIdentifier = segments[2]; + + switch (type) { + case 'recipe': + FlutterNativeSplash.remove(); + return '${RouteConstants.aboutRecipesScreen}?slug=$contentIdentifier'; + case 'blog_post': + FlutterNativeSplash.remove(); + return '${RouteConstants.blogPostScreen}?id=$contentIdentifier'; + case 'plan': + FlutterNativeSplash.remove(); + return '${RouteConstants.aboutPlanScreen}?slug=$contentIdentifier'; + case 'diet': + FlutterNativeSplash.remove(); + return '${RouteConstants.aboutDietScreen}?slug=$contentIdentifier'; + } + } + + return null; + } } diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart new file mode 100644 index 00000000..f9c945c9 --- /dev/null +++ b/lib/core/constants/app_constants.dart @@ -0,0 +1,72 @@ +/// Константы приложения +class AppConstants { + // Размеры + static const double defaultPadding = 16.0; + static const double smallPadding = 8.0; + static const double largePadding = 24.0; + static const double extraLargePadding = 32.0; + + // Радиусы + static const double defaultBorderRadius = 8.0; + static const double smallBorderRadius = 4.0; + static const double largeBorderRadius = 16.0; + + // Высоты + static const double defaultButtonHeight = 48.0; + static const double smallButtonHeight = 36.0; + static const double largeButtonHeight = 56.0; + + // Ширины + static const double maxContentWidth = 400.0; + static const double minContentWidth = 280.0; + + // Анимации + static const Duration defaultAnimationDuration = Duration(milliseconds: 300); + static const Duration fastAnimationDuration = Duration(milliseconds: 150); + static const Duration slowAnimationDuration = Duration(milliseconds: 500); + + // Таймауты + static const Duration defaultTimeout = Duration(seconds: 30); + static const Duration shortTimeout = Duration(seconds: 10); + static const Duration longTimeout = Duration(minutes: 2); + + // Лимиты + static const int maxImageSize = 5 * 1024 * 1024; // 5MB + static const int maxVideoSize = 100 * 1024 * 1024; // 100MB + static const int maxTextLength = 1000; + static const int maxTitleLength = 100; + static const int maxDescriptionLength = 500; + + // Пагинация + static const int defaultPageSize = 20; + static const int smallPageSize = 10; + static const int largePageSize = 50; + + // Форматы + static const List supportedImageFormats = ['jpg', 'jpeg', 'png', 'webp']; + static const List supportedVideoFormats = ['mp4', 'mov', 'avi']; + + // Регулярные выражения + static const String emailRegex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; + static const String phoneRegex = r'^\+?[1-9]\d{1,14}$'; + static const String urlRegex = r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$'; + + // Сообщения + static const String defaultErrorMessage = 'Произошла ошибка. Попробуйте еще раз.'; + static const String networkErrorMessage = 'Проблема с подключением к интернету.'; + static const String timeoutErrorMessage = 'Превышено время ожидания.'; + static const String unknownErrorMessage = 'Неизвестная ошибка.'; + + // Ключи для SharedPreferences + static const String welcomeScreenShownKey = 'welcomeScreenWasShowed'; + static const String userTokenKey = 'userToken'; + static const String userDataKey = 'userData'; + static const String settingsKey = 'settings'; + + // API endpoints + static const String apiVersion = '/api'; + static const String authEndpoint = '/auth'; + static const String usersEndpoint = '/users'; + static const String postsEndpoint = '/posts'; + static const String commentsEndpoint = '/comments'; +} diff --git a/lib/core/cubit/base_cubit.dart b/lib/core/cubit/base_cubit.dart new file mode 100644 index 00000000..3cd6c9d9 --- /dev/null +++ b/lib/core/cubit/base_cubit.dart @@ -0,0 +1,42 @@ +import 'dart:developer'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// Базовый класс для всех Cubit'ов с унифицированной обработкой ошибок +abstract class BaseCubit extends Cubit { + BaseCubit(super.initialState); + + /// Унифицированная обработка ошибок + void handleError(Object error, [StackTrace? stackTrace]) { + log('Error in ${runtimeType}: $error', stackTrace: stackTrace); + // Здесь можно добавить отправку ошибок в Sentry или другую систему мониторинга + } + + /// Безопасное выполнение асинхронных операций + Future safeExecute(Future Function() operation) async { + try { + return await operation(); + } catch (error, stackTrace) { + handleError(error, stackTrace); + return null; + } + } + + /// Безопасное выполнение асинхронных операций с обработкой состояния + Future safeExecuteWithState( + Future Function() operation, { + required State loadingState, + required State Function(T result) successState, + required State Function(Object error) errorState, + }) async { + try { + emit(loadingState); + final result = await operation(); + emit(successState(result)); + return result; + } catch (error, stackTrace) { + handleError(error, stackTrace); + emit(errorState(error)); + return null; + } + } +} diff --git a/lib/core/cubit/state_utils.dart b/lib/core/cubit/state_utils.dart new file mode 100644 index 00000000..9c6dfb78 --- /dev/null +++ b/lib/core/cubit/state_utils.dart @@ -0,0 +1,28 @@ +/// Утилиты для работы с состояниями Cubit'ов +class StateUtils { + /// Проверяет, является ли состояние загрузочным + static bool isLoading(dynamic state) { + return state.toString().contains('loading') || state.toString().contains('Loading'); + } + + /// Проверяет, является ли состояние загруженным + static bool isLoaded(dynamic state) { + return state.toString().contains('loaded') || state.toString().contains('Loaded'); + } + + /// Проверяет, является ли состояние ошибочным + static bool isError(dynamic state) { + return state.toString().contains('error') || state.toString().contains('Error'); + } + + /// Получает сообщение об ошибке из состояния + static String? getErrorMessage(dynamic state) { + if (isError(state)) { + // Попытка извлечь сообщение об ошибке из состояния + final stateString = state.toString(); + final errorMatch = RegExp(r'error[:\s]+([^,}]+)').firstMatch(stateString); + return errorMatch?.group(1)?.trim(); + } + return null; + } +} diff --git a/lib/core/routing/auth_routes.dart b/lib/core/routing/auth_routes.dart new file mode 100644 index 00000000..fc4558d1 --- /dev/null +++ b/lib/core/routing/auth_routes.dart @@ -0,0 +1,37 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart'; +import 'package:go_router/go_router.dart'; + +/// Маршруты для аутентификации +class AuthRoutes { + /// Создание маршрутов аутентификации + static List createRoutes() { + return [ + GoRoute( + path: RouteConstants.authScreen, + name: 'authScreen', + builder: (context, state) => const AuthScreen(), + routes: [ + GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), + GoRoute( + path: RouteConstants.forgetPasswordScreen, + name: 'forgetPasswordScreen', + builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool), + ), + GoRoute(path: RouteConstants.scanQRCodeScreen, name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()), + ], + ), + GoRoute( + path: RouteConstants.verifyScreen, + name: 'verifyScreen', + builder: (context, state) => VerifyScreen(passKey: state.extra as String), + ), + GoRoute(path: RouteConstants.verifyEmailScreen, name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), + ]; + } +} diff --git a/lib/core/routing/content_routes.dart b/lib/core/routing/content_routes.dart new file mode 100644 index 00000000..69d7ebf4 --- /dev/null +++ b/lib/core/routing/content_routes.dart @@ -0,0 +1,73 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; +import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart'; +import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart'; +import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; +import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; +import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart'; +import 'package:go_router/go_router.dart'; + +/// Маршруты для контента +class ContentRoutes { + /// Создание маршрутов контента + static List createRoutes() { + return [ + GoRoute( + path: RouteConstants.aboutPlanScreen, + name: 'aboutPlanScreen', + builder: (context, state) { + final slug = state.uri.queryParameters['slug']; + if (slug != null) { + return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); + } + + final data = state.extra as Map; + return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + }, + ), + GoRoute( + path: RouteConstants.aboutRecipesScreen, + name: 'aboutRecipesScreen', + builder: (context, state) { + final slug = state.uri.queryParameters['slug']; + if (slug != null) { + return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null); + } + + final data = state.extra as Map; + return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + }, + ), + GoRoute( + path: RouteConstants.aboutDietScreen, + name: 'aboutDietScreen', + builder: (context, state) { + final slug = state.uri.queryParameters['slug']; + if (slug != null) { + return AboutDietScreen(slug: slug, index: null, navigateToComments: false); + } + final data = state.extra as Map; + return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + }, + ), + GoRoute( + path: RouteConstants.blogPostCreateScreen, + name: 'blogPostCreateScreen', + builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null), + ), + GoRoute( + path: RouteConstants.blogPostScreen, + name: 'blogPostScreen', + builder: (context, state) { + final id = state.uri.queryParameters['id']; + final parsedId = int.tryParse(id ?? ''); + if (parsedId != null) { + return BlogPostScreen(id: parsedId); + } + + return BlogPostScreen(id: state.extra as int); + }, + ), + ]; + } +} diff --git a/lib/core/routing/main_navigation_routes.dart b/lib/core/routing/main_navigation_routes.dart new file mode 100644 index 00000000..818f625c --- /dev/null +++ b/lib/core/routing/main_navigation_routes.dart @@ -0,0 +1,94 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; +import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; +import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'dart:developer'; + +/// Маршруты для основной навигации +class MainNavigationRoutes { + /// Создание маршрутов основной навигации + static List createRoutes() { + return [ + ShellRoute( + builder: (context, state, child) { + return BlocBuilder( + builder: (BuildContext context, ProfileAvatarState state) { + if (state is ProfileAvatarStateLoaded) { + return BottomNavigationBarWidget(profileModel: state.profile, child: child); + } + return const SizedBox.shrink(); + }, + ); + }, + routes: [ + GoRoute( + path: RouteConstants.myProfileScreen, + name: 'myProfileScreen', + pageBuilder: (context, state) { + return const NoTransitionPage(child: ProfileScreen()); + }, + ), + GoRoute( + path: RouteConstants.feedScreen, + name: 'feedScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()), + redirect: (context, state) async { + try { + await sl().initialize(); + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + return null; + }, + ), + GoRoute( + path: RouteConstants.diaryScreen, + name: 'diaryScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), + ), + GoRoute( + path: ConsultationsScreen.path, + name: ConsultationsScreen.name, + pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()), + ), + GoRoute( + path: RouteConstants.chatSelectScreen, + name: 'chatSelectScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), + ), + GoRoute( + path: RouteConstants.notificationsScreen, + name: 'notificationsScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()), + ), + GoRoute( + path: RouteConstants.menuScreen, + name: 'menuScreen', + pageBuilder: (context, state) => NoTransitionPage( + child: BlocBuilder( + builder: (BuildContext context, ProfileAvatarState state) { + if (state is ProfileAvatarStateLoaded) { + return MenuScreen(profileModel: state.profile); + } + return const SizedBox.shrink(); + }, + ), + ), + ), + ], + ), + ]; + } +} diff --git a/lib/core/routing/other_routes.dart b/lib/core/routing/other_routes.dart new file mode 100644 index 00000000..43cf885f --- /dev/null +++ b/lib/core/routing/other_routes.dart @@ -0,0 +1,229 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/core/token_storage.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'; +import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; +import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; +import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; +import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; +import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart'; +import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart'; +import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; +import 'package:eatvio_app/features/order_info/presentation/info_by_order_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/pages/my_recipes_screen.dart'; +import 'package:eatvio_app/features/support/presentation/pages/support_screen.dart'; +import 'package:eatvio_app/features/update_screen/presentation/update_screen.dart'; +import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; +import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:go_router/go_router.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:version/version.dart'; + +/// Остальные маршруты приложения +class OtherRoutes { + /// Создание остальных маршрутов + static List createRoutes() { + return [ + // Welcome and update screens + GoRoute( + path: RouteConstants.welcomeScreen, + name: 'welcomeScreen', + builder: (context, state) => const WelcomeScreen(), + redirect: (context, state) async { + try { + final sharedPreferences = await SharedPreferences.getInstance(); + final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; + await sl().registerPushToken(); + if (welcomeScreenWasShowed == false) { + return null; + } else if (await TokenStorage.haveTokenInStorage()) { + return RouteConstants.feedScreen; + } else { + return RouteConstants.authScreen; + } + } catch (e) { + return RouteConstants.feedScreen; + } + }, + ), + GoRoute( + path: RouteConstants.updateScreen, + name: 'updateScreen', + builder: (context, state) => const UpdateScreen(), + redirect: (context, state) async { + try { + final versions = (await sl().get('$baseUrl/api/app_versions')).data; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + String version = packageInfo.version; + Version currentVersion = Version.parse(version); + Version latestVersion = Version.parse(versions['min_version']); + if (latestVersion > currentVersion) { + return null; + } else { + return RouteConstants.welcomeScreen; + } + } catch (e) { + return RouteConstants.welcomeScreen; + } + }, + ), + + // Archive routes + GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()), + GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()), + GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()), + + // Finance routes + GoRoute(path: RouteConstants.userFinanceScreen, name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()), + GoRoute( + path: CardSettingsScreen.path, + name: CardSettingsScreen.name, + builder: (context, state) { + final chosenPlanId = state.extra as int?; + return CardSettingsScreen(chosenPlanId: chosenPlanId); + }, + ), + + // Content creation routes + GoRoute( + path: RecipeCreateScreen.path, + name: RecipeCreateScreen.name, + builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null), + ), + GoRoute( + path: CreateDietScreen.path, + name: CreateDietScreen.name, + builder: (context, state) { + final diet = state.extra as Diet?; + return CreateDietScreen(diet: diet); + }, + ), + GoRoute( + path: DietPreviewScreen.path, + name: DietPreviewScreen.name, + builder: (context, state) { + final data = state.extra as Diet; + return DietPreviewScreen(diet: data); + }, + ), + GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()), + GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()), + GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()), + GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()), + + // Profile routes + GoRoute( + path: RouteConstants.profileScreen, + name: 'profileScreen', + builder: (context, state) => ProfileScreen(author: state.extra as Author), + ), + GoRoute( + path: RouteConstants.subsScreen, + name: 'subsScreen', + builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType), + ), + + // Chat routes + GoRoute( + path: RouteConstants.chatScreen, + name: 'chatScreen', + builder: (context, state) { + final data = state.extra as Map; + final uuid = data['uuid'] as String; + final sendInitialMessage = data['sendInitialMessage'] as bool?; + return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); + }, + ), + GoRoute( + path: RouteConstants.emptyChatScreen, + name: 'emptyChatScreen', + builder: (context, state) => EmptyChatScreen(author: state.extra as Author), + ), + + // Consultant routes + GoRoute(path: RouteConstants.becomeConsultantScreen, name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()), + GoRoute( + path: RouteConstants.stepperBecomeConsultantScreen, + name: 'stepperBecomeConsultantScreen', + builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), + ), + GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), + GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()), + GoRoute( + path: RouteConstants.photoViewScreen, + name: 'photoViewScreen', + builder: (context, state) => PhotoViewScreen(imageUrls: state.extra as List, index: int.parse(state.pathParameters['index']!)), + ), + + // Other routes + GoRoute(path: RouteConstants.glossaryScreen, name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()), + GoRoute(path: RouteConstants.blockedUsersScreen, name: 'blockedUsersScreen', builder: (context, state) => const BlockedUsersScreen()), + GoRoute( + path: RouteConstants.notificationsSettingsScreen, + name: 'notificationsSettingsScreen', + builder: (context, state) => const NotificationsSettingsScreen(), + ), + GoRoute( + path: UserDetailsScreen.path, + name: UserDetailsScreen.name, + builder: (context, state) => UserDetailsScreen(slug: state.extra as String), + ), + GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()), + GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()), + GoRoute( + path: InfoByOrderScreen.path, + name: InfoByOrderScreen.name, + builder: (context, state) { + final data = state.extra as Map; + final planId = data['planId'] as int; + final successfulPayment = data['successfulPayment'] as bool?; + return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment); + }, + ), + GoRoute( + path: PaymentScreen.path, + name: PaymentScreen.name, + builder: (context, state) { + final data = state.extra as Map; + final planId = data['planId'] as int; + final url = data['url'] as String; + return PaymentScreen(url: url, planId: planId); + }, + ), + GoRoute( + path: UserDiaryScreen.path, + name: UserDiaryScreen.name, + builder: (context, state) { + final data = state.extra as Map; + final diaryId = data['diaryId'] as int; + final currentPlanDay = data['currentDiaryDay'] as int; + return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay); + }, + ), + ]; + } +} diff --git a/lib/core/routing/route_constants.dart b/lib/core/routing/route_constants.dart new file mode 100644 index 00000000..dcc82c17 --- /dev/null +++ b/lib/core/routing/route_constants.dart @@ -0,0 +1,84 @@ +/// Константы маршрутов приложения +class RouteConstants { + // Auth routes + static const String authScreen = '/auth-screen'; + static const String registrationScreen = 'registration-screen'; + static const String forgetPasswordScreen = 'forget-password-screen'; + static const String scanQRCodeScreen = 'scan-qr-code-screen'; + static const String verifyScreen = '/verify-screen'; + static const String verifyEmailScreen = '/verify-email-screen'; + + // Main navigation routes + static const String myProfileScreen = '/my-profile-screen'; + static const String feedScreen = '/feed-screen'; + static const String diaryScreen = '/diary-screen'; + static const String consultationsScreen = '/consultations-screen'; + static const String chatSelectScreen = '/chat-select-screen'; + static const String notificationsScreen = '/notifications-screen'; + static const String menuScreen = '/menu-screen'; + + // Content routes + static const String aboutPlanScreen = '/about-plan-screen'; + static const String aboutRecipesScreen = '/about-recipes-screen'; + static const String aboutDietScreen = '/about-diet-screen'; + static const String blogPostScreen = '/blog-post-screen'; + static const String blogPostCreateScreen = '/blog-post-create-screen'; + + // Profile routes + static const String profileScreen = '/profile-screen'; + static const String subsScreen = '/subs-screen'; + + // Settings routes + static const String settingsScreen = '/settings-screen'; + static const String generalInfoScreen = 'general-info-screen'; + static const String otherInfoScreen = 'other-info-screen'; + static const String physicalActivityScreen = 'physical-activity-screen'; + static const String physicalFormScreen = 'physical-form-screen'; + static const String pressureScreen = 'pressure-screen'; + static const String changePasswordScreen = 'change-password-screen'; + static const String changeEmailScreen = 'change-email-screen'; + + // Consultant routes + static const String becomeConsultantScreen = '/become-consultant-screen'; + static const String stepperBecomeConsultantScreen = '/stepper-become-consultant-screen'; + static const String consultantRatingScreen = '/consultant-rating-screen'; + + // Other routes + static const String welcomeScreen = '/welcome-screen'; + static const String updateScreen = '/update-screen'; + static const String calendarScreen = '/calendar-screen'; + static const String supportScreen = '/support-screen'; + static const String photoViewScreen = '/photo-view-screen'; + static const String chatScreen = '/chat-screen'; + static const String emptyChatScreen = '/empty-chat-screen'; + static const String glossaryScreen = '/glossary-screen'; + static const String blockedUsersScreen = '/blocked-users-screen'; + static const String notificationsSettingsScreen = '/notifications-settings-screen'; + + // Finance routes + static const String userFinanceScreen = '/user-finance-screen'; + static const String cardSettingsScreen = '/card-settings-screen'; + static const String orderInfoScreen = '/order-info-screen'; + static const String paymentScreen = '/payment-screen'; + + // Content creation routes + static const String recipeCreateScreen = '/recipe-create-screen'; + static const String createDietScreen = '/create-diet-screen'; + static const String dietPreviewScreen = '/diet-preview-screen'; + static const String createPlanScreen = '/create-plan-screen'; + static const String myDietsScreen = '/my-diets-screen'; + static const String myPlansScreen = '/my-plans-screen'; + static const String myRecipesScreen = '/my-recipes-screen'; + + // User details + static const String userDetailsScreen = '/user-details-screen'; + static const String userDiaryScreen = '/user-diary-screen'; + + // Dev tools + static const String devToolsScreen = '/dev-tools-screen'; + + // Archive routes + static const String consultationsArchiveScreen = '/consultations-archive-screen'; + static const String consultantFinancesScreen = '/consultant-finances-screen'; + static const String userPurchasesScreen = '/user-purchases-screen'; +} diff --git a/lib/core/routing/settings_routes.dart b/lib/core/routing/settings_routes.dart new file mode 100644 index 00000000..8316617c --- /dev/null +++ b/lib/core/routing/settings_routes.dart @@ -0,0 +1,62 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/change_password_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart'; +import 'package:go_router/go_router.dart'; + +/// Маршруты для настроек +class SettingsRoutes { + /// Создание маршрутов настроек + static List createRoutes() { + return [ + GoRoute( + path: RouteConstants.settingsScreen, + name: 'settingsScreen', + builder: (context, state) => const SettingsScreen(), + routes: [ + GoRoute( + path: RouteConstants.generalInfoScreen, + name: 'generalInfoScreen', + builder: (context, state) => GeneralInfoScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.otherInfoScreen, + name: 'otherInfoScreen', + builder: (context, state) => OtherInfoScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.physicalActivityScreen, + name: 'physicalActivityScreen', + builder: (context, state) => PhysicalActivityScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.physicalFormScreen, + name: 'physicalFormScreen', + builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.pressureScreen, + name: 'pressureScreen', + builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.changePasswordScreen, + name: 'changePasswordScreen', + builder: (context, state) => ChangePasswordScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.changeEmailScreen, + name: 'changeEmailScreen', + builder: (context, state) => ChangeEmailScreen(settingsEntity: state.extra as SettingsEntity), + ), + ], + ), + ]; + } +} diff --git a/lib/core/utils/validation_utils.dart b/lib/core/utils/validation_utils.dart new file mode 100644 index 00000000..1add6cda --- /dev/null +++ b/lib/core/utils/validation_utils.dart @@ -0,0 +1,87 @@ +import 'package:eatvio_app/core/constants/app_constants.dart'; + +/// Утилиты для валидации +class ValidationUtils { + /// Валидация email + static bool isValidEmail(String email) { + if (email.isEmpty) return false; + final regex = RegExp(AppConstants.emailRegex); + return regex.hasMatch(email); + } + + /// Валидация телефона + static bool isValidPhone(String phone) { + if (phone.isEmpty) return false; + final regex = RegExp(AppConstants.phoneRegex); + return regex.hasMatch(phone); + } + + /// Валидация URL + static bool isValidUrl(String url) { + if (url.isEmpty) return false; + final regex = RegExp(AppConstants.urlRegex); + return regex.hasMatch(url); + } + + /// Валидация длины текста + static bool isValidTextLength(String text, {int? maxLength}) { + final length = maxLength ?? AppConstants.maxTextLength; + return text.length <= length; + } + + /// Валидация длины заголовка + static bool isValidTitleLength(String title) { + return title.isNotEmpty && title.length <= AppConstants.maxTitleLength; + } + + /// Валидация длины описания + static bool isValidDescriptionLength(String description) { + return description.length <= AppConstants.maxDescriptionLength; + } + + /// Валидация пароля + static bool isValidPassword(String password) { + // Минимум 8 символов, хотя бы одна буква и одна цифра + if (password.length < 8) return false; + final hasLetter = RegExp(r'[a-zA-Z]').hasMatch(password); + final hasDigit = RegExp(r'[0-9]').hasMatch(password); + return hasLetter && hasDigit; + } + + /// Валидация имени пользователя + static bool isValidUsername(String username) { + // От 3 до 20 символов, только буквы, цифры и подчеркивания + if (username.length < 3 || username.length > 20) return false; + final regex = RegExp(r'^[a-zA-Z0-9_]+$'); + return regex.hasMatch(username); + } + + /// Получение сообщения об ошибке валидации + static String? getValidationErrorMessage(String? value, ValidationType type) { + if (value == null || value.isEmpty) { + return 'Поле не может быть пустым'; + } + + switch (type) { + case ValidationType.email: + return isValidEmail(value) ? null : 'Введите корректный email'; + case ValidationType.phone: + return isValidPhone(value) ? null : 'Введите корректный номер телефона'; + case ValidationType.url: + return isValidUrl(value) ? null : 'Введите корректный URL'; + case ValidationType.password: + return isValidPassword(value) ? null : 'Пароль должен содержать минимум 8 символов, включая буквы и цифры'; + case ValidationType.username: + return isValidUsername(value) ? null : 'Имя пользователя должно содержать от 3 до 20 символов (буквы, цифры, подчеркивания)'; + case ValidationType.title: + return isValidTitleLength(value) ? null : 'Заголовок не может быть пустым и должен содержать не более ${AppConstants.maxTitleLength} символов'; + case ValidationType.description: + return isValidDescriptionLength(value) ? null : 'Описание не может содержать более ${AppConstants.maxDescriptionLength} символов'; + case ValidationType.text: + return isValidTextLength(value) ? null : 'Текст не может содержать более ${AppConstants.maxTextLength} символов'; + } + } +} + +/// Типы валидации +enum ValidationType { email, phone, url, password, username, title, description, text } diff --git a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart index df228298..6e93944a 100644 --- a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart +++ b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart @@ -1,61 +1,34 @@ -import 'dart:developer'; - +import 'package:eatvio_app/core/cubit/base_cubit.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart'; import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; -class BlogPostCreateCubit extends Cubit { +class BlogPostCreateCubit extends BaseCubit { final BlogPostRepository blogPostRepository; - BlogPostCreateCubit({ - required this.blogPostRepository, - }) : super(const BlogPostCreateState()); + BlogPostCreateCubit({required this.blogPostRepository}) : super(const BlogPostCreateState()); - Future createBlogPost({ - String? title, - String? videoUrl, - String? description, - XFile? image, - }) async { + Future createBlogPost({String? title, String? videoUrl, String? description, XFile? image}) async { try { emit(const BlogPostCreateState.loading()); - await blogPostRepository.createBlogPost( - title, - videoUrl, - description, - image, - ); + await blogPostRepository.createBlogPost(title, videoUrl, description, image); + emit(const BlogPostCreateState.loaded()); return true; - } catch (e, st) { - log(e.toString(), stackTrace: st); + } catch (error, stackTrace) { + handleError(error, stackTrace); emit(const BlogPostCreateState.loaded()); return false; } } - Future updateBlogPost({ - required int id, - String? title, - String? videoUrl, - String? description, - XFile? image, - String? oldImageUrl, - }) async { + Future updateBlogPost({required int id, String? title, String? videoUrl, String? description, XFile? image, String? oldImageUrl}) async { try { emit(const BlogPostCreateState.loading()); - await blogPostRepository.updateBlogPost( - id, - title, - videoUrl, - description, - image, - oldImageUrl, - ); + await blogPostRepository.updateBlogPost(id, title, videoUrl, description, image, oldImageUrl); emit(const BlogPostCreateState.loaded()); return true; - } catch (e, st) { - log(e.toString(), stackTrace: st); + } catch (error, stackTrace) { + handleError(error, stackTrace); emit(const BlogPostCreateState.loaded()); return false; } @@ -65,8 +38,8 @@ class BlogPostCreateCubit extends Cubit { try { await blogPostRepository.deleteBlogPost(id); return true; - } catch (e, st) { - log(e.toString(), stackTrace: st); + } catch (error, stackTrace) { + handleError(error, stackTrace); return false; } } From 766d5ba248fd573f9a6088227adab8296aa84f73 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 2 Oct 2025 00:07:28 +0300 Subject: [PATCH 25/49] fix: cubit --- lib/core/di/cubit_registrations.dart | 41 ++++++++-------------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index 156e43b1..babff4f6 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -23,6 +23,7 @@ import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/con import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; @@ -60,15 +61,9 @@ class CubitRegistrations { /// Регистрация Cubit'ов как синглтонов static void _registerSingletonCubits() { sl.registerLazySingleton(() => NewMessagesCubit()); - sl.registerLazySingleton( - () => NotificationsCubit(wsManager: sl(), notificationsRepository: sl()), - ); - sl.registerLazySingleton( - () => ProfileAvatarCubit(profileRepository: sl()), - ); - sl.registerLazySingleton( - () => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl()), - ); + sl.registerLazySingleton(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl())); + sl.registerLazySingleton(() => ProfileAvatarCubit(profileRepository: sl())); + sl.registerLazySingleton(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl())); } /// Регистрация Cubit'ов как фабрик @@ -81,34 +76,25 @@ class CubitRegistrations { // Feed related cubits sl.registerFactory(() => FeedCubit(feedRepository: sl())); - sl.registerFactory( - () => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()), - ); + sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl())); sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); sl.registerFactory(() => ConsultantsCubit(feedRepository: sl())); sl.registerFactory(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl())); + sl.registerFactory(() => FeedProfileCubit(profileRepository: sl())); // Chat related cubits - sl.registerFactory( - () => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl()), - ); + sl.registerFactory(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl())); // Profile related cubits - sl.registerFactory( - () => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()), - ); + sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl())); sl.registerFactory(() => profile_plans.PlansCubit(plansRepository: sl())); sl.registerFactory(() => SubCubit(profileRepository: sl())); // Content creation cubits - sl.registerFactory( - () => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl()), - ); + sl.registerFactory(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl())); sl.registerFactory(() => CreatePlanCubit(createPlanRepository: sl())); - sl.registerFactory( - () => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl()), - ); + sl.registerFactory(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl())); sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); // Content viewing cubits @@ -135,12 +121,7 @@ class CubitRegistrations { ), ); sl.registerFactory( - () => SettingsCubit( - profileRepository: sl(), - settingsRepository: sl(), - consultantRepository: sl(), - consultationsRepository: sl(), - ), + () => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl()), ); sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); sl.registerFactory(() => ComplainingCubit(repository: sl())); From 086e6cf94ea6aefad278ff2255eb1b2ff0f3fa30 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 2 Oct 2025 00:13:35 +0300 Subject: [PATCH 26/49] fix: recipe --- lib/core/di/repository_registrations.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/core/di/repository_registrations.dart b/lib/core/di/repository_registrations.dart index 71d1523d..6d467810 100644 --- a/lib/core/di/repository_registrations.dart +++ b/lib/core/di/repository_registrations.dart @@ -12,6 +12,7 @@ import 'package:eatvio_app/features/consultant_rating/repositories/consultant_ra import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; +import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; @@ -56,6 +57,7 @@ class RepositoryRegistrations { sl.registerLazySingleton(() => PlansRepository(plansService: sl())); sl.registerLazySingleton(() => PlanRepository(planService: sl())); sl.registerLazySingleton(() => AboutRecipeRepository(recipeService: sl())); + sl.registerLazySingleton(() => RecipeRepository(recipeService: sl())); sl.registerLazySingleton(() => ComplainingRepository(service: sl())); sl.registerLazySingleton(() => AuthRepository(authService: sl())); sl.registerLazySingleton(() => CardsRepository(cardsService: sl())); From 09d025252d6587bde222d797b158462aa6df7711 Mon Sep 17 00:00:00 2001 From: misha Date: Thu, 2 Oct 2025 10:25:10 +0300 Subject: [PATCH 27/49] feat(fixes): fixed analyzer issue --- lib/core/locator_service.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index ffe747a9..c9c61e56 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -46,7 +46,6 @@ import 'package:eatvio_app/features/blog_post_create/services/blog_post_service. import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; -import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; From 37cedb223858a75f4b0e9994972673ac2b418143 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 3 Oct 2025 16:57:22 +0300 Subject: [PATCH 28/49] fix(rating_consultant): Issue-513 --- assets/icons/consultant_rating_icon.svg | 1 + lib/core/di/cubit_registrations.dart | 2 +- .../models/statistics_model.dart | 1 + .../consultant_rating_screen.dart | 11 ++- .../cubit/consultant_rating_cubit.dart | 10 ++- .../cubit/consultant_rating_state.dart | 1 + .../widgets/statistic_widget.dart | 2 +- .../feed/repositories/feed_repository.dart | 4 +- lib/features/feed/services/feed_service.dart | 79 +++++-------------- .../menu/presentation/pages/menu_screen.dart | 2 +- lib/generated/intl/messages_en.dart | 3 + lib/generated/l10n.dart | 10 +++ lib/l10n/intl_en.arb | 1 + 13 files changed, 56 insertions(+), 71 deletions(-) create mode 100644 assets/icons/consultant_rating_icon.svg diff --git a/assets/icons/consultant_rating_icon.svg b/assets/icons/consultant_rating_icon.svg new file mode 100644 index 00000000..493b4d95 --- /dev/null +++ b/assets/icons/consultant_rating_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index babff4f6..ec2de9ce 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -135,7 +135,7 @@ class CubitRegistrations { sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); - sl.registerFactory(() => ConsultantRatingCubit(sl())); + sl.registerFactory(() => ConsultantRatingCubit(feedRepository: sl(), repository: sl(), profileRepository: sl())); sl.registerFactory(() => ViewsCubit(viewsRepository: sl())); sl.registerFactory(() => UserDetailsCubit(profileRepository: sl())); } diff --git a/lib/features/consultant_rating/models/statistics_model.dart b/lib/features/consultant_rating/models/statistics_model.dart index bbac675b..1e04eebc 100644 --- a/lib/features/consultant_rating/models/statistics_model.dart +++ b/lib/features/consultant_rating/models/statistics_model.dart @@ -13,6 +13,7 @@ abstract class StatisticsModel with _$StatisticsModel { @JsonKey(name: 'count_diets') int? dietsCount, @JsonKey(name: 'count_consultants') int? consultationsCount, @JsonKey(name: 'count_products_nutritional_values') int? qwe, + @JsonKey(name: 'referral_link') String? refLink, }) = _StatisticsModel; factory StatisticsModel.fromJson(Map json) => _$StatisticsModelFromJson(json); diff --git a/lib/features/consultant_rating/presentation/consultant_rating_screen.dart b/lib/features/consultant_rating/presentation/consultant_rating_screen.dart index 8c6cb3d4..000367e0 100644 --- a/lib/features/consultant_rating/presentation/consultant_rating_screen.dart +++ b/lib/features/consultant_rating/presentation/consultant_rating_screen.dart @@ -1,6 +1,7 @@ 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/toasts.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/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; @@ -10,6 +11,7 @@ import 'package:eatvio_app/features/consultant_rating/presentation/widgets/ratin import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ConsultantRatingScreen extends StatelessWidget { @@ -53,8 +55,8 @@ class ConsultantRatingPageContent extends StatelessWidget { child: Column( children: [ const SizedBox(height: 16), - const RatingWidget( - rating: 1058, + RatingWidget( + rating: state.absoluteRating!, ), const SizedBox(height: 16), Padding( @@ -67,7 +69,10 @@ class ConsultantRatingPageContent extends StatelessWidget { side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), ), - onPressed: () {}, + onPressed: () { + Clipboard.setData(ClipboardData(text: state.statistics?.refLink ?? '')); + showInfoToast(s.link_was_copied); + }, ), ), const SizedBox(height: 16), diff --git a/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart b/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart index 291f450c..d3ff72a9 100644 --- a/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart +++ b/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart @@ -2,17 +2,23 @@ import 'dart:developer'; import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart'; import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart'; +import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ConsultantRatingCubit extends Cubit { final ConsultantRatingRepository repository; - ConsultantRatingCubit(this.repository) : super(ConsultantRatingState()); + final FeedRepository feedRepository; + final ProfileRepository profileRepository; + + ConsultantRatingCubit({required this.repository, required this.feedRepository, required this.profileRepository}) : super(ConsultantRatingState()); Future getConsultantStatistics({String? date}) async { try { emit(state.copyWith(isStatisticsLoading: true)); final statistics = await repository.getConsultantStatistics(date: date); - emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null)); + final absoluteRating = await feedRepository.getConsultants((await profileRepository.getMyProfile()).name); + emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null, absoluteRating: absoluteRating.first.totalScores)); } catch (e, st) { emit(state.copyWith(isStatisticsLoading: true, error: e.toString())); log(e.toString(), stackTrace: st); diff --git a/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart b/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart index 20cc458f..7c1e0942 100644 --- a/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart +++ b/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart @@ -12,5 +12,6 @@ abstract class ConsultantRatingState with _$ConsultantRatingState { @Default(null) StatisticsModel? statistics, @Default(null) String? error, @Default([]) List ratings, + @Default(0) int? absoluteRating, }) = _ConsultantRatingState; } diff --git a/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart b/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart index 3be44129..ef148adf 100644 --- a/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart +++ b/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart @@ -71,7 +71,7 @@ class StatisticWidget extends StatelessWidget { children: [ Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)), const SizedBox(width: 8), - Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.active_consultations_count)), + Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.added_products_in_glossary)), ], ), const SizedBox( diff --git a/lib/features/feed/repositories/feed_repository.dart b/lib/features/feed/repositories/feed_repository.dart index 108a8b11..e540486f 100644 --- a/lib/features/feed/repositories/feed_repository.dart +++ b/lib/features/feed/repositories/feed_repository.dart @@ -133,9 +133,9 @@ class FeedRepository { } } - Future> getConsultants() async { + Future> getConsultants([String? name]) async { try { - return (await feedService.getConsultants()).data; + return (await feedService.getConsultants(name: name)).data; } catch (e) { rethrow; } diff --git a/lib/features/feed/services/feed_service.dart b/lib/features/feed/services/feed_service.dart index 265c2d38..c756f8c7 100644 --- a/lib/features/feed/services/feed_service.dart +++ b/lib/features/feed/services/feed_service.dart @@ -16,15 +16,10 @@ abstract class FeedService { factory FeedService(Dio dio, {String baseUrl}) = _FeedService; @GET('/api/blogPosts/index') - Future getBlogPostList({ - @Query('page') required int page, - @Query('query_search') String? query, - }); + Future getBlogPostList({@Query('page') required int page, @Query('query_search') String? query}); @GET('/api/blogPosts/show/{postId}') - Future getBlogPost({ - @Path('postId') required int postId, - }); + Future getBlogPost({@Path('postId') required int postId}); @GET('/api/recipes/index') Future getRecipes({ @@ -53,88 +48,50 @@ abstract class FeedService { }); @GET('/api/blogPosts/{id}/comments/show') - Future getBlogPostComments({ - @Path() required String id, - }); + Future getBlogPostComments({@Path() required String id}); @POST('/api/blogPosts/{id}/comment/create') - Future sendBlogPostComment({ - @Path() required String id, - @Part() required String body, - @Part() int? parentCommentId, - }); + Future sendBlogPostComment({@Path() required String id, @Part() required String body, @Part() int? parentCommentId}); @GET('/api/diets/{id}/comments/show') - Future getDietsComments({ - @Path() required String id, - }); + Future getDietsComments({@Path() required String id}); @POST('/api/diets/{id}/comment/create') - Future sendDietsComment({ - @Path() required String id, - @Part() required String body, - @Part() int? parentCommentId, - }); + Future sendDietsComment({@Path() required String id, @Part() required String body, @Part() int? parentCommentId}); @GET('/api/plans/{slug}/comments/show') - Future getPlansComments({ - @Path() required String slug, - }); + Future getPlansComments({@Path() required String slug}); @POST('/api/plans/{slug}/comment/create') - Future sendPlansComment({ - @Path() required String slug, - @Part() required String body, - @Part() int? parentCommentId, - }); + Future sendPlansComment({@Path() required String slug, @Part() required String body, @Part() int? parentCommentId}); @GET('/api/recipes/{id}/comments/show') - Future getRecipesComments({ - @Path() required String id, - }); + Future getRecipesComments({@Path() required String id}); @POST('/api/recipes/{id}/comment/create') - Future sendRecipesComment({ - @Path() required String id, - @Part() required String body, - @Part() int? parentCommentId, - }); + Future sendRecipesComment({@Path() required String id, @Part() required String body, @Part() int? parentCommentId}); @POST('/api/blogPosts/{id}/like') - Future likeBlogPost({ - @Path() required int id, - }); + Future likeBlogPost({@Path() required int id}); @POST('/api/blogPosts/{id}/unliked') - Future unlikedBlogPost({ - @Path() required int id, - }); + Future unlikedBlogPost({@Path() required int id}); @GET('/api/consultants/index') - Future getConsultants(); + Future getConsultants({@Query('filters[name][\$startsWith]') String? name}); @POST('/api/blogPosts/{id}/view') - Future viewPost({ - @Path() required int id, - }); + Future viewPost({@Path() required int id}); @POST('/api/recipes/{slug}/view') - Future viewRecipe({ - @Path() required String slug, - }); + Future viewRecipe({@Path() required String slug}); @POST('/api/diets/{id}/view') - Future viewDiet({ - @Path() required int id, - }); + Future viewDiet({@Path() required int id}); @POST('/api/plans/{slug}/view') - Future viewPlan({ - @Path() required String slug, - }); + Future viewPlan({@Path() required String slug}); @POST('/api/views') - Future viewContent({ - @Body() required ViewModel viewModel, - }); + Future viewContent({@Body() required ViewModel viewModel}); } diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 5c350d3c..123e5faf 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -58,7 +58,7 @@ class MenuScreen extends StatelessWidget { if (profileModel.roles.contains('consultant')) _divider(context), if (profileModel.roles.contains('consultant')) _tile( - leading: 'assets/icons/finance.svg', + leading: 'assets/icons/consultant_rating_icon.svg', title: 'Мой рейтинг', onTap: () { GoRouter.of(context).push(ConsultantRatingScreen.path); diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index c054a4b5..dc9e4c16 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -66,6 +66,9 @@ class MessageLookup extends MessageLookupByLibrary { "add_card": MessageLookupByLibrary.simpleMessage("Привязать карту"), "add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"), "add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"), + "added_products_in_glossary": MessageLookupByLibrary.simpleMessage( + "Кол-во продуктов добавленных в глоссарий", + ), "adding_plan": MessageLookupByLibrary.simpleMessage("Добавление плана"), "additional_information": MessageLookupByLibrary.simpleMessage( "Дополнительная информация", diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 85f74918..0ccdeac3 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2399,6 +2399,16 @@ class S { ); } + /// `Кол-во продуктов добавленных в глоссарий` + String get added_products_in_glossary { + return Intl.message( + 'Кол-во продуктов добавленных в глоссарий', + name: 'added_products_in_glossary', + desc: '', + args: [], + ); + } + /// `Кол-во активных консультаций` String get active_consultations_count { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 57145fb9..1fc575a8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -300,6 +300,7 @@ "published_recipes_count": "Кол-во опубликованных рецептов", "published_diets_count": "Кол-во опубликованных диет", "consultations_count": "Кол-во проведенных консулдьтаций", + "added_products_in_glossary": "Кол-во продуктов добавленных в глоссарий", "active_consultations_count": "Кол-во активных консультаций", "rating_history": "Начисление рейтинга", "something_went_wrong": "Упс! Что-то пошло не так", From 5275273830a9cb00ae15b9771a2509d9c7d66671 Mon Sep 17 00:00:00 2001 From: misha Date: Mon, 6 Oct 2025 16:46:59 +0300 Subject: [PATCH 29/49] feat(socialAuth): added profile data fetching for social aunthentication --- lib/core/cubit/base_cubit.dart | 2 +- .../auth/entities/auth_result_entity.dart | 51 ++++ .../auth/models/yandex_profile_model.dart | 30 +++ .../auth/presentation/pages/auth_screen.dart | 28 +-- lib/features/auth/sdk/social_auth_facade.dart | 220 +++++++++++------- 5 files changed, 228 insertions(+), 103 deletions(-) create mode 100644 lib/features/auth/entities/auth_result_entity.dart create mode 100644 lib/features/auth/models/yandex_profile_model.dart diff --git a/lib/core/cubit/base_cubit.dart b/lib/core/cubit/base_cubit.dart index 3cd6c9d9..03ff3cc2 100644 --- a/lib/core/cubit/base_cubit.dart +++ b/lib/core/cubit/base_cubit.dart @@ -7,7 +7,7 @@ abstract class BaseCubit extends Cubit { /// Унифицированная обработка ошибок void handleError(Object error, [StackTrace? stackTrace]) { - log('Error in ${runtimeType}: $error', stackTrace: stackTrace); + log('Error in $runtimeType: $error', stackTrace: stackTrace); // Здесь можно добавить отправку ошибок в Sentry или другую систему мониторинга } diff --git a/lib/features/auth/entities/auth_result_entity.dart b/lib/features/auth/entities/auth_result_entity.dart new file mode 100644 index 00000000..2f296e5b --- /dev/null +++ b/lib/features/auth/entities/auth_result_entity.dart @@ -0,0 +1,51 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:vk_id/vk_id.dart'; + +part 'auth_result_entity.freezed.dart'; + +@freezed +abstract class AuthResultEntity with _$AuthResultEntity { + const factory AuthResultEntity({ + required String accessToken, + required String userId, + String? refreshToken, + String? firstName, + String? lastName, + String? email, + String? phone, + GenderEnum? gender, + String? birthDate, + }) = _AuthResultEntity; +} + +enum GenderEnum { + male, + female, + notSet; + + static GenderEnum? fromVkGender(VkSex? vkGender) { + switch (vkGender) { + case VkSex.male: + return GenderEnum.male; + case VkSex.female: + return GenderEnum.female; + case VkSex.notStated: + return GenderEnum.notSet; + case null: + return null; + } + } + + static GenderEnum? fromString(String? gender) { + switch (gender) { + case 'male': + return GenderEnum.male; + case 'female': + return GenderEnum.female; + case 'notSet': + return GenderEnum.notSet; + default: + return null; + } + } +} diff --git a/lib/features/auth/models/yandex_profile_model.dart b/lib/features/auth/models/yandex_profile_model.dart new file mode 100644 index 00000000..9bb84998 --- /dev/null +++ b/lib/features/auth/models/yandex_profile_model.dart @@ -0,0 +1,30 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'yandex_profile_model.freezed.dart'; +part 'yandex_profile_model.g.dart'; + +@freezed +abstract class YandexProfileModel with _$YandexProfileModel { + factory YandexProfileModel({ + required String id, + required String login, + required String birthday, + @JsonKey(name: 'client_id') required String clientId, + @JsonKey(name: 'display_name') required String displayName, + @JsonKey(name: 'real_name') required String realName, + @JsonKey(name: 'first_name') required String firstName, + @JsonKey(name: 'last_name') required String lastName, + @JsonKey(name: 'sex') required String sex, + @JsonKey(name: 'default_email') required String defaultEmail, + @JsonKey(name: 'default_phone') required YandexPhoneData defaultPhone, + }) = _YandexProfileModel; + + factory YandexProfileModel.fromJson(Map json) => _$YandexProfileModelFromJson(json); +} + +@freezed +abstract class YandexPhoneData with _$YandexPhoneData { + factory YandexPhoneData({required int id, required String number}) = _YandexPhoneData; + + factory YandexPhoneData.fromJson(Map json) => _$YandexPhoneDataFromJson(json); +} diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 30ef29cf..6f49ea2a 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -1,6 +1,7 @@ // ignore_for_file: unused_element, duplicate_ignore import 'dart:async'; +import 'dart:developer'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; @@ -127,7 +128,7 @@ class _AuthScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _SocialButton( - onTap: () => _onVkPressed(context), + onTap: () => _onVkPressed(), backgroundColor: const Color(0xFF2787F5), tooltip: 'VK ID', child: SvgPicture.asset('assets/icons/vk_logo.svg', height: 24, width: 24), @@ -388,19 +389,14 @@ class _SocialButton extends StatelessWidget { } } -void _onVkPressed(BuildContext context) async { +void _onVkPressed() async { try { - final accessToken = await SocialAuthFacade.signInWithVk(context: context); + final accessToken = await SocialAuthFacade.signInWithVk(); if (accessToken == null) { return; } - final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); - if (isAuthenticated) { - if (AppRouter.navigatorKey.currentContext != null) { - await _reconnectAfterAuth(); - } - } } catch (e) { + log(e.toString()); } } @@ -410,11 +406,8 @@ void _onOkPressed() async { if (accessToken == null) { return; } - final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); - if (isAuthenticated) { - await _reconnectAfterAuth(); - } } catch (e) { + log(e.toString()); } } @@ -424,21 +417,14 @@ void _onMailRuPressed() async { if (accessToken == null) { return; } - final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); - if (isAuthenticated) { - await _reconnectAfterAuth(); - } } catch (e) { + log(e.toString()); } } void _onYandexPressed() async { final accessToken = await SocialAuthFacade.signInWithYandex(); if (accessToken == null) return; - final isAuthenticated = await sl().getTokenBySocial(provider: 'yandex', accessToken: accessToken); - if (isAuthenticated) { - await _reconnectAfterAuth(); - } } Future _reconnectAfterAuth() async { diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 74959498..a235f03d 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -1,80 +1,125 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; +import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_login_yandex/flutter_login_yandex.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/env_config.dart'; +import 'package:http/http.dart' as http; import 'package:vk_id/vk_id.dart'; import 'package:webview_flutter/webview_flutter.dart'; -class VkAuthResult { - VkAuthResult({required this.accessToken, this.refreshToken, this.userId, this.firstName, this.lastName, this.email, this.phone, this.gender, this.birthDate}); - - final String accessToken; - final String? refreshToken; - final String? userId; - final String? firstName; - final String? lastName; - final String? email; - final String? phone; - final String? gender; - final String? birthDate; -} - class SocialAuthFacade { - static Future signInWithVk({required BuildContext context}) async { + static Future signInWithVk() async { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return null; final result = await signInWithVkAndFetchProfile(context: context); return result?.accessToken; } - static Future signInWithVkAndFetchProfile({required BuildContext context}) async { + static Future signInWithVkAndFetchProfile({required BuildContext context}) async { final clientIdStr = EnvConfig.vkClientId.trim(); if (clientIdStr.isEmpty) { return null; } try { - dynamic controller = VkIDController(clID: int.parse(clientIdStr)); + final controller = VkIDController(clID: int.parse(clientIdStr)); final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); - String authUrl; - if (authUri is MapEntry) { - authUrl = authUri.value.toString(); - } else { - authUrl = authUri.toString(); - } - + final authUrl = authUri.value.toString(); + final codeVerifier = authUri.key; final redirectScheme = 'vk$clientIdStr://'; final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); if (redirectParams == null) { return null; } - - final code = redirectParams['code']; - final deviceId = redirectParams['device_id']; - if (code == null || deviceId == null) { - return null; - } - return VkAuthResult(accessToken: code); + return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier); } catch (e) { return null; } } + static Future _parseVKAuthResult({ + required Map redirectParams, + required VkIDController controller, + required String codeVerifier, + }) async { + final code = redirectParams['code']; + final deviceId = redirectParams['device_id']; + final state = redirectParams['state']; + if (code == null || deviceId == null || state == null) { + return null; + } + final authRes = await controller.retrieveOAuthToken(authorizationCode: code, deviceId: deviceId, codeVerifier: codeVerifier, state: state); + final auth = authRes.result; + if (auth == null) { + var msg = "Retrieve OAuth token error"; + final err = authRes.error?.vkErr; + if (err != null) { + msg += ": ${err.error} - ${err.description}"; + } + log(msg); + return null; + } + final profileRes = await controller.getProfileInfo(); + final profile = profileRes.result; + if (profile == null) { + var msg = "Profile info load error"; + final err = profileRes.error?.vkErr; + if (err != null) { + msg += ": ${err.error} - ${err.description}"; + } + log(msg); + return null; + } + + return AuthResultEntity( + accessToken: auth.accessToken, + refreshToken: auth.refreshToken, + userId: profile.userId.toString(), + firstName: profile.firstName, + lastName: profile.lastName, + email: profile.email, + phone: profile.phone, + gender: GenderEnum.fromVkGender(profile.parsedSex), + birthDate: profile.bDayDt?.toString(), + ); + } + + Future?> exchangeCodeForToken(String code, String clientId, String clientSecret) async { + final response = await http.post( + Uri.parse('https://oauth.yandex.ru/token'), + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret}, + ); + + if (response.statusCode == 200) { + return jsonDecode(response.body) as Map; + } else { + log('Error exchanging code: ${response.body}'); + } + return null; + } + static Future signInWithOk() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; - return _signInWithVkIdProvider(context: context, provider: 'ok'); + final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu); + return vkAuthRes?.accessToken; } static Future signInWithMailRu() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; - return _signInWithVkIdProvider(context: context, provider: 'mail'); + final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu); + return vkAuthRes?.accessToken; } - static Future signInWithYandex() async { + static Future signInWithYandex() async { final clientId = EnvConfig.yandexClientId.trim(); final redirectUri = EnvConfig.yandexRedirectUri.trim(); @@ -87,7 +132,7 @@ class SocialAuthFacade { if (context == null) return null; final authorizeUrl = Uri.https('oauth.yandex.ru', '/authorize', { - 'response_type': 'token', + 'response_type': 'code', 'client_id': clientId, 'redirect_uri': redirectUri, 'force_confirm': 'yes', @@ -99,62 +144,78 @@ class SocialAuthFacade { if (params == null) return null; - final token = params['access_token']; - if (token != null && token.isNotEmpty) { - return token; - } - final code = params['code']; - if (code != null && code.isNotEmpty) { - return code; - } - - return null; - } catch (e) { - try { - final plugin = FlutterLoginYandex(); - final response = await plugin.signIn(); - final token = (response is Map) ? response['token'] as String? : null; - return (token != null && token.isNotEmpty) ? token : null; - } catch (_) { + if (code == null || code.isEmpty) { return null; } + + final tokens = await _getTokensFomYandex(code: code, clientId: clientId, clientSecret: EnvConfig.yandexClientSecret); + if (tokens == null) { + return null; + } + final response = await http.get(Uri.parse('https://login.yandex.ru/info?format=json'), headers: {'Authorization': 'OAuth ${tokens.accessToken}'}); + + final Map json = jsonDecode(response.body); + final yandexProfileData = YandexProfileModel.fromJson(json); + return AuthResultEntity( + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + userId: yandexProfileData.id, + firstName: yandexProfileData.firstName, + lastName: yandexProfileData.lastName, + email: yandexProfileData.defaultEmail, + phone: yandexProfileData.defaultPhone.number, + gender: GenderEnum.fromString(yandexProfileData.sex), + birthDate: yandexProfileData.birthday, + ); + } catch (e) { + log(e.toString()); } + return null; } - static Future _signInWithVkIdProvider({required BuildContext context, required String provider}) async { + static Future<({String accessToken, String refreshToken})?> _getTokensFomYandex({ + required String code, + required String clientId, + required String clientSecret, + }) async { + final response = await http.post( + Uri.parse('https://oauth.yandex.ru/token'), + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret}, + ); + + if (response.statusCode != 200) { + log('Yandex token exchange failed: ${response.body}'); + return null; + } + + final tokens = jsonDecode(response.body); + final accessToken = tokens['access_token'] as String; + final refreshToken = tokens['refresh_token'] as String; + return (accessToken: accessToken, refreshToken: refreshToken); + } + + static Future _signInWithVkIdProvider({required BuildContext context, required VkProvider provider}) async { final clientIdStr = EnvConfig.vkClientId.trim(); if (clientIdStr.isEmpty) { return null; } try { - dynamic controller = VkIDController(clID: int.parse(clientIdStr)); + final controller = VkIDController(clID: int.parse(clientIdStr)); - dynamic authUri; - try { - authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); - } catch (e) { - try { - authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: null); - } catch (e2) { - authUri = controller.generateAuthorizeLinkWithCodeVerifier(); - } - } - - // Extract URL from MapEntry - String authUrl; - if (authUri is MapEntry) { - authUrl = authUri.value.toString(); - } else { - authUrl = authUri.toString(); - } + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); + final codeVerifier = authUri.key; + final authUrl = authUri.value.toString(); final redirectScheme = 'vk$clientIdStr://'; final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); if (redirectParams == null) return null; + final code = redirectParams['code']; final deviceId = redirectParams['device_id']; if (code == null || deviceId == null) return null; - return code; + + return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier); } catch (e) { return null; } @@ -190,12 +251,9 @@ class SocialAuthFacade { } return NavigationDecision.navigate; }, - onPageStarted: (url) { - }, - onPageFinished: (url) { - }, - onWebResourceError: (error) { - }, + onPageStarted: (url) {}, + onPageFinished: (url) {}, + onWebResourceError: (error) {}, ), ); @@ -204,7 +262,7 @@ class SocialAuthFacade { } catch (e) { return null; } - + if (!context.mounted) return null; return showDialog>( context: context, barrierDismissible: false, From 03448ebd2d5e205b0fde4aa644d1ba0b4f0daab2 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 6 Oct 2025 17:27:11 +0300 Subject: [PATCH 30/49] feat(oauth): Issue-691 --- lib/core/routing/auth_routes.dart | 2 + lib/core/routing/route_constants.dart | 1 + .../pages/register_via_social_screen.dart | 100 ++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 lib/features/auth/presentation/pages/register_via_social_screen.dart diff --git a/lib/core/routing/auth_routes.dart b/lib/core/routing/auth_routes.dart index fc4558d1..67ecbca6 100644 --- a/lib/core/routing/auth_routes.dart +++ b/lib/core/routing/auth_routes.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/register_via_social_screen.dart'; import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart'; @@ -32,6 +33,7 @@ class AuthRoutes { builder: (context, state) => VerifyScreen(passKey: state.extra as String), ), GoRoute(path: RouteConstants.verifyEmailScreen, name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), + GoRoute(path: RouteConstants.registerViaSocialScreen, name: 'registerViaSocialScreen', builder: (context, state) => const RegisterViaSocialScreen()), ]; } } diff --git a/lib/core/routing/route_constants.dart b/lib/core/routing/route_constants.dart index dcc82c17..1702b425 100644 --- a/lib/core/routing/route_constants.dart +++ b/lib/core/routing/route_constants.dart @@ -7,6 +7,7 @@ class RouteConstants { static const String scanQRCodeScreen = 'scan-qr-code-screen'; static const String verifyScreen = '/verify-screen'; static const String verifyEmailScreen = '/verify-email-screen'; + static const String registerViaSocialScreen = '/register-via-social-screen'; // Main navigation routes static const String myProfileScreen = '/my-profile-screen'; diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart new file mode 100644 index 00000000..24c4fc46 --- /dev/null +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -0,0 +1,100 @@ +import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:flutter/material.dart'; +import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; + +class RegisterViaSocialScreen extends StatefulWidget { + const RegisterViaSocialScreen({super.key}); + + @override + State createState() => _RegisterViaSocialScreenState(); +} + +class _RegisterViaSocialScreenState extends State { + final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')}); + final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(toolbarHeight: 0), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + const Center( + child: Text( + 'Регистрация', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1, color: Color(0xFF323F4B)), + ), + ), + const SizedBox(height: 24), + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Имя'), + const SizedBox(height: 4), + CustomTextFormField(controller: TextEditingController(), hintText: 'Введите имя', onChanged: (newValue) {}), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Фамилия'), + const SizedBox(height: 4), + CustomTextFormField(controller: TextEditingController(), hintText: 'Введите фамилию', onChanged: (newValue) {}), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + const Text('Почта'), + const SizedBox(height: 4), + CustomTextFormField( + controller: TextEditingController(), + textInputType: TextInputType.emailAddress, + keyboardType: TextInputType.emailAddress, + hintText: 'Введите email', + autofocus: true, + textInputAction: TextInputAction.next, + onChanged: (newValue) {}, + ), + const SizedBox(height: 12), + const Text('Номер телефона'), + const SizedBox(height: 4), + CustomTextFormField( + textInputAction: TextInputAction.done, + hintText: '+7 (999) 999 99 99', + controller: TextEditingController(), + keyboardType: TextInputType.number, + inputFormatters: [maskFormatter], + onChanged: (newValue) {}, + ), + const SizedBox(height: 12), + const Text('Дата рождения'), + const SizedBox(height: 4), + CustomTextFormField( + textInputAction: TextInputAction.done, + hintText: '01.01.2000', + controller: TextEditingController(), + keyboardType: TextInputType.number, + inputFormatters: [maskFormatterForBirthday], + onChanged: (newValue) {}, + ), + ], + ), + ), + ); + } +} From b0d2d86a18053ce5d876d5bb7fa475b2642af5b1 Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 7 Oct 2025 18:03:36 +0300 Subject: [PATCH 31/49] feat(tests): added 2 e2e tests --- .../auth_screen/auth_login_test.dart | 38 +++++++++++++++++++ .../auth_screen/auth_logo_button_test.dart | 32 ++++++++++++++++ .../common_cases/skip_welcome_screen.dart | 8 ++++ ios/Runner/AppDelegate.swift | 19 +++++++++- ios/Runner/Info.plist | 19 ++++++++++ lib/app_keys.dart | 13 +++++++ lib/core/app_initializer.dart | 16 +++++++- .../auth/presentation/pages/auth_screen.dart | 28 ++++++++++---- .../presentation/pages/welcome_screen.dart | 2 + lib/generated/l10n.dart | 7 ++-- pubspec.yaml | 2 + 11 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 integration_test/auth_screen/auth_login_test.dart create mode 100644 integration_test/auth_screen/auth_logo_button_test.dart create mode 100644 integration_test/common_cases/skip_welcome_screen.dart create mode 100644 lib/app_keys.dart diff --git a/integration_test/auth_screen/auth_login_test.dart b/integration_test/auth_screen/auth_login_test.dart new file mode 100644 index 00000000..711e0aa3 --- /dev/null +++ b/integration_test/auth_screen/auth_login_test.dart @@ -0,0 +1,38 @@ +import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:eatvio_app/main.dart' as app; + +import '../common_cases/skip_welcome_screen.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + await AppInitializer.initialize(buildType: AppBuildType.tests); + }); + + testWidgets('Signs in successfully with valid credentials', (WidgetTester tester) async { + await tester.pumpWidget(const app.MyApp()); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await skipWelcomeScreen(tester); + final emailFieldFinder = find.byKey(AppKeys.emailTextFieldAuthScreen); + final passwordFieldFinder = find.byKey(AppKeys.passwordTextFieldAuthScreen); + final signInButtonFinder = find.byKey(AppKeys.signInButtonAuthScreen); + + expect(emailFieldFinder, findsOneWidget, reason: 'Email text field should be found.'); + expect(passwordFieldFinder, findsOneWidget, reason: 'Password text field should be found.'); + expect(signInButtonFinder, findsOneWidget, reason: 'Sign In button should be found.'); + + await tester.enterText(emailFieldFinder, 'reshetnik.n+user3@gmail.com'); + await tester.enterText(passwordFieldFinder, '1234567890'); + await tester.pumpAndSettle(); + + await tester.tap(signInButtonFinder); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + expect(signInButtonFinder, findsNothing, reason: 'Sign-in button should not be visible after successful login and navigation.'); + }); +} diff --git a/integration_test/auth_screen/auth_logo_button_test.dart b/integration_test/auth_screen/auth_logo_button_test.dart new file mode 100644 index 00000000..c427f327 --- /dev/null +++ b/integration_test/auth_screen/auth_logo_button_test.dart @@ -0,0 +1,32 @@ +import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:eatvio_app/main.dart' as app; + +import '../common_cases/skip_welcome_screen.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + await AppInitializer.initialize(buildType: AppBuildType.tests); + }); + + testWidgets('tapping logo 7 times opens dev tools', (WidgetTester tester) async { + await tester.pumpWidget(const app.MyApp()); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await skipWelcomeScreen(tester); + final logoButtonFinder = find.byKey(AppKeys.hiddenLogoButtonAuthScreen); + expect(logoButtonFinder, findsOneWidget, reason: 'The logo GestureDetector should be found in auth screen'); + + for (int i = 0; i < 7; i++) { + await tester.tap(logoButtonFinder); + await tester.pump(const Duration(milliseconds: 50)); + } + await tester.pumpAndSettle(); + + expect(find.byType(DevToolsScreen), findsOneWidget, reason: 'DevToolsScreen should be visible after tapping the logo 7 times.'); + }); +} diff --git a/integration_test/common_cases/skip_welcome_screen.dart b/integration_test/common_cases/skip_welcome_screen.dart new file mode 100644 index 00000000..20f8899b --- /dev/null +++ b/integration_test/common_cases/skip_welcome_screen.dart @@ -0,0 +1,8 @@ +import 'package:eatvio_app/app_keys.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future skipWelcomeScreen(WidgetTester tester) async { + final welcomeScreenButtonFinder = find.byKey(AppKeys.welcomeScreenButton); + await tester.tap(welcomeScreenButtonFinder); + await tester.pumpAndSettle(); +} \ No newline at end of file diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index b6363034..fc70b0b0 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ -import UIKit import Flutter +import UIKit +import YandexLoginSDK @main @objc class AppDelegate: FlutterAppDelegate { @@ -10,4 +11,20 @@ import Flutter GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + @available(iOS 8.0, *) + func application( + _ application: UIApplication, continue userActivity: NSUserActivity, + restorationHandler: @escaping ([Any]?) -> Void + ) -> Bool { + YXLSdk.shared.processUserActivity(userActivity) + return true + } + + override public func application( + _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return YXLSdk.shared.handleOpen( + url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String) + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7e82c5b2..42ec3497 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -67,5 +67,24 @@ UIViewControllerBasedStatusBarAppearance + LSApplicationQueriesSchemes + + yandexauth + yandexauth2 + yandexauth4 + + YAClientId + 5399b03636104c6d920b87c1ad329254 + CFBundleURLTypes + + + CFBundleURLName + YandexLoginSDK + CFBundleURLSchemes + + yx5399b03636104c6d920b87c1ad329254 + + + diff --git a/lib/app_keys.dart b/lib/app_keys.dart new file mode 100644 index 00000000..3e732506 --- /dev/null +++ b/lib/app_keys.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class AppKeys { + + // Auth Screen + static const hiddenLogoButtonAuthScreen = ValueKey('hiddenLogoButtonAuthScreen'); + static const emailTextFieldAuthScreen = ValueKey('emailTextFieldAuthScreen'); + static const passwordTextFieldAuthScreen = ValueKey('passwordTextFieldAuthScreen'); + static const signInButtonAuthScreen = ValueKey('signInButtonAuthScreen'); + + // Welcome Screen + static const welcomeScreenButton = ValueKey('welcomeScreenButton'); +} diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart index a3798a34..45e89fcf 100644 --- a/lib/core/app_initializer.dart +++ b/lib/core/app_initializer.dart @@ -1,4 +1,5 @@ import 'dart:io' as io; +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/env_config.dart'; import 'package:eatvio_app/core/locator_service.dart' as di; import 'package:eatvio_app/firebase_options.dart'; @@ -12,10 +13,19 @@ import 'package:sentry_flutter/sentry_flutter.dart'; /// Класс для инициализации приложения class AppInitializer { + /// Инициализация всех сервисов приложения - static Future initialize() async { - await _initializeSentry(); + static Future initialize({AppBuildType buildType = AppBuildType.prod}) async { await _initializeEnvironment(); + switch (buildType) { + case AppBuildType.tests: + baseUrl = EnvConfig.devBaseUrl; + buildType = AppBuildType.tests; + break; + case AppBuildType.prod: + break; + } + await _initializeSentry(); await _initializeFirebase(); await _initializeDependencyInjection(); await _initializeDebugMode(); @@ -66,3 +76,5 @@ class AppInitializer { }, appRunner: appRunner); } } + +enum AppBuildType { prod, tests } diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 6f49ea2a..ce6fee32 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer'; +import 'package:eatvio_app/app_keys.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; @@ -46,7 +47,7 @@ class _AuthScreenState extends State { late StreamSubscription keyboardSubscription; bool isFocused = false; int tapsCount = 0; - + bool _isLoading = false; @override void initState() { super.initState(); @@ -92,6 +93,7 @@ class _AuthScreenState extends State { @override Widget build(BuildContext context) { var shortestSide = MediaQuery.sizeOf(context).shortestSide; + final screenSize = MediaQuery.of(context).size; final bool useMobileLayout = shortestSide < 600; if (!useMobileLayout) isFocused = true; final s = S.of(context); @@ -102,9 +104,10 @@ class _AuthScreenState extends State { builder: (BuildContext context, AuthState state) { return Scaffold( appBar: AppBar( + toolbarHeight: screenSize.height * 0.15, centerTitle: true, - toolbarHeight: isFocused ? 64 : MediaQuery.of(context).size.width - 64, title: GestureDetector( + key: AppKeys.hiddenLogoButtonAuthScreen, onTap: () { tapsCount++; if (tapsCount == 7) { @@ -112,7 +115,7 @@ class _AuthScreenState extends State { GoRouter.of(context).pushNamed(DevToolsScreen.name); } }, - child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: isFocused ? 64 : MediaQuery.of(context).size.width - 64, fit: BoxFit.contain), + child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: screenSize.height * 0.20), ), ), body: SafeArea( @@ -122,8 +125,8 @@ class _AuthScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Social auth buttons at the top (outlined) - const Text('Авторизация через:', style: TextStyle(fontWeight: FontWeight.w600)), - const SizedBox(height: 8), + const Center(child: Text('Авторизация через')), + const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -164,6 +167,7 @@ class _AuthScreenState extends State { textInputAction: TextInputAction.next, keyboardType: TextInputType.emailAddress, textInputType: TextInputType.emailAddress, + key: AppKeys.emailTextFieldAuthScreen, hintText: 'Введите email', ), const SizedBox(height: 16), @@ -172,6 +176,7 @@ class _AuthScreenState extends State { CustomTextFormField( obscureText: hidePassword, controller: passwordController, + key: AppKeys.passwordTextFieldAuthScreen, hintText: 'Введите пароль', suffix: IconButton( onPressed: () { @@ -249,21 +254,28 @@ class _AuthScreenState extends State { height: 48, width: MediaQuery.of(context).size.width - 88, child: FloatingActionButton.extended( + key: AppKeys.signInButtonAuthScreen, heroTag: 'authButton', highlightElevation: 0, elevation: 0, focusElevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, - onPressed: state is AuthStateLoading + backgroundColor: (state is AuthStateLoading || _isLoading) ? Colors.black.withAlpha(50) : appTheme.blueA400, + onPressed: (state is AuthStateLoading || _isLoading) ? null : () async { + setState(() { + _isLoading = true; + }); final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text); if (isAuthenticated) { if (context.mounted) { - _reconnectWebSocket(context); + await _reconnectWebSocket(context); } } + setState(() { + _isLoading = false; + }); }, label: state is AuthStateLoading ? const CupertinoActivityIndicator() diff --git a/lib/features/welcome/presentation/pages/welcome_screen.dart b/lib/features/welcome/presentation/pages/welcome_screen.dart index 31b4bf0f..d0b8f867 100644 --- a/lib/features/welcome/presentation/pages/welcome_screen.dart +++ b/lib/features/welcome/presentation/pages/welcome_screen.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/app_keys.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:flutter/material.dart'; @@ -79,6 +80,7 @@ class _WelcomeScreenState extends State { ), const SizedBox(height: 24), InkWell( + key: AppKeys.welcomeScreenButton, onTap: () async { final sharedPreferences = await SharedPreferences.getInstance(); await sharedPreferences.setBool('welcomeScreenWasShowed', true); diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 0ccdeac3..1055e664 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,9 +28,10 @@ class S { static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { - final name = (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); + final name = + (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; diff --git a/pubspec.yaml b/pubspec.yaml index 98a4429f..0891053c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,8 @@ dev_dependencies: sdk: flutter flutter_lints: ^6.0.0 + integration_test: + sdk: flutter flutter: uses-material-design: true From 79de6ee6fa16e81b845ab76d1015bbac4cc9307d Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 7 Oct 2025 22:01:44 +0300 Subject: [PATCH 32/49] feat(registration): Issue-691 register_via_social_screen.dart --- .../pages/register_via_social_screen.dart | 167 ++++++++++++++++-- 1 file changed, 157 insertions(+), 10 deletions(-) diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index 24c4fc46..211b9eae 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -1,6 +1,10 @@ +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; +import 'package:url_launcher/url_launcher.dart'; class RegisterViaSocialScreen extends StatefulWidget { const RegisterViaSocialScreen({super.key}); @@ -66,7 +70,6 @@ class _RegisterViaSocialScreenState extends State { textInputType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress, hintText: 'Введите email', - autofocus: true, textInputAction: TextInputAction.next, onChanged: (newValue) {}, ), @@ -82,19 +85,163 @@ class _RegisterViaSocialScreenState extends State { onChanged: (newValue) {}, ), const SizedBox(height: 12), - const Text('Дата рождения'), - const SizedBox(height: 4), - CustomTextFormField( - textInputAction: TextInputAction.done, - hintText: '01.01.2000', - controller: TextEditingController(), - keyboardType: TextInputType.number, - inputFormatters: [maskFormatterForBirthday], - onChanged: (newValue) {}, + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Дата рождения'), + const SizedBox(height: 4), + CustomTextFormField( + textInputAction: TextInputAction.done, + hintText: '01.01.2000', + controller: TextEditingController(), + keyboardType: TextInputType.number, + inputFormatters: [maskFormatterForBirthday], + onChanged: (newValue) {}, + ), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Пол'), + const SizedBox(height: 4), + Row( + children: [ + Expanded(child: _genderButton('М', true)), + const SizedBox(width: 4), + Expanded(child: _genderButton('Ж', false)), + ], + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Вес'), + const SizedBox(height: 4), + CustomTextFormField(controller: TextEditingController(), hintText: 'Введите вес', onChanged: (newValue) {}), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Рост'), + const SizedBox(height: 4), + CustomTextFormField(controller: TextEditingController(), hintText: 'Введите рост', onChanged: (newValue) {}), + ], + ), + ), + ], + ), + const SizedBox(height: 28), + const Center( + child: Text( + 'Проверьте правильность заполненных\nданных. Пол, рост и вес требуются для\nрасчета вашей дневной нормы калорий', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 28), + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () {}, + child: Ink( + width: MediaQuery.of(context).size.width - 32, + height: 52, + decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)), + child: const Center( + child: Text( + 'Зарегистрироваться', + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18, color: Colors.white), + ), + ), + ), + ), + const SizedBox(height: 16), + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + text: 'Нажимая "Зарегистрироваться", Вы соглашаетесь\n', + style: const TextStyle(fontSize: 16, color: Colors.black), + children: [ + const TextSpan( + text: 'c ', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + if (!hidePlans) ...[ + TextSpan( + text: ' Публичной офертой', + style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), + ), + const TextSpan( + text: ' и ', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + ], + TextSpan( + text: 'Политикой конфиденциальности и защитой персональных данных', + style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + ), + ], + ), + ), ), ], ), ), ); } + + Widget _genderButton(String gender, bool isActive) { + return InkWell( + onTap: () {}, + borderRadius: BorderRadius.circular(20), + child: Ink( + height: 52, + decoration: BoxDecoration( + color: const Color(0xFFF6F6F6), + borderRadius: BorderRadius.circular(20), + border: isActive ? Border.all( + width: 2, + color: appTheme.blueA400, + ) : null, + ), + child: Center( + child: Text( + gender, + style: TextStyle( + color: isActive ? appTheme.blueA400 : const Color(0xFF323F4B), + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + } } From 87c0d4ec2ae01b3ecd638c6a3c15d3891cc11c4c Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 8 Oct 2025 14:04:25 +0300 Subject: [PATCH 33/49] feat(analyzer): fixed issues --- lib/core/app_config.dart | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/lib/core/app_config.dart b/lib/core/app_config.dart index 7017e570..3fb06b39 100644 --- a/lib/core/app_config.dart +++ b/lib/core/app_config.dart @@ -6,7 +6,6 @@ import 'package:eatvio_app/features/notifications/presentation/notifications_cub import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; -import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -17,25 +16,12 @@ class AppConfig { /// Список глобальных BlocProvider'ов static List get globalBlocProviders => [ BlocProvider(create: (context) => sl()), - BlocProvider( - create: (context) => sl()..getNotifications(), - ), - BlocProvider( - create: (context) => sl()..loadFeedBlogPostList(), - ), - BlocProvider( - create: (context) => sl()..getMyProfile(), - ), - BlocProvider( - create: (context) => sl()..getChatRoomsWithLoading(), - ), - BlocProvider( - create: (context) => sl(), - ), - BlocProvider( - lazy: false, - create: (context) => sl(), - ), + BlocProvider(create: (context) => sl()..getNotifications()), + BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), + BlocProvider(create: (context) => sl()..getMyProfile()), + BlocProvider(create: (context) => sl()..getChatRoomsWithLoading()), + BlocProvider(create: (context) => sl()), + BlocProvider(lazy: false, create: (context) => sl()), ]; /// Делегаты локализации @@ -47,8 +33,5 @@ class AppConfig { ]; /// Поддерживаемые локали - static const List supportedLocales = [ - Locale.fromSubtags(languageCode: 'ru'), - ]; - + static const List supportedLocales = [Locale.fromSubtags(languageCode: 'ru')]; } From 9ac576221ab5b0e8699a171191ae3b5e732be85e Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 8 Oct 2025 17:23:51 +0300 Subject: [PATCH 34/49] feat(tests): changed base url handling logic, added build type checking for push permission --- .../auth_screen/auth_login_test.dart | 7 +- .../auth_screen/auth_logo_button_test.dart | 3 +- lib/core/app_config.dart | 24 +++++- lib/core/app_initializer.dart | 29 +++----- lib/core/const.dart | 3 - lib/core/di/app_config_registration.dart | 12 +++ lib/core/di/external_registrations.dart | 6 +- lib/core/firebase_api.dart | 6 +- .../phone_verify_error_interceptor.dart | 0 lib/core/locator_service.dart | 17 +++-- lib/core/routing/other_routes.dart | 4 +- lib/core/websockets/ws_manager.dart | 7 +- lib/core/widgets/custom_popup.dart | 10 +-- .../auth/presentation/pages/auth_screen.dart | 7 +- .../pages/scan_qr_code_screen.dart | 31 ++------ .../presentation/pages/blog_post_screen.dart | 8 +- .../pages/blog_post_create_screen.dart | 4 +- .../repositories/blog_post_repository.dart | 5 +- .../widgets/bottom_navigation_bar_widget.dart | 5 +- .../presentation/pages/photo_view_screen.dart | 5 +- .../chat/presentation/widgets/order_card.dart | 5 +- .../presentation/pages/last_step_screen.dart | 4 +- .../presentation/cubit/create_diet_cubit.dart | 4 +- .../create_recipe_cubit.dart | 7 +- .../presentation/dev_tools_screen.dart | 20 ++--- .../widgets/photo_gallery_widget.dart | 7 +- .../presentation/pages/about_diet_screen.dart | 4 +- .../presentation/widgets/author_card.dart | 5 +- .../presentation/widgets/more_options.dart | 7 +- .../widgets/blog_post_widget.dart | 15 ++-- .../presentation/widgets/comments_widget.dart | 6 +- .../widgets/consultant_widget.dart | 74 +++++-------------- .../presentation/widgets/diets_widget.dart | 7 +- .../presentation/widgets/plan_widget.dart | 9 ++- .../presentation/widgets/recipes_widget.dart | 7 +- .../presentation/pages/glossary_screen.dart | 5 +- .../menu/presentation/pages/menu_screen.dart | 3 +- .../widgets/blocked_user_widget.dart | 5 +- .../presentation/widgets/my_diet_widget.dart | 7 +- .../presentation/widgets/my_plan_widget.dart | 7 +- .../presentation/widgets/plan_info.dart | 5 +- .../presentation/pages/about_plan_screen.dart | 4 +- .../presentation/pages/profile_screen.dart | 11 +-- .../widgets/profile_blog_post_widget.dart | 13 ++-- .../presentation/widgets/sub_widget.dart | 5 +- .../pages/about_recipe_screen.dart | 4 +- .../widgets/my_recipe_widget.dart | 6 +- .../presentation/widgets/steps_widget.dart | 5 +- .../presentation/pages/verify_screen.dart | 5 +- .../pages/webview_registration_screen.dart | 4 +- .../presentation/pages/support_screen.dart | 8 +- lib/main.dart | 11 +-- 52 files changed, 245 insertions(+), 237 deletions(-) create mode 100644 lib/core/di/app_config_registration.dart rename lib/core/{dio => interceptors}/phone_verify_error_interceptor.dart (100%) diff --git a/integration_test/auth_screen/auth_login_test.dart b/integration_test/auth_screen/auth_login_test.dart index 711e0aa3..ce538770 100644 --- a/integration_test/auth_screen/auth_login_test.dart +++ b/integration_test/auth_screen/auth_login_test.dart @@ -1,9 +1,8 @@ import 'package:eatvio_app/app_keys.dart'; import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/main.dart' as app; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:eatvio_app/main.dart' as app; - import '../common_cases/skip_welcome_screen.dart'; void main() { @@ -13,7 +12,7 @@ void main() { await AppInitializer.initialize(buildType: AppBuildType.tests); }); - testWidgets('Signs in successfully with valid credentials', (WidgetTester tester) async { + testWidgets('Signs in successfully with valid credentials', (WidgetTester tester) async { await tester.pumpWidget(const app.MyApp()); await tester.pumpAndSettle(const Duration(seconds: 3)); await skipWelcomeScreen(tester); @@ -25,7 +24,7 @@ void main() { expect(passwordFieldFinder, findsOneWidget, reason: 'Password text field should be found.'); expect(signInButtonFinder, findsOneWidget, reason: 'Sign In button should be found.'); - await tester.enterText(emailFieldFinder, 'reshetnik.n+user3@gmail.com'); + await tester.enterText(emailFieldFinder, 'reshetnick.mike+user4@gmail.com'); await tester.enterText(passwordFieldFinder, '1234567890'); await tester.pumpAndSettle(); diff --git a/integration_test/auth_screen/auth_logo_button_test.dart b/integration_test/auth_screen/auth_logo_button_test.dart index c427f327..c7ac598d 100644 --- a/integration_test/auth_screen/auth_logo_button_test.dart +++ b/integration_test/auth_screen/auth_logo_button_test.dart @@ -4,7 +4,6 @@ import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:eatvio_app/main.dart' as app; - import '../common_cases/skip_welcome_screen.dart'; void main() { @@ -15,7 +14,7 @@ void main() { }); testWidgets('tapping logo 7 times opens dev tools', (WidgetTester tester) async { - await tester.pumpWidget(const app.MyApp()); + await tester.pumpWidget(const app.MyApp()); await tester.pumpAndSettle(const Duration(seconds: 3)); await skipWelcomeScreen(tester); final logoButtonFinder = find.byKey(AppKeys.hiddenLogoButtonAuthScreen); diff --git a/lib/core/app_config.dart b/lib/core/app_config.dart index 3fb06b39..03513645 100644 --- a/lib/core/app_config.dart +++ b/lib/core/app_config.dart @@ -1,3 +1,5 @@ +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/core/env_config.dart'; import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; @@ -13,8 +15,24 @@ import 'package:flutter_localizations/flutter_localizations.dart'; /// Конфигурация приложения class AppConfig { + AppConfig({required this.buildType}){ + _baseUrl = switch (buildType) { + AppBuildType.prod => EnvConfig.baseUrl, + AppBuildType.tests => EnvConfig.devBaseUrl, + AppBuildType.dev => EnvConfig.devBaseUrl, + }; + } + + final AppBuildType buildType; + late final String _baseUrl; + String get baseUrl => _baseUrl; + + void setBaseUrl(String baseUrls) { + _baseUrl = baseUrls; + } + /// Список глобальных BlocProvider'ов - static List get globalBlocProviders => [ + List get globalBlocProviders => [ BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()..getNotifications()), BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), @@ -25,7 +43,7 @@ class AppConfig { ]; /// Делегаты локализации - static const List localizationDelegates = [ + List get localizationDelegates => [ S.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, @@ -33,5 +51,5 @@ class AppConfig { ]; /// Поддерживаемые локали - static const List supportedLocales = [Locale.fromSubtags(languageCode: 'ru')]; + List get supportedLocales => [const Locale.fromSubtags(languageCode: 'ru')]; } diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart index 45e89fcf..57ccbfb0 100644 --- a/lib/core/app_initializer.dart +++ b/lib/core/app_initializer.dart @@ -1,10 +1,12 @@ import 'dart:io' as io; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/env_config.dart'; import 'package:eatvio_app/core/locator_service.dart' as di; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/firebase_options.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -13,21 +15,12 @@ import 'package:sentry_flutter/sentry_flutter.dart'; /// Класс для инициализации приложения class AppInitializer { - /// Инициализация всех сервисов приложения - static Future initialize({AppBuildType buildType = AppBuildType.prod}) async { + static Future initialize({required AppBuildType buildType}) async { await _initializeEnvironment(); - switch (buildType) { - case AppBuildType.tests: - baseUrl = EnvConfig.devBaseUrl; - buildType = AppBuildType.tests; - break; - case AppBuildType.prod: - break; - } - await _initializeSentry(); + await _initializeDependencyInjection(buildType: buildType); + await _initializeSentry(); await _initializeFirebase(); - await _initializeDependencyInjection(); await _initializeDebugMode(); await _initializeAnalytics(); } @@ -46,12 +39,14 @@ class AppInitializer { /// Инициализация Firebase static Future _initializeFirebase() async { await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - await FirebaseMessaging.instance.requestPermission(); + if (sl().buildType == AppBuildType.prod) { + await FirebaseMessaging.instance.requestPermission(); + } } /// Инициализация системы внедрения зависимостей - static Future _initializeDependencyInjection() async { - await di.init(); + static Future _initializeDependencyInjection({required AppBuildType buildType}) async { + await di.init(buildType: buildType); } /// Настройка режима отладки @@ -77,4 +72,4 @@ class AppInitializer { } } -enum AppBuildType { prod, tests } +enum AppBuildType { prod, tests, dev } diff --git a/lib/core/const.dart b/lib/core/const.dart index a5f06149..5aab4fd4 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,6 +1,3 @@ -import 'package:eatvio_app/core/env_config.dart'; - -String baseUrl = EnvConfig.devBaseUrl; bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: ''); diff --git a/lib/core/di/app_config_registration.dart b/lib/core/di/app_config_registration.dart new file mode 100644 index 00000000..de003d20 --- /dev/null +++ b/lib/core/di/app_config_registration.dart @@ -0,0 +1,12 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:get_it/get_it.dart'; + +class AppConfigRegistration { + static final sl = GetIt.instance; + + /// Регистрация AppConfig + static void registerAppConfig({required AppBuildType buildType}) { + sl.registerLazySingleton(() => AppConfig(buildType: buildType)); + } +} diff --git a/lib/core/di/external_registrations.dart b/lib/core/di/external_registrations.dart index 38b3caa0..231eeef3 100644 --- a/lib/core/di/external_registrations.dart +++ b/lib/core/di/external_registrations.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; import 'package:get_it/get_it.dart'; @@ -18,7 +18,7 @@ class ExternalRegistrations { sl.registerLazySingleton( () => Dio( BaseOptions( - baseUrl: baseUrl, + baseUrl: sl().baseUrl, headers: { 'accept': 'application/json', 'content-type': 'application/json', diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index 2d89bd43..7f4baad7 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -1,5 +1,7 @@ import 'dart:developer'; import 'dart:io'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:flutter/services.dart'; @@ -19,7 +21,9 @@ class FirebaseApi { if (await _isHuaweiDevice()) { token = await _getHuaweiPushTokenNative(); } else { - await _firebaseMessaging.requestPermission(); + if ((sl().buildType == AppBuildType.prod)) { + await _firebaseMessaging.requestPermission(); + } token = await _firebaseMessaging.getToken(); } return token; diff --git a/lib/core/dio/phone_verify_error_interceptor.dart b/lib/core/interceptors/phone_verify_error_interceptor.dart similarity index 100% rename from lib/core/dio/phone_verify_error_interceptor.dart rename to lib/core/interceptors/phone_verify_error_interceptor.dart diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 8299fea2..d3ed9b95 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -1,3 +1,5 @@ +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/core/di/app_config_registration.dart'; import 'package:eatvio_app/core/di/cubit_registrations.dart'; import 'package:eatvio_app/core/di/external_registrations.dart'; import 'package:eatvio_app/core/di/repository_registrations.dart'; @@ -9,19 +11,22 @@ import 'package:get_it/get_it.dart'; final sl = GetIt.instance; /// Инициализация системы внедрения зависимостей -Future init() async { +Future init({required AppBuildType buildType}) async { + // Регистрация AppConfig + AppConfigRegistration.registerAppConfig(buildType: buildType); + // Регистрация внешних зависимостей (Dio, etc.) ExternalRegistrations.registerAll(); - + // Регистрация сервисов ServiceRegistrations.registerAll(); - + // Регистрация репозиториев RepositoryRegistrations.registerAll(); - + // Регистрация Cubit'ов CubitRegistrations.registerAll(); - + // Регистрация WebSocket менеджера sl.registerLazySingleton(() => WsManager(wsService: sl())); -} \ No newline at end of file +} diff --git a/lib/core/routing/other_routes.dart b/lib/core/routing/other_routes.dart index 43cf885f..7ae20a0f 100644 --- a/lib/core/routing/other_routes.dart +++ b/lib/core/routing/other_routes.dart @@ -1,5 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/const.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/routing/route_constants.dart'; @@ -76,7 +76,7 @@ class OtherRoutes { builder: (context, state) => const UpdateScreen(), redirect: (context, state) async { try { - final versions = (await sl().get('$baseUrl/api/app_versions')).data; + final versions = (await sl().get('${sl().baseUrl}/api/app_versions')).data; PackageInfo packageInfo = await PackageInfo.fromPlatform(); String version = packageInfo.version; Version currentVersion = Version.parse(version); diff --git a/lib/core/websockets/ws_manager.dart b/lib/core/websockets/ws_manager.dart index 528f2ae9..24430f6c 100644 --- a/lib/core/websockets/ws_manager.dart +++ b/lib/core/websockets/ws_manager.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart'; @@ -111,7 +112,7 @@ class WsManager { _privateNotificationChannel = client?.privateChannel( '$newNotificationsChannelName.$userId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( - authorizationEndpoint: Uri.parse('$baseUrl/broadcasting/auth'), + authorizationEndpoint: Uri.parse('${sl().baseUrl}/broadcasting/auth'), headers: { 'Authorization': 'Bearer $token', }, @@ -124,7 +125,7 @@ class WsManager { _privateMessagesChannel = client?.privateChannel( '$newMessagesChannelName.$userId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( - authorizationEndpoint: Uri.parse('$baseUrl/broadcasting/auth'), + authorizationEndpoint: Uri.parse('${sl().baseUrl}/broadcasting/auth'), headers: { 'Authorization': 'Bearer $token', }, @@ -137,7 +138,7 @@ class WsManager { _privateDirectChatChannel = client?.privateChannel( '$directChatChannelName.$chatId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( - authorizationEndpoint: Uri.parse('$baseUrl/broadcasting/auth'), + authorizationEndpoint: Uri.parse('${sl().baseUrl}/broadcasting/auth'), headers: { 'Authorization': 'Bearer $token', }, diff --git a/lib/core/widgets/custom_popup.dart b/lib/core/widgets/custom_popup.dart index b5934f4d..dbe0090b 100644 --- a/lib/core/widgets/custom_popup.dart +++ b/lib/core/widgets/custom_popup.dart @@ -1,4 +1,4 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -86,7 +86,7 @@ class CustomPopup extends StatelessWidget { if (isAuthor) { addItem('assets/icons/copy2.svg', 'Скопировать ссылку', () { - Clipboard.setData(ClipboardData(text: '$baseUrl/ru/$path')); + Clipboard.setData(ClipboardData(text: '${sl().baseUrl}/ru/$path')); showInfoToast(s.link_was_copied); }); addItem('assets/icons/share2.svg', 'Поделиться', () { @@ -94,7 +94,7 @@ class CustomPopup extends StatelessWidget { SharePlus.instance.share( ShareParams( uri: Uri.parse( - '$baseUrl/ru/$path', + '${sl().baseUrl}/ru/$path', ), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), @@ -140,7 +140,7 @@ class CustomPopup extends StatelessWidget { }); } else { addItem('assets/icons/copy2.svg', 'Скопировать ссылку', () { - Clipboard.setData(ClipboardData(text: '$baseUrl/ru/$path')); + Clipboard.setData(ClipboardData(text: '${sl().baseUrl}/ru/$path')); showInfoToast(s.link_was_copied); }); addItem('assets/icons/share2.svg', 'Поделиться', () { @@ -148,7 +148,7 @@ class CustomPopup extends StatelessWidget { SharePlus.instance.share( ShareParams( uri: Uri.parse( - '$baseUrl/ru/$path', + '${sl().baseUrl}/ru/$path', ), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index ce6fee32..102f31cb 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; @@ -229,7 +230,7 @@ class _AuthScreenState extends State { decorationColor: appTheme.blueA400, decoration: TextDecoration.underline, ), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl().baseUrl}/oferta_bs.pdf')), ), const TextSpan(text: ' и '), ], @@ -241,7 +242,7 @@ class _AuthScreenState extends State { decorationColor: appTheme.blueA400, decoration: TextDecoration.underline, ), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl().baseUrl}/docs/privacy.pdf')), ), ], ), @@ -308,7 +309,7 @@ class _AuthScreenState extends State { children: [ const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)), Text( - '1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"', + '1. Войдите на сайте ${sl().baseUrl} в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"', ), const SizedBox(height: 12), Row( diff --git a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart index 83296f0e..723f95e9 100644 --- a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart +++ b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; @@ -40,17 +40,11 @@ class _ScanQRCodeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Сканирование QR-кода'), - centerTitle: true, - ), + appBar: AppBar(title: const Text('Сканирование QR-кода'), centerTitle: true), body: Column( children: [ Expanded( - child: QRView( - key: qrKey, - onQRViewCreated: _onQRViewCreated, - ), + child: QRView(key: qrKey, onQRViewCreated: _onQRViewCreated), ), ], ), @@ -68,17 +62,9 @@ class _ScanQRCodeScreenState extends State { final scannedUrl = scanData.code; final uri = Uri.parse(scannedUrl ?? ''); final scannedBaseUrl = (uri.hasAuthority) ? '${uri.scheme}://${uri.host}' : ''; - baseUrl = scannedBaseUrl; - sl().options.baseUrl = baseUrl; - final response = await Dio().getUri( - uri, - options: Options( - headers: { - 'accept': 'application/json', - 'content-type': 'application/json', - }, - ), - ); + sl().setBaseUrl(scannedBaseUrl); + sl().options.baseUrl = scannedBaseUrl; + final response = await Dio().getUri(uri, options: Options(headers: {'accept': 'application/json', 'content-type': 'application/json'})); final token = response.data['token']; await TokenStorage.saveTokenInStorage(token); try { @@ -97,10 +83,7 @@ class _ScanQRCodeScreenState extends State { } catch (e) { rethrow; } - AppRouter.navigatorKey.currentContext?.go( - '/feed-screen', - extra: {'needToInitializeWebSocket': false}, - ); + AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); showSuccessToast('Добро пожаловать!'); } catch (e) { await controller.resumeCamera(); diff --git a/lib/features/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart index 61b64f9e..76562bcd 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -1,6 +1,6 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; @@ -207,7 +207,7 @@ class _BlogPostScreenState extends State { children: [ CircleAvatar( radius: 16, - foregroundImage: NetworkImage(baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')), + foregroundImage: NetworkImage(sl().baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), const SizedBox(width: 8), @@ -239,7 +239,7 @@ class _BlogPostScreenState extends State { ), const SizedBox(height: 8), if (blogPostState.blogPostModel?.image.isNotEmpty ?? false) - CachedNetworkImage(imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}'), + CachedNetworkImage(imageUrl: '${sl().baseUrl}/storage/${blogPostState.blogPostModel?.image ?? ''}'), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( @@ -267,7 +267,7 @@ class _BlogPostScreenState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/blog_post/${widget.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/blog_post/${widget.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart index 6be43b3a..40ad3dfa 100644 --- a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart +++ b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/permission_handler/permission_handler.dart'; import 'package:eatvio_app/core/theme/app_decoration.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; @@ -304,7 +304,7 @@ class _BlogPostCreateScreenState extends State { child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Image( - image: NetworkImage('$baseUrl/storage/${widget.feedBlogPostModel!.image}'), + image: NetworkImage('${sl().baseUrl}/storage/${widget.feedBlogPostModel!.image}'), ), ), ), diff --git a/lib/features/blog_post_create/repositories/blog_post_repository.dart b/lib/features/blog_post_create/repositories/blog_post_repository.dart index aeea8474..48f4f0c1 100644 --- a/lib/features/blog_post_create/repositories/blog_post_repository.dart +++ b/lib/features/blog_post_create/repositories/blog_post_repository.dart @@ -1,7 +1,8 @@ import 'dart:developer'; import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:http/http.dart' as http; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; @@ -36,7 +37,7 @@ class BlogPostRepository { Future downloadImage(String imageUrl) async { try { // Загрузка изображения - final response = await http.get(Uri.parse('$baseUrl/storage/$imageUrl')); + final response = await http.get(Uri.parse('${sl().baseUrl}/storage/$imageUrl')); if (response.statusCode == 200) { // Получение временной директории diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index a67fbbaf..b1e846ca 100644 --- a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart @@ -1,5 +1,6 @@ import 'package:badges/badges.dart' as badges; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; @@ -102,7 +103,7 @@ class _BottomNavigationBarWidgetState extends State { label: '', icon: CircleAvatar( foregroundImage: NetworkImage( - widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : baseUrl + widget.profileModel.profilePhotoUrl, + widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : sl().baseUrl + widget.profileModel.profilePhotoUrl, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: 16, diff --git a/lib/features/chat/presentation/pages/photo_view_screen.dart b/lib/features/chat/presentation/pages/photo_view_screen.dart index 1c17d40e..3cbdc744 100644 --- a/lib/features/chat/presentation/pages/photo_view_screen.dart +++ b/lib/features/chat/presentation/pages/photo_view_screen.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/app_decoration.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -88,7 +89,7 @@ class _PhotoViewScreenState extends State { color: Colors.white, ), title: Text( - widget.imageUrls[index].replaceAll('$baseUrl/storage/chat_files/', ''), + widget.imageUrls[index].replaceAll('${sl().baseUrl}/storage/chat_files/', ''), style: const TextStyle( color: Colors.white, fontSize: 12, diff --git a/lib/features/chat/presentation/widgets/order_card.dart b/lib/features/chat/presentation/widgets/order_card.dart index 31d99aad..2b7b50b2 100644 --- a/lib/features/chat/presentation/widgets/order_card.dart +++ b/lib/features/chat/presentation/widgets/order_card.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.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'; @@ -47,7 +48,7 @@ class _OrderCardState extends State { ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.network( - '$baseUrl/storage/${widget.plan.image}', + '${sl().baseUrl}/storage/${widget.plan.image}', width: double.infinity, fit: BoxFit.cover, ), diff --git a/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart b/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart index d44920cb..734387f1 100644 --- a/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart +++ b/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart @@ -1,4 +1,6 @@ +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; @@ -49,7 +51,7 @@ class _LastStepScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: TextButton( - onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '$baseUrl/docs/privacy.pdf' : '$baseUrl/oferta_bs.pdf')), + onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '${sl().baseUrl}/docs/privacy.pdf' : '${sl().baseUrl}/oferta_bs.pdf')), child: Text( hidePlans ? 'Политика конфиденциальности и пользовательское соглашение' : 'Договор офёрты', style: const TextStyle(height: 1, fontSize: 16, color: Color(0xFF323F4B), decoration: TextDecoration.underline), diff --git a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart index 6eb41358..757101ae 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -2,8 +2,10 @@ import 'dart:convert'; import 'dart:io'; import 'dart:developer'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; @@ -53,7 +55,7 @@ class CreateDietCubit extends Cubit { name: diet.name, duration: diet.duration, type: type, - imageUrl: '$baseUrl/storage/${diet.image}', + imageUrl: '${sl().baseUrl}/storage/${diet.image}', ), ); emit(state.copyWith(dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration))); 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 4ca081d4..4541782b 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 @@ -1,7 +1,8 @@ import 'dart:developer'; import 'dart:io'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.dart'; import 'package:eatvio_app/features/create_recipe/models/ingredient.dart' as i; import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; @@ -187,13 +188,13 @@ class CreateRecipeCubit extends Cubit { } Future recipeToEntity(Recipe recipe) async { - final file = await downloadImageAsFile('$baseUrl/storage/${recipe.image}'); + final file = await downloadImageAsFile('${sl().baseUrl}/storage/${recipe.image}'); final List steps = await Future.wait( recipe.steps!.map( (e) async => StepEntity( description: e.description, - image: e.image.isEmpty ? null : await downloadImageAsFile('$baseUrl/storage/${e.image}'), + image: e.image.isEmpty ? null : await downloadImageAsFile('${sl().baseUrl}/storage/${e.image}'), ), ), ); diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart index 4922a87d..0be58bee 100644 --- a/lib/features/dev_tools/presentation/dev_tools_screen.dart +++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; @@ -84,7 +84,7 @@ class _DevToolsScreenContentState extends State { void initState() { super.initState(); setState(() { - baseUrlForTest = baseUrl; + baseUrlForTest = sl().baseUrl; }); testsStates = List.generate(testCases.length, (index) => TestState.notRun); } @@ -111,8 +111,8 @@ class _DevToolsScreenContentState extends State { onPressed: () { setState(() { baseUrlForTest = controller.text; - baseUrl = baseUrlForTest.toLowerCase(); - sl().options.baseUrl = baseUrl; + sl().setBaseUrl(baseUrlForTest.toLowerCase()); + sl().options.baseUrl = baseUrlForTest.toLowerCase(); controller.clear(); }); }, @@ -122,23 +122,23 @@ class _DevToolsScreenContentState extends State { ), const SizedBox(height: 10), CustomOutlinedButton( - text: baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url', + text: sl().baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url', onPressed: () { setState(() { - if (baseUrl == EnvConfig.baseUrl) { + if (sl().baseUrl == EnvConfig.baseUrl) { baseUrlForTest = EnvConfig.devBaseUrl; } else { baseUrlForTest = EnvConfig.baseUrl; } - baseUrl = baseUrlForTest.toLowerCase(); - sl().options.baseUrl = baseUrl; + sl().setBaseUrl(baseUrlForTest.toLowerCase()); + sl().options.baseUrl = baseUrlForTest.toLowerCase(); controller.clear(); }); }, buttonStyle: const ButtonStyle(), ), - SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10), - if (baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase()) + SizedBox(height: baseUrlForTest != sl().baseUrl ? 40 : 10), + if (sl().baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase()) ListView.builder( shrinkWrap: true, itemCount: testCases.length, diff --git a/lib/features/diary/presentation/widgets/photo_gallery_widget.dart b/lib/features/diary/presentation/widgets/photo_gallery_widget.dart index 352b6519..87643817 100644 --- a/lib/features/diary/presentation/widgets/photo_gallery_widget.dart +++ b/lib/features/diary/presentation/widgets/photo_gallery_widget.dart @@ -1,8 +1,9 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/permission_handler/permission_handler.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; @@ -139,7 +140,7 @@ class _PhotoGalleryWidgetState extends State { ClipRRect( borderRadius: BorderRadius.circular(12), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex].path}', + imageUrl: '${sl().baseUrl}/storage/${state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex].path}', ), ), if (!state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented) @@ -211,7 +212,7 @@ class _PhotoGalleryWidgetState extends State { height: 52, width: 52, child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${state.diary.meals[mealIndex].photos[i].path}', + imageUrl: '${sl().baseUrl}/storage/${state.diary.meals[mealIndex].photos[i].path}', fit: BoxFit.fitWidth, ), ), diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index e4dd4a75..e06989f1 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; @@ -353,7 +353,7 @@ class _AboutDietScreenState extends State { Stack( children: [ CachedNetworkImage( - imageUrl: '$baseUrl/storage/${dietState.diet.image}', + imageUrl: '${sl().baseUrl}/storage/${dietState.diet.image}', height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill, diff --git a/lib/features/diets/presentation/widgets/author_card.dart b/lib/features/diets/presentation/widgets/author_card.dart index 6fff0b93..548b9263 100644 --- a/lib/features/diets/presentation/widgets/author_card.dart +++ b/lib/features/diets/presentation/widgets/author_card.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.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'; @@ -37,7 +38,7 @@ class AuthorCard extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, image: DecorationImage( - image: NetworkImage(baseUrl + author.authorProfileIcon), + image: NetworkImage(sl().baseUrl + author.authorProfileIcon), fit: BoxFit.cover, ), ), diff --git a/lib/features/diets/presentation/widgets/more_options.dart b/lib/features/diets/presentation/widgets/more_options.dart index 4c28e5c9..480b9915 100644 --- a/lib/features/diets/presentation/widgets/more_options.dart +++ b/lib/features/diets/presentation/widgets/more_options.dart @@ -1,5 +1,6 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; @@ -35,7 +36,7 @@ class MoreOptions extends StatelessWidget { onTap: disableActions == true ? null : () { - Clipboard.setData(ClipboardData(text: '$baseUrl/ru/$path')); + Clipboard.setData(ClipboardData(text: '${sl().baseUrl}/ru/$path')); showInfoToast(s.link_was_copied); }, child: Row( @@ -62,7 +63,7 @@ class MoreOptions extends StatelessWidget { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/$path',), + uri: Uri.parse('${sl().baseUrl}/ru/$path',), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index e001a038..261e421a 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; @@ -64,7 +65,7 @@ class _BlogPostWidgetState extends State { authorName: state.feedBlogPostList[widget.index].author.authorName, authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https') ? state.feedBlogPostList[widget.index].author.authorProfileIcon - : baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, + : sl().baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe, ), ); @@ -75,7 +76,7 @@ class _BlogPostWidgetState extends State { foregroundImage: NetworkImage( state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https') ? state.feedBlogPostList[widget.index].author.authorProfileIcon - : baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, + : sl().baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), @@ -157,15 +158,15 @@ class _BlogPostWidgetState extends State { onTap: () { context.pushTransparentRoute( rootNavigator: true, - PhotoViewScreen(imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'], index: 0), + PhotoViewScreen(imageUrls: ['${sl().baseUrl}/storage/${state.feedBlogPostList[widget.index].image}'], index: 0), ); }, child: Hero( - tag: '$baseUrl/storage/${state.feedBlogPostList[widget.index].image}', + tag: '${sl().baseUrl}/storage/${state.feedBlogPostList[widget.index].image}', child: CachedNetworkImage( fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, - imageUrl: '$baseUrl/storage/${state.feedBlogPostList[widget.index].image}', + imageUrl: '${sl().baseUrl}/storage/${state.feedBlogPostList[widget.index].image}', errorWidget: (context, str, object) => const SizedBox.shrink(), ), ), @@ -219,7 +220,7 @@ class _BlogPostWidgetState extends State { 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('${sl().baseUrl}/ru/blog_post/${state.feedBlogPostList[widget.index].id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/comments_widget.dart b/lib/features/feed/presentation/widgets/comments_widget.dart index 3617fe33..b313ecaa 100644 --- a/lib/features/feed/presentation/widgets/comments_widget.dart +++ b/lib/features/feed/presentation/widgets/comments_widget.dart @@ -1,5 +1,5 @@ +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/comments/presentation/comments_actions_bottom_sheet.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; @@ -99,13 +99,13 @@ class _CommentsWidgetState extends State { extra: Author( authorSlug: comment.authorSlug, authorName: comment.authorName, - authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon, + authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : sl().baseUrl + comment.authorProfileIcon, authorIsMe: isMe, ), ); }, child: CircleAvatar( - foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon), + foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : sl().baseUrl + comment.authorProfileIcon), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: radius, ), diff --git a/lib/features/feed/presentation/widgets/consultant_widget.dart b/lib/features/feed/presentation/widgets/consultant_widget.dart index 3b4a4b80..57d34b07 100644 --- a/lib/features/feed/presentation/widgets/consultant_widget.dart +++ b/lib/features/feed/presentation/widgets/consultant_widget.dart @@ -1,5 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; @@ -14,10 +14,7 @@ import 'package:go_router/go_router.dart'; class ConsultantWidget extends StatelessWidget { final FeedConsultantModel consultant; - const ConsultantWidget({ - super.key, - required this.consultant, - }); + const ConsultantWidget({super.key, required this.consultant}); @override Widget build(BuildContext context) { @@ -36,10 +33,7 @@ class ConsultantWidget extends StatelessWidget { borderRadius: BorderRadius.circular(20), child: Ink( padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFFF6F6F6), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -51,46 +45,32 @@ class ConsultantWidget extends StatelessWidget { children: [ Ink( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), child: Text( '${consultant.ratingPlace}', - style: TextStyle( - fontSize: 20, - color: appTheme.blueA400, - fontWeight: FontWeight.w600, - ), + style: TextStyle(fontSize: 20, color: appTheme.blueA400, fontWeight: FontWeight.w600), ), ), const SizedBox(width: 8), - CircleAvatar( - foregroundImage: NetworkImage(baseUrl + consultant.photo), - backgroundImage: const AssetImage('assets/images/no_avatar.png'), - ), + if (consultant.photo.contains('https')) + CircleAvatar(foregroundImage: NetworkImage(consultant.photo), backgroundImage: const AssetImage('assets/images/no_avatar.png')) + else + CircleAvatar( + foregroundImage: NetworkImage(sl().baseUrl + consultant.photo), + backgroundImage: const AssetImage('assets/images/no_avatar.png'), + ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( consultant.name, - style: const TextStyle( - height: 1, - fontWeight: FontWeight.w500, - fontSize: 15, - color: Color(0xFF323F4B), - ), + style: const TextStyle(height: 1, fontWeight: FontWeight.w500, fontSize: 15, color: Color(0xFF323F4B)), ), const SizedBox(height: 4), Text( 'Рейтинг: ${consultant.totalScores}', - style: TextStyle( - color: appTheme.blueA400, - height: 1, - fontWeight: FontWeight.w500, - fontSize: 12, - ), + style: TextStyle(color: appTheme.blueA400, height: 1, fontWeight: FontWeight.w500, fontSize: 12), ), ], ), @@ -98,21 +78,13 @@ class ConsultantWidget extends StatelessWidget { ), Ink( padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Colors.white, - ), - child: SvgPicture.asset( - 'assets/icons/go_to_arrow.svg', - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: Colors.white), + child: SvgPicture.asset('assets/icons/go_to_arrow.svg'), ), ], ), const SizedBox(height: 8), - Text( - consultant.description, - maxLines: 2, - ), + Text(consultant.description, maxLines: 2), if (consultant.badges.isNotEmpty) const SizedBox(height: 8), _badges(consultant.badges, context), ], @@ -128,16 +100,8 @@ class ConsultantWidget extends StatelessWidget { children: [ for (int i = 0; i < badges.length; i++) ...[ badges[i].image.contains('svg') - ? SvgPicture.network( - '$baseUrl/storage/${badges[i].image}', - width: 28, - height: 28, - ) - : CachedNetworkImage( - imageUrl: '$baseUrl/storage/${badges[i].image}', - width: 28, - height: 28, - ), + ? SvgPicture.network('${sl().baseUrl}/storage/${badges[i].image}', width: 28, height: 28) + : CachedNetworkImage(imageUrl: '${sl().baseUrl}/storage/${badges[i].image}', width: 28, height: 28), i < badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(), ], ], diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index d91cdd74..1f39ff72 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/info_row.dart'; @@ -56,7 +57,7 @@ class _DietsWidgetState extends State { ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.diet.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.diet.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), @@ -189,7 +190,7 @@ class _DietsWidgetState extends State { final box = context.findRenderObject() as RenderBox?; final result = await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/diet/${widget.diet.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/diet/${widget.diet.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/plan_widget.dart b/lib/features/feed/presentation/widgets/plan_widget.dart index 588d61d0..ae43d830 100644 --- a/lib/features/feed/presentation/widgets/plan_widget.dart +++ b/lib/features/feed/presentation/widgets/plan_widget.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/info_row.dart'; @@ -61,7 +62,7 @@ class _PlanWidgetState extends State { ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.plan.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.plan.image}', fit: BoxFit.cover, width: MediaQuery.of(context).size.width, height: 426, @@ -187,7 +188,7 @@ class _PlanWidgetState extends State { decoration: BoxDecoration( shape: BoxShape.circle, image: DecorationImage( - image: NetworkImage(baseUrl + widget.plan.author.authorProfileIcon), + image: NetworkImage(sl().baseUrl + widget.plan.author.authorProfileIcon), fit: BoxFit.cover, ), ), @@ -263,7 +264,7 @@ class _PlanWidgetState extends State { final box = context.findRenderObject() as RenderBox?; final result = await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/diet/${widget.plan.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/diet/${widget.plan.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart index c0dbeb39..5fb415b1 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -1,8 +1,9 @@ // ignore_for_file: dead_code import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; @@ -75,7 +76,7 @@ class _RecipesWidgetState extends State { ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.recipe.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.recipe.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), @@ -231,7 +232,7 @@ class _RecipesWidgetState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/recipe/${widget.recipe.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/glossary/presentation/pages/glossary_screen.dart b/lib/features/glossary/presentation/pages/glossary_screen.dart index 7205b3d3..22050f00 100644 --- a/lib/features/glossary/presentation/pages/glossary_screen.dart +++ b/lib/features/glossary/presentation/pages/glossary_screen.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -18,7 +19,7 @@ class _GlossaryScreenState extends State { _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..loadRequest(Uri.parse('$baseUrl/product_nutritional_value')); + ..loadRequest(Uri.parse('${sl().baseUrl}/product_nutritional_value')); } @override diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 123e5faf..7d237245 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/token_storage.dart'; @@ -117,7 +118,7 @@ class MenuScreen extends StatelessWidget { CupertinoDialogAction( onPressed: () async { try { - await sl().post('$baseUrl/api/profile/logout'); + await sl().post('${sl().baseUrl}/api/profile/logout'); } finally { await TokenStorage.removeTokenInStorage(); if (context.mounted) { diff --git a/lib/features/menu/presentation/widgets/blocked_user_widget.dart b/lib/features/menu/presentation/widgets/blocked_user_widget.dart index 494df21c..a107a4df 100644 --- a/lib/features/menu/presentation/widgets/blocked_user_widget.dart +++ b/lib/features/menu/presentation/widgets/blocked_user_widget.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; @@ -33,7 +34,7 @@ class _BlockedUserWidgetState extends State { child: Row( children: [ CircleAvatar( - foregroundImage: NetworkImage(baseUrl + widget.profile.profilePhotoUrl), + foregroundImage: NetworkImage(sl().baseUrl + widget.profile.profilePhotoUrl), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: 28, ), diff --git a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart index 965bd088..d6f41d7f 100644 --- a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart +++ b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.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/enums/product_type.dart'; @@ -56,7 +57,7 @@ class _MyDietWidgetState extends State { child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.diet.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.diet.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), @@ -153,7 +154,7 @@ class _MyDietWidgetState extends State { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/diet/${widget.diet.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/diet/${widget.diet.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart index 7ecdc653..5d057be5 100644 --- a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart +++ b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.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/enums/product_type.dart'; @@ -42,7 +43,7 @@ class _MyPlanWidgetState extends State { onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'navigateToComments': false}), child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), - child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width), + child: CachedNetworkImage(imageUrl: '${sl().baseUrl}/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width), ), ), Positioned( @@ -157,7 +158,7 @@ class _MyPlanWidgetState extends State { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/plan/${widget.plan.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/plan/${widget.plan.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/order_info/presentation/widgets/plan_info.dart b/lib/features/order_info/presentation/widgets/plan_info.dart index 6c5e387d..d8f91880 100644 --- a/lib/features/order_info/presentation/widgets/plan_info.dart +++ b/lib/features/order_info/presentation/widgets/plan_info.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.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/widgets/main_background_bouble.dart'; @@ -31,7 +32,7 @@ class PlanInfo extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(8), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/$imagePath', + imageUrl: '${sl().baseUrl}/storage/$imagePath', height: 220, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill, diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index b5905664..64a437c1 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; @@ -401,7 +401,7 @@ class _AboutPlanScreenState extends State { Stack( children: [ CachedNetworkImage( - imageUrl: '$baseUrl/storage/${state.planModel?.image}', + imageUrl: '${sl().baseUrl}/storage/${state.planModel?.image}', height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill, diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 11ae9b7a..5c25dfe5 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; @@ -655,7 +656,7 @@ class _ProfileScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)], ), - child: SvgPicture.network('$baseUrl/storage/${state.profile.badges[i].image}', width: 40, height: 40), + child: SvgPicture.network('${sl().baseUrl}/storage/${state.profile.badges[i].image}', width: 40, height: 40), ) : Tooltip( message: state.profile.badges[i].description, @@ -667,7 +668,7 @@ class _ProfileScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)], ), - child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${state.profile.badges[i].image}', width: 40, height: 40), + child: CachedNetworkImage(imageUrl: '${sl().baseUrl}/storage/${state.profile.badges[i].image}', width: 40, height: 40), ), i < state.profile.badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(), ], @@ -803,7 +804,7 @@ class _ProfileScreenState extends State { icon: 'assets/icons/eye.svg', text: 'Посмотреть', onTap: () async { - context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0)); + context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [sl().baseUrl + state.profile.profilePhotoUrl], index: 0)); }, ), if (widget.author == null || widget.author!.authorIsMe) ...[ @@ -885,7 +886,7 @@ class _ProfileScreenState extends State { child: CircleAvatar( radius: 32, foregroundImage: NetworkImage( - state.profile.profilePhotoUrl.contains('https') ? state.profile.profilePhotoUrl : baseUrl + state.profile.profilePhotoUrl, + state.profile.profilePhotoUrl.contains('https') ? state.profile.profilePhotoUrl : sl().baseUrl + state.profile.profilePhotoUrl, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), @@ -960,7 +961,7 @@ class _ProfileScreenState extends State { onTap: () { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( - ShareParams(uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size), + ShareParams(uri: Uri.parse('${sl().baseUrl}/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size), ); }, ), diff --git a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart index 0f0408d8..7c55c420 100644 --- a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart +++ b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; @@ -69,7 +70,7 @@ class _ProfileBlogPostWidgetState extends State { foregroundImage: NetworkImage( state.blogPosts[widget.index].author.authorProfileIcon.contains('https') ? state.blogPosts[widget.index].author.authorProfileIcon - : baseUrl + state.blogPosts[widget.index].author.authorProfileIcon, + : sl().baseUrl + state.blogPosts[widget.index].author.authorProfileIcon, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), @@ -157,17 +158,17 @@ class _ProfileBlogPostWidgetState extends State { context.pushTransparentRoute( rootNavigator: true, PhotoViewScreen( - imageUrls: ['$baseUrl/storage/${state.blogPosts[widget.index].image}'], + imageUrls: ['${sl().baseUrl}/storage/${state.blogPosts[widget.index].image}'], index: 0, ), ); }, child: Hero( - tag: '$baseUrl/storage/${state.blogPosts[widget.index].image}', + tag: '${sl().baseUrl}/storage/${state.blogPosts[widget.index].image}', child: CachedNetworkImage( fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, - imageUrl: '$baseUrl/storage/${state.blogPosts[widget.index].image}', + imageUrl: '${sl().baseUrl}/storage/${state.blogPosts[widget.index].image}', errorWidget: (context, str, object) => const SizedBox.shrink(), ), ), @@ -226,7 +227,7 @@ class _ProfileBlogPostWidgetState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/blog_post/${state.blogPosts[widget.index].id}'), + uri: Uri.parse('${sl().baseUrl}/ru/blog_post/${state.blogPosts[widget.index].id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/profile/presentation/widgets/sub_widget.dart b/lib/features/profile/presentation/widgets/sub_widget.dart index 27365fd2..d5d3a61a 100644 --- a/lib/features/profile/presentation/widgets/sub_widget.dart +++ b/lib/features/profile/presentation/widgets/sub_widget.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.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'; @@ -36,7 +37,7 @@ class _SubWidgetState extends State { child: Row( children: [ CircleAvatar( - foregroundImage: NetworkImage(baseUrl + widget.subModel.profilePhotoUrl), + foregroundImage: NetworkImage(sl().baseUrl + widget.subModel.profilePhotoUrl), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: 28, ), diff --git a/lib/features/recipe/presentation/pages/about_recipe_screen.dart b/lib/features/recipe/presentation/pages/about_recipe_screen.dart index f23ac880..efc19a12 100644 --- a/lib/features/recipe/presentation/pages/about_recipe_screen.dart +++ b/lib/features/recipe/presentation/pages/about_recipe_screen.dart @@ -2,7 +2,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; @@ -247,7 +247,7 @@ class _AboutRecipeScreenState extends State { Stack( children: [ CachedNetworkImage( - imageUrl: '$baseUrl/storage/${recipeState.recipe.image}', + imageUrl: '${sl().baseUrl}/storage/${recipeState.recipe.image}', height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill, diff --git a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart index 2610ca13..d791135a 100644 --- a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart +++ b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart @@ -1,5 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.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'; @@ -71,7 +71,7 @@ class _MyRecipeWidgetState extends State { child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.recipe.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.recipe.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), @@ -162,7 +162,7 @@ class _MyRecipeWidgetState extends State { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.slug}'), + uri: Uri.parse('${sl().baseUrl}/ru/recipe/${widget.recipe.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/recipe/presentation/widgets/steps_widget.dart b/lib/features/recipe/presentation/widgets/steps_widget.dart index f4281c81..dbd45602 100644 --- a/lib/features/recipe/presentation/widgets/steps_widget.dart +++ b/lib/features/recipe/presentation/widgets/steps_widget.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.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/widgets/main_background_bouble.dart'; @@ -44,7 +45,7 @@ class StepsWidget extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Image.network( - '$baseUrl/storage/${recipe.steps![index].image}', + '${sl().baseUrl}/storage/${recipe.steps![index].image}', fit: BoxFit.cover, ), ), diff --git a/lib/features/registration/presentation/pages/verify_screen.dart b/lib/features/registration/presentation/pages/verify_screen.dart index 8d5df9df..867af06f 100644 --- a/lib/features/registration/presentation/pages/verify_screen.dart +++ b/lib/features/registration/presentation/pages/verify_screen.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; @@ -89,7 +90,7 @@ class _VerifyScreenState extends State { }, ), ) - ..loadRequest(Uri.parse('$baseUrl/login_with_pass_key?id=${widget.passKey}')); + ..loadRequest(Uri.parse('${sl().baseUrl}/login_with_pass_key?id=${widget.passKey}')); } @override diff --git a/lib/features/registration/presentation/pages/webview_registration_screen.dart b/lib/features/registration/presentation/pages/webview_registration_screen.dart index 05c0ffc1..6cc777d2 100644 --- a/lib/features/registration/presentation/pages/webview_registration_screen.dart +++ b/lib/features/registration/presentation/pages/webview_registration_screen.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:developer'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; @@ -54,7 +54,7 @@ class _WebViewRegistrationScreenState extends State { }, ), ) - ..loadRequest(Uri.parse('$baseUrl/registration/step1')); + ..loadRequest(Uri.parse('${sl().baseUrl}/registration/step1')); _injectJavaScript(); } diff --git a/lib/features/support/presentation/pages/support_screen.dart b/lib/features/support/presentation/pages/support_screen.dart index 6aa44a42..a01a88af 100644 --- a/lib/features/support/presentation/pages/support_screen.dart +++ b/lib/features/support/presentation/pages/support_screen.dart @@ -1,4 +1,6 @@ +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -71,7 +73,7 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 12), if (!hidePlans) ...[ InkWell( - onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/public_offer.pdf')), + onTap: () async => await launchUrl(Uri.parse('${sl().baseUrl}/docs/public_offer.pdf')), borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), @@ -81,7 +83,7 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 8), ], InkWell( - onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + onTap: () async => await launchUrl(Uri.parse('${sl().baseUrl}/docs/privacy.pdf')), borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), @@ -90,7 +92,7 @@ class SupportScreen extends StatelessWidget { ), const SizedBox(height: 8), InkWell( - onTap: () async => await launchUrl(Uri.parse('$baseUrl/company-details')), + onTap: () async => await launchUrl(Uri.parse('${sl().baseUrl}/company-details')), borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), diff --git a/lib/main.dart b/lib/main.dart index fa5ff699..af6db638 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -8,7 +9,7 @@ import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; Future main() async { - await AppInitializer.initialize(); + await AppInitializer.initialize(buildType: AppBuildType.prod); await AppInitializer.runAppWithSentry(() => runApp(const MyApp())); } @@ -18,15 +19,15 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiBlocProvider( - providers: AppConfig.globalBlocProviders, + providers: sl().globalBlocProviders, child: Portal( child: KeyboardDismisser( child: MaterialApp.router( theme: ThemeHelper().themeData(), - routerConfig: AppRouter.appRouter, + routerConfig: AppRouter.appRouter, debugShowCheckedModeBanner: false, - localizationsDelegates: AppConfig.localizationDelegates, - supportedLocales: AppConfig.supportedLocales, + localizationsDelegates: sl().localizationDelegates, + supportedLocales: sl().supportedLocales, ), ), ), From 183fa012d2f3f68b8cb155471a9b510d2b9f42b4 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 9 Oct 2025 17:24:36 +0300 Subject: [PATCH 35/49] feat(auth): Issue-691 --- lib/core/app_router.dart | 5 + lib/core/di/cubit_registrations.dart | 3 +- lib/core/routing/auth_routes.dart | 10 +- lib/features/auth/models/oauth_model.dart | 24 + .../bloc/oauth_cubit/oauth_cubit.dart | 44 ++ .../bloc/oauth_cubit/oauth_state.dart | 14 + .../auth/presentation/pages/auth_screen.dart | 33 +- .../pages/register_via_social_screen.dart | 652 +++++++++++++----- .../auth/repositories/auth_repository.dart | 52 +- lib/features/auth/sdk/social_auth_facade.dart | 12 +- lib/features/auth/services/auth_service.dart | 6 + 11 files changed, 637 insertions(+), 218 deletions(-) create mode 100644 lib/features/auth/models/oauth_model.dart create mode 100644 lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart create mode 100644 lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index f3715464..26355055 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -4,6 +4,7 @@ import 'package:eatvio_app/core/routing/main_navigation_routes.dart'; import 'package:eatvio_app/core/routing/other_routes.dart'; import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/routing/settings_routes.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:flutter/material.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:go_router/go_router.dart'; @@ -50,4 +51,8 @@ class AppRouter { return null; } + + static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) { + appRouter.goNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider}); + } } diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index ec2de9ce..bd9014e6 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; @@ -73,7 +74,7 @@ class CubitRegistrations { sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); - + sl.registerFactory(() => OAuthCubit(authRepository: sl())); // Feed related cubits sl.registerFactory(() => FeedCubit(feedRepository: sl())); sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl())); diff --git a/lib/core/routing/auth_routes.dart b/lib/core/routing/auth_routes.dart index 67ecbca6..b94cc2c8 100644 --- a/lib/core/routing/auth_routes.dart +++ b/lib/core/routing/auth_routes.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; import 'package:eatvio_app/features/auth/presentation/pages/register_via_social_screen.dart'; @@ -33,7 +34,14 @@ class AuthRoutes { builder: (context, state) => VerifyScreen(passKey: state.extra as String), ), GoRoute(path: RouteConstants.verifyEmailScreen, name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), - GoRoute(path: RouteConstants.registerViaSocialScreen, name: 'registerViaSocialScreen', builder: (context, state) => const RegisterViaSocialScreen()), + GoRoute( + path: RouteConstants.registerViaSocialScreen, + name: 'registerViaSocialScreen', + builder: (context, state) { + final extra = state.extra as Map?; + return RegisterViaSocialScreen(authResult: extra?['authResult'] as AuthResultEntity?, provider: extra?['provider'] as String?); + }, + ), ]; } } diff --git a/lib/features/auth/models/oauth_model.dart b/lib/features/auth/models/oauth_model.dart new file mode 100644 index 00000000..aff79b5f --- /dev/null +++ b/lib/features/auth/models/oauth_model.dart @@ -0,0 +1,24 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'oauth_model.freezed.dart'; +part 'oauth_model.g.dart'; + +@freezed +abstract class OAuthModel with _$OAuthModel { + const factory OAuthModel({ + required String provider, + required String id, + @JsonKey(name: 'access_token') required String accessToken, + @JsonKey(name: 'refresh_token') required String refreshToken, + @JsonKey(name: 'first_name') required String firstName, + @JsonKey(name: 'last_name') required String lastName, + required String email, + required String phone, + @JsonKey(name: 'date_of_birth') required String dateOfBirth, + required String gender, + required int weight, + required int height, + }) = _OAuthModel; + + factory OAuthModel.fromJson(Map json) => _$OAuthModelFromJson(json); +} diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart new file mode 100644 index 00000000..8d73b195 --- /dev/null +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -0,0 +1,44 @@ +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; +import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class OAuthCubit extends Cubit { + final AuthRepository authRepository; + + OAuthCubit({required this.authRepository}) : super(const OAuthState()); + + Future getOAuthToken({ + required String provider, + required String accessToken, + required String id, + required String refreshToken, + required String firstName, + required String lastName, + required String email, + required String phone, + required String dateOfBirth, + required String gender, + required int weight, + required int height, + }) async { + try { + final token = await authRepository.getOAuthToken( + provider: provider, + accessToken: accessToken, + id: id, + refreshToken: refreshToken, + firstName: firstName, + lastName: lastName, + email: email, + phone: phone, + dateOfBirth: dateOfBirth, + gender: gender, + weight: weight, + height: height, + ); + emit(OAuthState.loaded(token: token)); + } catch (e) { + emit(OAuthState.error(errorMessage: e.toString())); + } + } +} diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart new file mode 100644 index 00000000..7da9ab7c --- /dev/null +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'oauth_state.freezed.dart'; + +@freezed +abstract class OAuthState with _$OAuthState { + const factory OAuthState() = _OAuthState; + + const factory OAuthState.loading() = OAuthStateLoading; + + const factory OAuthState.loaded({required String token}) = OAuthStateLoaded; + + const factory OAuthState.error({required String errorMessage}) = OAuthStateError; +} diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 6f49ea2a..13713922 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -391,10 +391,14 @@ class _SocialButton extends StatelessWidget { void _onVkPressed() async { try { - final accessToken = await SocialAuthFacade.signInWithVk(); - if (accessToken == null) { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context); + if (authResult == null) { return; } + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'vk'); } catch (e) { log(e.toString()); } @@ -402,10 +406,14 @@ void _onVkPressed() async { void _onOkPressed() async { try { - final accessToken = await SocialAuthFacade.signInWithOk(); - if (accessToken == null) { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile(); + if (authResult == null) { return; } + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'ok'); } catch (e) { log(e.toString()); } @@ -413,18 +421,27 @@ void _onOkPressed() async { void _onMailRuPressed() async { try { - final accessToken = await SocialAuthFacade.signInWithMailRu(); - if (accessToken == null) { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile(); + if (authResult == null) { return; } + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'mailru'); } catch (e) { log(e.toString()); } } void _onYandexPressed() async { - final accessToken = await SocialAuthFacade.signInWithYandex(); - if (accessToken == null) return; + try { + final authResult = await SocialAuthFacade.signInWithYandex(); + if (authResult == null) return; + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'yandex'); + } catch (e) { + log(e.toString()); + } } Future _reconnectAfterAuth() async { diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index 211b9eae..22fa5d18 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -1,13 +1,29 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/core/di/cubit_registrations.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; +import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; import 'package:url_launcher/url_launcher.dart'; class RegisterViaSocialScreen extends StatefulWidget { - const RegisterViaSocialScreen({super.key}); + final AuthResultEntity? authResult; + final String? provider; + + const RegisterViaSocialScreen({super.key, this.authResult, this.provider}); @override State createState() => _RegisterViaSocialScreenState(); @@ -17,201 +33,421 @@ class _RegisterViaSocialScreenState extends State { final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')}); final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}); + // Контроллеры для полей ввода + final firstNameController = TextEditingController(); + final lastNameController = TextEditingController(); + final emailController = TextEditingController(); + final phoneController = TextEditingController(); + final dateOfBirthController = TextEditingController(); + final weightController = TextEditingController(); + final heightController = TextEditingController(); + + // Состояние для выбора пола + bool isMaleSelected = true; + + @override + void initState() { + super.initState(); + _initializeFieldsFromAuthResult(); + } + + Future _reconnectWebSocket(BuildContext context) async { + try { + await sl().clear(); + await sl().initialize(); + } catch (e) { + rethrow; + } + if (context.mounted) { + try { + await sl().getMyProfile(); + await sl().getChatRoomsWithLoading(); + await sl().connectToWebSocket(); + await sl().getNotifications(); + await sl().connectToWebSocket(); + await sl().registerPushToken(); + showSuccessToast('Успешная регистрация'); + } catch (e) { + rethrow; + } + } + AppRouter.appRouter.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); + } + + void _initializeFieldsFromAuthResult() { + if (widget.authResult != null) { + final authResult = widget.authResult!; + + // Логируем полученные данные для отладки + print('AuthResult data:'); + print('firstName: ${authResult.firstName}'); + print('lastName: ${authResult.lastName}'); + print('email: ${authResult.email}'); + print('phone: ${authResult.phone}'); + print('birthDate: ${authResult.birthDate}'); + print('gender: ${authResult.gender}'); + print('provider: ${widget.provider}'); + + // Предзаполняем поля из AuthResultEntity + if (authResult.firstName != null) { + firstNameController.text = authResult.firstName!; + } + if (authResult.lastName != null) { + lastNameController.text = authResult.lastName!; + } + if (authResult.email != null) { + emailController.text = authResult.email!; + } + if (authResult.phone != null) { + phoneController.text = authResult.phone!; + } + if (authResult.birthDate != null) { + dateOfBirthController.text = _formatBirthDate(authResult.birthDate!); + } + if (authResult.gender != null) { + isMaleSelected = authResult.gender == GenderEnum.male; + } + } + } + + String _formatBirthDate(String birthDate) { + try { + // Пытаемся распарсить различные форматы даты + DateTime? date; + + // Если дата в формате ISO (YYYY-MM-DD) + if (birthDate.contains('-')) { + date = DateTime.tryParse(birthDate); + } + // Если дата в формате DD.MM.YYYY + else if (birthDate.contains('.')) { + final parts = birthDate.split('.'); + if (parts.length == 3) { + final day = int.tryParse(parts[0]); + final month = int.tryParse(parts[1]); + final year = int.tryParse(parts[2]); + if (day != null && month != null && year != null) { + date = DateTime(year, month, day); + } + } + } + + if (date != null) { + // Форматируем в DD.MM.YYYY + final day = date.day.toString().padLeft(2, '0'); + final month = date.month.toString().padLeft(2, '0'); + final year = date.year.toString(); + return '$day.$month.$year'; + } + } catch (e) { + // Если не удалось распарсить, возвращаем исходную строку + } + + return birthDate; + } + + String _getProviderName(String? provider) { + switch (provider) { + case 'vk': + return 'ВКонтакте'; + case 'yandex': + return 'Яндекс'; + case 'ok': + return 'Одноклассники'; + case 'mailru': + return 'Mail.ru'; + default: + return 'социальной сети'; + } + } + + String _getDataStatusMessage(AuthResultEntity authResult) { + final hasEmail = authResult.email != null && authResult.email!.isNotEmpty; + final hasPhone = authResult.phone != null && authResult.phone!.isNotEmpty; + final hasBirthDate = authResult.birthDate != null && authResult.birthDate!.isNotEmpty; + + if (hasEmail && hasPhone && hasBirthDate) { + return 'Все данные успешно получены и предзаполнены.'; + } else if (hasEmail && hasPhone) { + return 'Email и номер телефона получены. Дата рождения может быть недоступна из-за настроек приватности.'; + } else if (hasEmail) { + return 'Email получен. Номер телефона и дата рождения могут быть недоступны из-за настроек приватности.'; + } else if (hasPhone) { + return 'Номер телефона получен. Email и дата рождения могут быть недоступны из-за настроек приватности.'; + } else { + // Специальное сообщение для VK + if (widget.provider == 'vk') { + return 'Основные данные получены. Email запрашивается автоматически. Номер телефона получается через VK API. Если номер замаскирован, это означает, что пользователь ограничил доступ к нему в настройках приватности.'; + } else { + return 'Основные данные получены. Email, номер телефона и дата рождения могут быть недоступны из-за настроек приватности.'; + } + } + } + + @override + void dispose() { + firstNameController.dispose(); + lastNameController.dispose(); + emailController.dispose(); + phoneController.dispose(); + dateOfBirthController.dispose(); + weightController.dispose(); + heightController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(toolbarHeight: 0), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 24), - const Center( - child: Text( - 'Регистрация', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1, color: Color(0xFF323F4B)), - ), - ), - const SizedBox(height: 24), - Row( - children: [ - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 22, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Имя'), - const SizedBox(height: 4), - CustomTextFormField(controller: TextEditingController(), hintText: 'Введите имя', onChanged: (newValue) {}), - ], + return BlocProvider( + create: (context) => CubitRegistrations.sl(), + child: Scaffold( + appBar: AppBar(toolbarHeight: 0), + body: BlocBuilder( + builder: (context, state) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + const Center( + child: Text( + 'Регистрация', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1, color: Color(0xFF323F4B)), + ), ), - ), - const SizedBox(width: 12), - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 22, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: 24), + Row( children: [ - const Text('Фамилия'), - const SizedBox(height: 4), - CustomTextFormField(controller: TextEditingController(), hintText: 'Введите фамилию', onChanged: (newValue) {}), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - const Text('Почта'), - const SizedBox(height: 4), - CustomTextFormField( - controller: TextEditingController(), - textInputType: TextInputType.emailAddress, - keyboardType: TextInputType.emailAddress, - hintText: 'Введите email', - textInputAction: TextInputAction.next, - onChanged: (newValue) {}, - ), - const SizedBox(height: 12), - const Text('Номер телефона'), - const SizedBox(height: 4), - CustomTextFormField( - textInputAction: TextInputAction.done, - hintText: '+7 (999) 999 99 99', - controller: TextEditingController(), - keyboardType: TextInputType.number, - inputFormatters: [maskFormatter], - onChanged: (newValue) {}, - ), - const SizedBox(height: 12), - Row( - children: [ - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 22, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Дата рождения'), - const SizedBox(height: 4), - CustomTextFormField( - textInputAction: TextInputAction.done, - hintText: '01.01.2000', - controller: TextEditingController(), - keyboardType: TextInputType.number, - inputFormatters: [maskFormatterForBirthday], - onChanged: (newValue) {}, + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Имя'), + const SizedBox(height: 4), + CustomTextFormField(controller: firstNameController, hintText: 'Введите имя', onChanged: (newValue) {}), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Фамилия'), + const SizedBox(height: 4), + CustomTextFormField(controller: lastNameController, hintText: 'Введите фамилию', onChanged: (newValue) {}), + ], + ), ), ], ), - ), - const SizedBox(width: 12), - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 22, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: 12), + const Text('Почта'), + const SizedBox(height: 4), + CustomTextFormField( + controller: emailController, + textInputType: TextInputType.emailAddress, + keyboardType: TextInputType.emailAddress, + hintText: 'Введите email', + textInputAction: TextInputAction.next, + onChanged: (newValue) {}, + ), + const SizedBox(height: 12), + const Text('Номер телефона'), + const SizedBox(height: 4), + CustomTextFormField( + textInputAction: TextInputAction.done, + hintText: '+7 (999) 999 99 99', + controller: phoneController, + keyboardType: TextInputType.number, + inputFormatters: [maskFormatter], + onChanged: (newValue) {}, + ), + const SizedBox(height: 12), + Row( children: [ - const Text('Пол'), - const SizedBox(height: 4), - Row( + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Дата рождения'), + const SizedBox(height: 4), + CustomTextFormField( + textInputAction: TextInputAction.done, + hintText: '01.01.2000', + controller: dateOfBirthController, + keyboardType: TextInputType.number, + inputFormatters: [maskFormatterForBirthday], + onChanged: (newValue) {}, + ), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Пол'), + const SizedBox(height: 4), + Row( + children: [ + Expanded(child: _genderButton('М', isMaleSelected)), + const SizedBox(width: 4), + Expanded(child: _genderButton('Ж', !isMaleSelected)), + ], + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Вес'), + const SizedBox(height: 4), + CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Рост'), + const SizedBox(height: 4), + CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}), + ], + ), + ), + ], + ), + const SizedBox(height: 28), + const Center( + child: Text( + 'Проверьте правильность заполненных\nданных. Пол, рост и вес требуются для\nрасчета вашей дневной нормы калорий', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 28), + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => _handleRegistration(context), + child: Ink( + width: MediaQuery.of(context).size.width - 32, + height: 52, + decoration: BoxDecoration(color: state is OAuthStateLoading ? Colors.grey : appTheme.blueA400, borderRadius: BorderRadius.circular(20)), + child: Center( + child: state is OAuthStateLoading + ? const CircularProgressIndicator(color: Colors.white) + : const Text( + 'Зарегистрироваться', + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18, color: Colors.white), + ), + ), + ), + ), + const SizedBox(height: 16), + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + text: 'Нажимая "Зарегистрироваться", Вы соглашаетесь\n', + style: const TextStyle(fontSize: 16, color: Colors.black), children: [ - Expanded(child: _genderButton('М', true)), - const SizedBox(width: 4), - Expanded(child: _genderButton('Ж', false)), + const TextSpan( + text: 'c ', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + if (!hidePlans) ...[ + TextSpan( + text: ' Публичной офертой', + style: TextStyle( + fontSize: 16, + color: appTheme.blueA400, + decorationColor: appTheme.blueA400, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), + ), + const TextSpan( + text: ' и ', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + ], + TextSpan( + text: 'Политикой конфиденциальности и защитой персональных данных', + style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + ), ], ), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 22, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Вес'), - const SizedBox(height: 4), - CustomTextFormField(controller: TextEditingController(), hintText: 'Введите вес', onChanged: (newValue) {}), - ], - ), - ), - const SizedBox(width: 12), - SizedBox( - width: MediaQuery.of(context).size.width / 2 - 22, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Рост'), - const SizedBox(height: 4), - CustomTextFormField(controller: TextEditingController(), hintText: 'Введите рост', onChanged: (newValue) {}), - ], - ), - ), - ], - ), - const SizedBox(height: 28), - const Center( - child: Text( - 'Проверьте правильность заполненных\nданных. Пол, рост и вес требуются для\nрасчета вашей дневной нормы калорий', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), - textAlign: TextAlign.center, - ), - ), - const SizedBox(height: 28), - InkWell( - borderRadius: BorderRadius.circular(20), - onTap: () {}, - child: Ink( - width: MediaQuery.of(context).size.width - 32, - height: 52, - decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)), - child: const Center( - child: Text( - 'Зарегистрироваться', - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18, color: Colors.white), - ), - ), - ), - ), - const SizedBox(height: 16), - Center( - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - text: 'Нажимая "Зарегистрироваться", Вы соглашаетесь\n', - style: const TextStyle(fontSize: 16, color: Colors.black), - children: [ - const TextSpan( - text: 'c ', - style: TextStyle(fontSize: 16, color: Colors.black), ), - if (!hidePlans) ...[ - TextSpan( - text: ' Публичной офертой', - style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), + ), + // Показываем ошибки если есть + if (state is OAuthStateError) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.shade200), ), - const TextSpan( - text: ' и ', - style: TextStyle(fontSize: 16, color: Colors.black), + child: Text( + state.errorMessage, + style: TextStyle(color: Colors.red.shade700), + textAlign: TextAlign.center, ), - ], - TextSpan( - text: 'Политикой конфиденциальности и защитой персональных данных', - style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), ), ], - ), + // Обработка успешной регистрации + if (state is OAuthStateLoaded) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: const Text( + 'Регистрация успешно завершена!', + style: TextStyle(color: Colors.green), + textAlign: TextAlign.center, + ), + ), + // Автоматический переход на главный экран после успешной регистрации + Builder( + builder: (context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _reconnectWebSocket(context); + }); + return const SizedBox.shrink(); + }, + ), + ], + ], ), - ), - ], + ); + }, ), ), ); @@ -219,29 +455,67 @@ class _RegisterViaSocialScreenState extends State { Widget _genderButton(String gender, bool isActive) { return InkWell( - onTap: () {}, + onTap: () { + setState(() { + isMaleSelected = gender == 'М'; + }); + }, borderRadius: BorderRadius.circular(20), child: Ink( height: 52, decoration: BoxDecoration( color: const Color(0xFFF6F6F6), borderRadius: BorderRadius.circular(20), - border: isActive ? Border.all( - width: 2, - color: appTheme.blueA400, - ) : null, + border: isActive ? Border.all(width: 2, color: appTheme.blueA400) : null, ), child: Center( child: Text( gender, - style: TextStyle( - color: isActive ? appTheme.blueA400 : const Color(0xFF323F4B), - fontSize: 16, - fontWeight: FontWeight.w500, - ), + style: TextStyle(color: isActive ? appTheme.blueA400 : const Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500), ), ), ), ); } + + void _handleRegistration(BuildContext context) { + // Валидация полей + if (firstNameController.text.isEmpty || + lastNameController.text.isEmpty || + emailController.text.isEmpty || + phoneController.text.isEmpty || + dateOfBirthController.text.isEmpty || + weightController.text.isEmpty || + heightController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Пожалуйста, заполните все поля'))); + return; + } + + // Парсим числовые значения + int? weight; + int? height; + try { + weight = int.parse(weightController.text); + height = int.parse(heightController.text); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Вес и рост должны быть числами'))); + return; + } + + // Вызываем OAuth регистрацию + context.read().getOAuthToken( + provider: widget.provider ?? 'social', + accessToken: widget.authResult?.accessToken ?? 'dummy_token', + id: widget.authResult?.userId ?? 'dummy_id', + refreshToken: widget.authResult?.refreshToken ?? 'dummy_refresh_token', + firstName: firstNameController.text, + lastName: lastNameController.text, + email: emailController.text, + phone: phoneController.text, + dateOfBirth: dateOfBirthController.text, + gender: isMaleSelected ? 'М' : 'Ж', + weight: weight, + height: height, + ); + } } diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index 84df92f6..bddebce1 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/features/auth/models/credentials_model.dart'; +import 'package:eatvio_app/features/auth/models/oauth_model.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; @@ -8,26 +9,17 @@ class AuthRepository { AuthRepository({required this.authService}); - Future getTokenByCredentials( - String email, - String password, - ) async { + Future getTokenByCredentials(String email, String password) async { try { return await authService.getTokenByCredentials( - credentials: CredentialsModel( - email: email, - password: password, - ), + credentials: CredentialsModel(email: email, password: password), ); } catch (e) { rethrow; } } - Future getTokenBySocial({ - required String provider, - required String accessToken, - }) async { + Future getTokenBySocial({required String provider, required String accessToken}) async { try { return await authService.getTokenBySocial( body: SocialExchangeRequest(provider: provider, accessToken: accessToken), @@ -36,4 +28,40 @@ class AuthRepository { rethrow; } } + + Future getOAuthToken({ + required String provider, + required String accessToken, + required String id, + required String refreshToken, + required String firstName, + required String lastName, + required String email, + required String phone, + required String dateOfBirth, + required String gender, + required int weight, + required int height, + }) async { + try { + return (await authService.getOAuthToken( + body: OAuthModel( + provider: provider, + accessToken: accessToken, + id: id, + refreshToken: refreshToken, + firstName: firstName, + lastName: lastName, + email: email, + phone: phone, + dateOfBirth: dateOfBirth, + gender: gender == 'М' ? 'male' : 'female', + weight: weight, + height: height, + ), + ))['token']! as String; + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index a235f03d..ada242a9 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -27,7 +27,7 @@ class SocialAuthFacade { try { final controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: VkProvider.vkID, scopes: [VkScope.email, VkScope.phone]); final authUrl = authUri.value.toString(); final codeVerifier = authUri.key; @@ -105,18 +105,16 @@ class SocialAuthFacade { return null; } - static Future signInWithOk() async { + static Future signInWithOkAndFetchProfile() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; - final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu); - return vkAuthRes?.accessToken; + return await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu); } - static Future signInWithMailRu() async { + static Future signInWithMailRuAndFetchProfile() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; - final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu); - return vkAuthRes?.accessToken; + return await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu); } static Future signInWithYandex() async { diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 7dabb65b..3d8d43e2 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/features/auth/models/credentials_model.dart'; +import 'package:eatvio_app/features/auth/models/oauth_model.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; import 'package:retrofit/retrofit.dart'; @@ -19,4 +20,9 @@ abstract class AuthService { Future getTokenBySocial({ @Body() required SocialExchangeRequest body, }); + + @POST('/api/oauth/callback') + Future> getOAuthToken({ + @Body() required OAuthModel body, + }); } From b2433b87b6d281066dfc0af394dcad09b31fb8fd Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 10 Oct 2025 14:08:21 +0300 Subject: [PATCH 36/49] feat(diary): added navigation to detailed diet screen form diary --- lib/core/routing/content_routes.dart | 9 +- lib/features/auth/sdk/social_auth_facade.dart | 13 +- .../widgets/chosen_diet_widget.dart | 110 ++++++++------- .../presentation/cubit/about_diet_cubit.dart | 129 ++++++++---------- .../presentation/pages/about_diet_screen.dart | 11 +- 5 files changed, 139 insertions(+), 133 deletions(-) diff --git a/lib/core/routing/content_routes.dart b/lib/core/routing/content_routes.dart index 69d7ebf4..0556fab6 100644 --- a/lib/core/routing/content_routes.dart +++ b/lib/core/routing/content_routes.dart @@ -44,10 +44,15 @@ class ContentRoutes { builder: (context, state) { final slug = state.uri.queryParameters['slug']; if (slug != null) { - return AboutDietScreen(slug: slug, index: null, navigateToComments: false); + return AboutDietScreen(id: null, slug: slug, index: null, navigateToComments: false); } final data = state.extra as Map; - return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + return AboutDietScreen( + id: data['id'] as int?, + slug: data['slug'] as String?, + index: data['index'] as int?, + navigateToComments: data['navigateToComments'] as bool, + ); }, ), GoRoute( diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index a235f03d..8ea57b48 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -27,7 +27,7 @@ class SocialAuthFacade { try { final controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(scopes: [VkScope.phone, VkScope.email, VkScope.personalInfo]); final authUrl = authUri.value.toString(); final codeVerifier = authUri.key; @@ -76,7 +76,16 @@ class SocialAuthFacade { log(msg); return null; } + final response = await http.get(Uri.parse('https://id.vk.com/openapi/v1/phone_number'), headers: {'Authorization': 'Bearer ${auth.accessToken}'}); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + final phone = json['phone_number']; + log(json.toString()); + log('Phone: $phone'); + } else { + log('VK phone request failed: ${response.statusCode} ${response.body}'); + } return AuthResultEntity( accessToken: auth.accessToken, refreshToken: auth.refreshToken, @@ -204,7 +213,7 @@ class SocialAuthFacade { try { final controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email, VkScope.personalInfo]); final codeVerifier = authUri.key; final authUrl = authUri.value.toString(); final redirectScheme = 'vk$clientIdStr://'; diff --git a/lib/features/diary/presentation/widgets/chosen_diet_widget.dart b/lib/features/diary/presentation/widgets/chosen_diet_widget.dart index 3b5f23fd..622e0e5e 100644 --- a/lib/features/diary/presentation/widgets/chosen_diet_widget.dart +++ b/lib/features/diary/presentation/widgets/chosen_diet_widget.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/routing/route_constants.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/widgets/custom_divider.dart'; @@ -6,77 +7,84 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; class ChosenDietWidget extends StatelessWidget { const ChosenDietWidget({super.key}); @override Widget build(BuildContext context) { - return BlocBuilder(builder: (context, state) { - if (state is DiaryStateLoaded) { - if (state.diary.typeOfDiary.hasChosenDiet) { - return ChosenDietWidgetContent( - currentDietDay: state.diary.dayOfChosenDiet ?? 0, - dietName: state.diary.diet?.name ?? '', - dietDuration: state.diary.diet?.duration ?? 0, - ); + return BlocBuilder( + builder: (context, state) { + if (state is DiaryStateLoaded) { + if (state.diary.typeOfDiary.hasChosenDiet) { + return ChosenDietWidgetContent( + currentDietDay: state.diary.dayOfChosenDiet ?? 0, + dietName: state.diary.diet?.name ?? '', + dietDuration: state.diary.diet?.duration ?? 0, + dietId: state.diary.diet?.id ?? 0, + ); + } } - } - if (state is DiaryStateLoadedTomorrowWithDiet) { - if (state.diary.typeOfDiary.hasChosenDiet) { - return Column( - children: [ - const CustomDivider(), - const SizedBox(height: 16), - ChosenDietWidgetContent( - currentDietDay: state.diary.dayOfChosenDiet ?? 0, - dietName: state.diary.diet?.name ?? '', - dietDuration: state.diary.diet?.duration ?? 0, - ), - ], - ); + if (state is DiaryStateLoadedTomorrowWithDiet) { + if (state.diary.typeOfDiary.hasChosenDiet) { + return Column( + children: [ + const CustomDivider(), + const SizedBox(height: 16), + ChosenDietWidgetContent( + currentDietDay: state.diary.dayOfChosenDiet ?? 0, + dietName: state.diary.diet?.name ?? '', + dietDuration: state.diary.diet?.duration ?? 0, + dietId: state.diary.diet?.id ?? 0, + ), + ], + ); + } } - } - return const SizedBox.shrink(); - }); + return const SizedBox.shrink(); + }, + ); } } class ChosenDietWidgetContent extends StatelessWidget { - const ChosenDietWidgetContent({ - super.key, - required this.currentDietDay, - required this.dietName, - required this.dietDuration, - }); + const ChosenDietWidgetContent({super.key, required this.currentDietDay, required this.dietName, required this.dietDuration, required this.dietId}); final int currentDietDay; final String dietName; final int dietDuration; + final int dietId; @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); final s = S.of(context); - return Column( - children: [ - Row( - children: [ - SizedBox( - width: size.width * 0.4, - child: Text(dietName, style: TextStyles.textLMedium.copyWith(color: appTheme.black)), - ), - const Spacer(), - Container( - decoration: BoxDecoration(color: appTheme.dividerColor, borderRadius: BorderRadius.circular(12)), - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), - child: Text('${s.day} $currentDietDay из $dietDuration', style: TextStyles.textMSemibold.copyWith(color: appTheme.gray)), - ) - ], - ), - const SizedBox( - height: 16, - ) - ], + return GestureDetector( + onTap: () { + GoRouter.of(context).push(RouteConstants.aboutDietScreen, extra: {'id': dietId, 'navigateToComments': false}); + }, + child: Column( + children: [ + Row( + children: [ + SizedBox( + width: size.width * 0.4, + child: Text( + dietName, + style: TextStyles.textLMedium.copyWith(color: appTheme.black, decoration: TextDecoration.underline), + ), + ), + const Spacer(), + Container( + decoration: BoxDecoration(color: appTheme.dividerColor, borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + child: Text('${s.day} $currentDietDay из $dietDuration', style: TextStyles.textMSemibold.copyWith(color: appTheme.gray)), + ), + ], + ), + const SizedBox(height: 16), + ], + ), ); } } diff --git a/lib/features/diets/presentation/cubit/about_diet_cubit.dart b/lib/features/diets/presentation/cubit/about_diet_cubit.dart index 9d02534e..6ba7980f 100644 --- a/lib/features/diets/presentation/cubit/about_diet_cubit.dart +++ b/lib/features/diets/presentation/cubit/about_diet_cubit.dart @@ -19,9 +19,14 @@ class AboutDietCubit extends Cubit { String? userStartedDietName; List _selectedIndexes = []; - Future getDiet(String slug, {int? index}) async { + Future getDiet({String? slug, int? id, int? index}) async { try { - final diet = await dietsRepository.getDietBySlug(slug); + final Diet diet; + if (slug != null) { + diet = await dietsRepository.getDietBySlug(slug); + } else { + diet = await dietsRepository.getDiet(id!); + } if (index != null) { dietsRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: diet.totalComments)); } @@ -32,10 +37,16 @@ class AboutDietCubit extends Cubit { } } - Future initDietPage(String slug) async { + Future initDietPage({String? slug, int? id}) async { try { emit(const AboutDietState.loading()); - final diet = await dietsRepository.getDietBySlug(slug); + final Diet diet; + if (slug != null) { + diet = await dietsRepository.getDietBySlug(slug); + } else { + diet = await dietsRepository.getDiet(id!); + } + final chosenDiets = await profileRepository.getChosenDiets(); final lastChosenDiet = chosenDiets.firstOrNull; if (lastChosenDiet != null && ['inited', 'started'].contains(lastChosenDiet.state)) { @@ -50,12 +61,7 @@ class AboutDietCubit extends Cubit { } } _selectedIndexes = List.generate(diet.dailySchedule.length, (index) => 0); - emit(AboutDietState.loaded( - diet: diet, - isStarted: isCurrentDietStarted, - hasStartedDiet: hasStartedDiet, - selectedIndexes: _selectedIndexes, - )); + emit(AboutDietState.loaded(diet: diet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes)); } catch (e) { log(e.toString()); emit(AboutDietState.error(message: e.toString())); @@ -68,41 +74,30 @@ class AboutDietCubit extends Cubit { final currentDiet = (state as DietStateLoaded).diet; if (currentDiet.wasLikedByMe) { - final updatedDiet = currentDiet.copyWith( - totalLikes: currentDiet.totalLikes - 1, - wasLikedByMe: false, - ); + final updatedDiet = currentDiet.copyWith(totalLikes: currentDiet.totalLikes - 1, wasLikedByMe: false); if (dietIndexInFeed != null) { dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); } emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes)); } else { - final updatedDiet = currentDiet.copyWith( - totalLikes: currentDiet.totalLikes + 1, - wasLikedByMe: true, - ); + final updatedDiet = currentDiet.copyWith(totalLikes: currentDiet.totalLikes + 1, wasLikedByMe: true); if (dietIndexInFeed != null) { dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); } emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes)); } - EasyDebounce.debounce( - 'like_diet', - const Duration(seconds: 1), - () async { - try { - await dietsRepository.likeDiet(id); - } catch (e) { - log(e.toString()); - if (dietIndexInFeed != null) { - dietsRepository - .emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); - } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + EasyDebounce.debounce('like_diet', const Duration(seconds: 1), () async { + try { + await dietsRepository.likeDiet(id); + } catch (e) { + log(e.toString()); + if (dietIndexInFeed != null) { + dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); } - }, - ); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + } + }); } Future addToFavorite(int id, int? dietIndex) async { @@ -110,30 +105,24 @@ class AboutDietCubit extends Cubit { final currentDiet = (state as DietStateLoaded).diet; - final updatedDiet = currentDiet.copyWith( - isFavorite: true, - ); + final updatedDiet = currentDiet.copyWith(isFavorite: true); if (dietIndex != null) { dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); - EasyDebounce.debounce( - 'add_or_remove_diet_to_favorites', - const Duration(seconds: 1), - () async { - try { - await dietsRepository.addToFavorite(id); - } catch (e) { - log(e.toString()); - showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - if (dietIndex != null) { - dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); - } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + EasyDebounce.debounce('add_or_remove_diet_to_favorites', const Duration(seconds: 1), () async { + try { + await dietsRepository.addToFavorite(id); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); } - }, - ); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + } + }); } Future removeFromFavorite(int id, int? dietIndex) async { @@ -141,30 +130,24 @@ class AboutDietCubit extends Cubit { final currentDiet = (state as DietStateLoaded).diet; - final updatedDiet = currentDiet.copyWith( - isFavorite: false, - ); + final updatedDiet = currentDiet.copyWith(isFavorite: false); if (dietIndex != null) { dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); - EasyDebounce.debounce( - 'add_or_remove_diet_to_favorites', - const Duration(seconds: 1), - () async { - try { - await dietsRepository.removeFromFavorite(id); - } catch (e) { - log(e.toString()); - showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - if (dietIndex != null) { - dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); - } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + EasyDebounce.debounce('add_or_remove_diet_to_favorites', const Duration(seconds: 1), () async { + try { + await dietsRepository.removeFromFavorite(id); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); } - }, - ); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + } + }); } Future startDiet(DateTime date) async { @@ -199,13 +182,13 @@ class AboutDietCubit extends Cubit { void clearStartedDietsState() { if (state is! DietStateLoaded) return; final diet = (state as DietStateLoaded).diet; - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet)); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: diet)); } void emitCurrentStartedDietsState() { if (state is! DietStateLoaded) return; final diet = (state as DietStateLoaded).diet; - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet, isStarted: true)); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: diet, isStarted: true)); } Future getDietBySlug(String slug) async { diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index e4dd4a75..5b6dc8b8 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -35,11 +35,12 @@ import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; class AboutDietScreen extends StatefulWidget { - final String slug; + final String? slug; + final int? id; final int? index; final bool navigateToComments; - const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug}); + const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug, required this.id}); @override State createState() => _AboutDietScreenState(); @@ -79,7 +80,7 @@ class _AboutDietScreenState extends State { final s = S.of(context); return BlocProvider( - create: (context) => dietsCubit..initDietPage(widget.slug), + create: (context) => dietsCubit..initDietPage(slug: widget.slug, id: widget.id), child: BlocProvider( create: (BuildContext context) => commentsCubit, child: BlocConsumer( @@ -283,7 +284,7 @@ class _AboutDietScreenState extends State { replyId = null; }); Future.delayed(const Duration(milliseconds: 500), () { - dietsCubit.getDiet(dietState.diet.slug, index: widget.index); + dietsCubit.getDiet(slug: dietState.diet.slug, index: widget.index); }); FocusManager.instance.primaryFocus?.unfocus(); } @@ -526,7 +527,7 @@ class _AboutDietScreenState extends State { ), ), body: DataErrorWidget( - onReload: () => context.read().getDiet(widget.slug, index: widget.index), + onReload: () => context.read().getDiet(slug: widget.slug, id: widget.id, index: widget.index), ), ); } From 654edc54573f756294c9c4b86bf7de70a8b23fd7 Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 10 Oct 2025 14:37:27 +0300 Subject: [PATCH 37/49] fix: analyzer issues --- lib/core/app_initializer.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart index 57ccbfb0..ea0b8f5d 100644 --- a/lib/core/app_initializer.dart +++ b/lib/core/app_initializer.dart @@ -6,7 +6,6 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/firebase_options.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; From f90f1d32841f011750e8457c307a0693ea5bca2b Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 10 Oct 2025 14:45:15 +0300 Subject: [PATCH 38/49] feat(social): fixed baseUrl for RegisterViaSocialScreen --- .../auth/presentation/pages/register_via_social_screen.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index 22fa5d18..72fa9d91 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; @@ -385,7 +386,7 @@ class _RegisterViaSocialScreenState extends State { decorationColor: appTheme.blueA400, decoration: TextDecoration.underline, ), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl().baseUrl}oferta_bs.pdf')), ), const TextSpan( text: ' и ', @@ -395,7 +396,7 @@ class _RegisterViaSocialScreenState extends State { TextSpan( text: 'Политикой конфиденциальности и защитой персональных данных', style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl().baseUrl}/docs/privacy.pdf')), ), ], ), From 239ff675dbd323734e3f73c34ccf94a1c7d32e71 Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 10 Oct 2025 14:58:24 +0300 Subject: [PATCH 39/49] fix: fixed author card image url handling --- .../presentation/widgets/author_card.dart | 48 ++++--------------- 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/lib/features/diets/presentation/widgets/author_card.dart b/lib/features/diets/presentation/widgets/author_card.dart index 548b9263..34131dac 100644 --- a/lib/features/diets/presentation/widgets/author_card.dart +++ b/lib/features/diets/presentation/widgets/author_card.dart @@ -38,45 +38,25 @@ class AuthorCard extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, image: DecorationImage( - image: NetworkImage(sl().baseUrl + author.authorProfileIcon), + image: NetworkImage( + author.authorProfileIcon.contains('https') ? author.authorProfileIcon : sl().baseUrl + author.authorProfileIcon, + ), fit: BoxFit.cover, ), ), ), - subtitle: Text( - author.authorName, - style: TextStyles.textLSemibold.copyWith( - color: appTheme.black, - ), - ), - title: Text( - title, - style: titleStyle ?? TextStyles.textSSemibold.copyWith( - color: appTheme.gray, - ), - ), + subtitle: Text(author.authorName, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)), + title: Text(title, style: titleStyle ?? TextStyles.textSSemibold.copyWith(color: appTheme.gray)), ), Row( children: [ - Text( - 'Консультант', - style: TextStyles.textMMedium.copyWith( - color: appTheme.black, - ), - ), + Text('Консультант', style: TextStyles.textMMedium.copyWith(color: appTheme.black)), const SizedBox(width: 10), if (author.authorPlaceInRating != null) InfoBouble( radius: 12, backgroundColor: appTheme.whiteA700, - children: [ - Text( - '${author.authorPlaceInRating}-й в рейтинге', - style: TextStyles.textMMediumAuto.copyWith( - color: appTheme.blueA400, - ), - ), - ], + children: [Text('${author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400))], ), ], ), @@ -89,18 +69,8 @@ class AuthorCard extends StatelessWidget { child: Container( width: 28, height: 28, - decoration: BoxDecoration( - color: appTheme.whiteA700, - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: SvgPicture.asset( - 'assets/icons/arrow.svg', - width: 12, - height: 12, - fit: BoxFit.none, - ), - ), + decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(8)), + child: Center(child: SvgPicture.asset('assets/icons/arrow.svg', width: 12, height: 12, fit: BoxFit.none)), ), ), ], From 21492a1ad9ad39bef7e2630cc664098286d1b86c Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 10 Oct 2025 15:18:25 +0300 Subject: [PATCH 40/49] fix: removed wrong code --- lib/features/auth/sdk/social_auth_facade.dart | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 80703020..0b60f6a6 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -76,16 +76,7 @@ class SocialAuthFacade { log(msg); return null; } - final response = await http.get(Uri.parse('https://id.vk.com/openapi/v1/phone_number'), headers: {'Authorization': 'Bearer ${auth.accessToken}'}); - if (response.statusCode == 200) { - final json = jsonDecode(response.body); - final phone = json['phone_number']; - log(json.toString()); - log('Phone: $phone'); - } else { - log('VK phone request failed: ${response.statusCode} ${response.body}'); - } return AuthResultEntity( accessToken: auth.accessToken, refreshToken: auth.refreshToken, @@ -211,7 +202,7 @@ class SocialAuthFacade { try { final controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email, VkScope.personalInfo]); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email,]); final codeVerifier = authUri.key; final authUrl = authUri.value.toString(); final redirectScheme = 'vk$clientIdStr://'; From 24675714e016fc8e0ee64e202f98b60f4c44b3aa Mon Sep 17 00:00:00 2001 From: misha Date: Fri, 10 Oct 2025 17:02:08 +0300 Subject: [PATCH 41/49] feat(social): added vk client id for ios --- lib/core/env_config.dart | 3 ++- lib/features/auth/sdk/social_auth_facade.dart | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart index 446bc65b..3d451c6f 100644 --- a/lib/core/env_config.dart +++ b/lib/core/env_config.dart @@ -45,7 +45,8 @@ class EnvConfig { static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? ''; // VK ID Configuration - static String get vkClientId => dotenv.env['VK_CLIENT_ID'] ?? ''; + static String get vkClientIdIos => dotenv.env['VK_CLIENT_ID_IOS'] ?? ''; + static String get vkClientIdAndroid => dotenv.env['VK_CLIENT_ID_ANDROID'] ?? ''; // Yandex ID Configuration static String get yandexClientId => dotenv.env['YANDEX_CLIENT_ID'] ?? ''; diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index ada242a9..b7f4f93d 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; +import 'dart:io'; import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart'; @@ -20,7 +21,7 @@ class SocialAuthFacade { } static Future signInWithVkAndFetchProfile({required BuildContext context}) async { - final clientIdStr = EnvConfig.vkClientId.trim(); + final clientIdStr = Platform.isIOS ? EnvConfig.vkClientIdIos.trim() : EnvConfig.vkClientIdAndroid.trim(); if (clientIdStr.isEmpty) { return null; } @@ -195,7 +196,7 @@ class SocialAuthFacade { } static Future _signInWithVkIdProvider({required BuildContext context, required VkProvider provider}) async { - final clientIdStr = EnvConfig.vkClientId.trim(); + final clientIdStr = Platform.isIOS ? EnvConfig.vkClientIdIos.trim() : EnvConfig.vkClientIdAndroid.trim(); if (clientIdStr.isEmpty) { return null; } From 09a00a41adaa887c6ed81990fa4affd064aec09f Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 14 Oct 2025 12:09:26 +0300 Subject: [PATCH 42/49] added scopes for yandex id --- lib/features/auth/sdk/social_auth_facade.dart | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index b7f4f93d..4cab0962 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -91,21 +91,6 @@ class SocialAuthFacade { ); } - Future?> exchangeCodeForToken(String code, String clientId, String clientSecret) async { - final response = await http.post( - Uri.parse('https://oauth.yandex.ru/token'), - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret}, - ); - - if (response.statusCode == 200) { - return jsonDecode(response.body) as Map; - } else { - log('Error exchanging code: ${response.body}'); - } - return null; - } - static Future signInWithOkAndFetchProfile() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; @@ -134,7 +119,8 @@ class SocialAuthFacade { 'response_type': 'code', 'client_id': clientId, 'redirect_uri': redirectUri, - 'force_confirm': 'yes', + 'scope': 'login:info login:email', + 'force_confirm': 'true', }).toString(); final redirectSchemePrefix = redirectUri; From 5354c3f517c21f3af679074f8d9e28b7bfa67e66 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 14 Oct 2025 12:16:20 +0300 Subject: [PATCH 43/49] feat(auth): Issue-691 --- lib/core/app_router.dart | 1 + .../auth/models/oauth_login_request.dart | 15 ++++ .../bloc/oauth_cubit/oauth_cubit.dart | 19 ++++ .../auth/presentation/pages/auth_screen.dart | 90 +++++++++++++++++-- .../pages/register_via_social_screen.dart | 24 +++-- .../auth/repositories/auth_repository.dart | 42 +++++---- lib/features/auth/services/auth_service.dart | 16 ++-- 7 files changed, 168 insertions(+), 39 deletions(-) create mode 100644 lib/features/auth/models/oauth_login_request.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 26355055..5ce04746 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -53,6 +53,7 @@ class AppRouter { } static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) { + print('AppRouter: Navigating to register via social screen for provider: $provider'); appRouter.goNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider}); } } diff --git a/lib/features/auth/models/oauth_login_request.dart b/lib/features/auth/models/oauth_login_request.dart new file mode 100644 index 00000000..43da454b --- /dev/null +++ b/lib/features/auth/models/oauth_login_request.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'oauth_login_request.freezed.dart'; +part 'oauth_login_request.g.dart'; + +@freezed +abstract class OAuthLoginRequest with _$OAuthLoginRequest { + const factory OAuthLoginRequest({ + required String provider, + @JsonKey(name: 'access_token') required String accessToken, + @JsonKey(name: 'refresh_token') required String refreshToken, + }) = _OAuthLoginRequest; + + factory OAuthLoginRequest.fromJson(Map json) => _$OAuthLoginRequestFromJson(json); +} diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart index 8d73b195..7d45b4b1 100644 --- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -7,6 +8,23 @@ class OAuthCubit extends Cubit { OAuthCubit({required this.authRepository}) : super(const OAuthState()); + Future getOAuthLogin({required String provider, required String accessToken, required String refreshToken}) async { + try { + log('OAuthCubit: Starting OAuth login for provider: $provider'); + emit(const OAuthState.loading()); + log('OAuthCubit: Emitted loading state'); + + final tokenModel = await authRepository.getOAuthLogin(provider: provider, accessToken: accessToken, refreshToken: refreshToken); + log('OAuthCubit: Got token model: ${tokenModel.token}'); + emit(OAuthState.loaded(token: tokenModel.token)); + log('OAuthCubit: Emitted loaded state'); + } catch (e) { + log('OAuthCubit: Error occurred: $e'); + emit(OAuthState.error(errorMessage: e.toString())); + log('OAuthCubit: Emitted error state'); + } + } + Future getOAuthToken({ required String provider, required String accessToken, @@ -22,6 +40,7 @@ class OAuthCubit extends Cubit { required int height, }) async { try { + emit(const OAuthState.loading()); final token = await authRepository.getOAuthToken( provider: provider, accessToken: accessToken, diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 13713922..fbdbd809 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -14,6 +14,10 @@ import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; +import 'package:eatvio_app/core/di/cubit_registrations.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; @@ -391,16 +395,29 @@ class _SocialButton extends StatelessWidget { void _onVkPressed() async { try { + log('VK button pressed'); final context = AppRouter.navigatorKey.currentContext; - if (context == null) return; - - final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context); - if (authResult == null) { + if (context == null) { + log('Context is null'); return; } - AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'vk'); + + log('Getting VK auth result'); + final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context); + if (authResult == null) { + log('VK auth result is null'); + return; + } + + log('VK auth result received, trying OAuth login'); + // Пробуем сначала авторизоваться через новый API + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'vk'); + } else { + log('Context is not mounted'); + } } catch (e) { - log(e.toString()); + log('VK button error: $e'); } } @@ -413,7 +430,11 @@ void _onOkPressed() async { if (authResult == null) { return; } - AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'ok'); + + // Пробуем сначала авторизоваться через новый API + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'ok'); + } } catch (e) { log(e.toString()); } @@ -428,7 +449,11 @@ void _onMailRuPressed() async { if (authResult == null) { return; } - AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'mailru'); + + // Пробуем сначала авторизоваться через новый API + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'mailru'); + } } catch (e) { log(e.toString()); } @@ -436,14 +461,61 @@ void _onMailRuPressed() async { void _onYandexPressed() async { try { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + final authResult = await SocialAuthFacade.signInWithYandex(); if (authResult == null) return; - AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'yandex'); + + // Пробуем сначала авторизоваться через новый API + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'yandex'); + } } catch (e) { log(e.toString()); } } +/// Пробует авторизоваться через новый OAuth login API +/// Если получает ошибку (включая 422 - пользователь не найден), переходит к регистрации +Future _tryOAuthLogin(BuildContext context, AuthResultEntity authResult, String provider) async { + log('Starting OAuth login for provider: $provider'); + + try { + // Создаем OAuth cubit + final oauthCubit = CubitRegistrations.sl(); + StreamSubscription? subscription; + + // Слушаем изменения состояния + subscription = oauthCubit.stream.listen((state) { + log('OAuth state changed: ${state.runtimeType}'); + + if (state is OAuthStateLoaded) { + // Успешная авторизация - переходим на главный экран + log('OAuth login successful, navigating to main screen'); + subscription?.cancel(); + _reconnectAfterAuth(); + } else if (state is OAuthStateError) { + // Ошибка авторизации (включая 422 - пользователь не найден) - переходим к регистрации + log('OAuth login failed: ${state.errorMessage}'); + subscription?.cancel(); + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); + } + }); + + // Пробуем авторизоваться + log('Calling getOAuthLogin with provider: $provider'); + await oauthCubit.getOAuthLogin(provider: provider, accessToken: authResult.accessToken, refreshToken: authResult.refreshToken ?? ''); + + // Ждем немного, чтобы состояние успело обновиться + await Future.delayed(const Duration(milliseconds: 100)); + } catch (e) { + // В случае любой ошибки (включая 422 - пользователь не найден) переходим к регистрации + log('OAuth login error in catch block: $e'); + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); + } +} + Future _reconnectAfterAuth() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return; diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index 22fa5d18..a0175726 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -30,8 +30,8 @@ class RegisterViaSocialScreen extends StatefulWidget { } class _RegisterViaSocialScreenState extends State { - final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')}); - final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}); + final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy); + final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy); // Контроллеры для полей ввода final firstNameController = TextEditingController(); @@ -99,10 +99,16 @@ class _RegisterViaSocialScreenState extends State { emailController.text = authResult.email!; } if (authResult.phone != null) { - phoneController.text = authResult.phone!; + final cleanPhone = authResult.phone!.replaceAll(RegExp(r'[^\d]'), ''); + final normalizedPhone = cleanPhone.startsWith('8') ? '7${cleanPhone.substring(1)}' : cleanPhone; + final phoneWithCountryCode = normalizedPhone.startsWith('7') ? normalizedPhone : '7$normalizedPhone'; + final formattedPhone = maskFormatter.maskText('+$phoneWithCountryCode'); + phoneController.text = formattedPhone; } if (authResult.birthDate != null) { - dateOfBirthController.text = _formatBirthDate(authResult.birthDate!); + final formattedDate = _formatBirthDate(authResult.birthDate!); + final maskedDate = maskFormatterForBirthday.maskText(formattedDate); + dateOfBirthController.text = maskedDate; } if (authResult.gender != null) { isMaleSelected = authResult.gender == GenderEnum.male; @@ -265,7 +271,10 @@ class _RegisterViaSocialScreenState extends State { controller: phoneController, keyboardType: TextInputType.number, inputFormatters: [maskFormatter], - onChanged: (newValue) {}, + onChanged: (newValue) { + // Дополнительная обработка изменений номера телефона + // Это поможет маске корректно работать при редактировании + }, ), const SizedBox(height: 12), Row( @@ -502,6 +511,9 @@ class _RegisterViaSocialScreenState extends State { return; } + // Очищаем номер телефона от маски для отправки + final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), ''); + // Вызываем OAuth регистрацию context.read().getOAuthToken( provider: widget.provider ?? 'social', @@ -511,7 +523,7 @@ class _RegisterViaSocialScreenState extends State { firstName: firstNameController.text, lastName: lastNameController.text, email: emailController.text, - phone: phoneController.text, + phone: cleanPhone, dateOfBirth: dateOfBirthController.text, gender: isMaleSelected ? 'М' : 'Ж', weight: weight, diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index bddebce1..1f66c039 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/features/auth/models/credentials_model.dart'; import 'package:eatvio_app/features/auth/models/oauth_model.dart'; +import 'package:eatvio_app/features/auth/models/oauth_login_request.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; @@ -29,6 +30,16 @@ class AuthRepository { } } + Future getOAuthLogin({required String provider, required String accessToken, required String refreshToken}) async { + try { + return await authService.getOAuthLogin( + body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken), + ); + } catch (e) { + rethrow; + } + } + Future getOAuthToken({ required String provider, required String accessToken, @@ -45,21 +56,22 @@ class AuthRepository { }) async { try { return (await authService.getOAuthToken( - body: OAuthModel( - provider: provider, - accessToken: accessToken, - id: id, - refreshToken: refreshToken, - firstName: firstName, - lastName: lastName, - email: email, - phone: phone, - dateOfBirth: dateOfBirth, - gender: gender == 'М' ? 'male' : 'female', - weight: weight, - height: height, - ), - ))['token']! as String; + body: OAuthModel( + provider: provider, + accessToken: accessToken, + id: id, + refreshToken: refreshToken, + firstName: firstName, + lastName: lastName, + email: email, + phone: phone, + dateOfBirth: dateOfBirth, + gender: gender == 'М' ? 'male' : 'female', + weight: weight, + height: height, + ), + ))['token']! + as String; } catch (e) { rethrow; } diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 3d8d43e2..02f8722a 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/features/auth/models/credentials_model.dart'; import 'package:eatvio_app/features/auth/models/oauth_model.dart'; +import 'package:eatvio_app/features/auth/models/oauth_login_request.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; import 'package:retrofit/retrofit.dart'; @@ -12,17 +13,14 @@ abstract class AuthService { factory AuthService(Dio dio, {String? baseUrl}) = _AuthService; @POST('/api/login') - Future getTokenByCredentials({ - @Body() required CredentialsModel credentials, - }); + Future getTokenByCredentials({@Body() required CredentialsModel credentials}); @POST('/api/social/login') - Future getTokenBySocial({ - @Body() required SocialExchangeRequest body, - }); + Future getTokenBySocial({@Body() required SocialExchangeRequest body}); @POST('/api/oauth/callback') - Future> getOAuthToken({ - @Body() required OAuthModel body, - }); + Future> getOAuthToken({@Body() required OAuthModel body}); + + @POST('/api/oauth/login') + Future getOAuthLogin({@Body() required OAuthLoginRequest body}); } From f573bb28f806b29b2991e21e969296a1153e5104 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 14 Oct 2025 19:25:26 +0300 Subject: [PATCH 44/49] feat(oauth): Issue-691 --- lib/core/app_router.dart | 3 +- .../pages/register_via_social_screen.dart | 54 +------------------ lib/features/auth/sdk/social_auth_facade.dart | 1 - lib/generated/l10n.dart | 7 ++- 4 files changed, 6 insertions(+), 59 deletions(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 5ce04746..d17019d2 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -53,7 +53,6 @@ class AppRouter { } static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) { - print('AppRouter: Navigating to register via social screen for provider: $provider'); - appRouter.goNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider}); + appRouter.pushNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider}); } } diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index b93f82d7..c803f687 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -79,17 +79,6 @@ class _RegisterViaSocialScreenState extends State { if (widget.authResult != null) { final authResult = widget.authResult!; - // Логируем полученные данные для отладки - print('AuthResult data:'); - print('firstName: ${authResult.firstName}'); - print('lastName: ${authResult.lastName}'); - print('email: ${authResult.email}'); - print('phone: ${authResult.phone}'); - print('birthDate: ${authResult.birthDate}'); - print('gender: ${authResult.gender}'); - print('provider: ${widget.provider}'); - - // Предзаполняем поля из AuthResultEntity if (authResult.firstName != null) { firstNameController.text = authResult.firstName!; } @@ -153,44 +142,6 @@ class _RegisterViaSocialScreenState extends State { return birthDate; } - String _getProviderName(String? provider) { - switch (provider) { - case 'vk': - return 'ВКонтакте'; - case 'yandex': - return 'Яндекс'; - case 'ok': - return 'Одноклассники'; - case 'mailru': - return 'Mail.ru'; - default: - return 'социальной сети'; - } - } - - String _getDataStatusMessage(AuthResultEntity authResult) { - final hasEmail = authResult.email != null && authResult.email!.isNotEmpty; - final hasPhone = authResult.phone != null && authResult.phone!.isNotEmpty; - final hasBirthDate = authResult.birthDate != null && authResult.birthDate!.isNotEmpty; - - if (hasEmail && hasPhone && hasBirthDate) { - return 'Все данные успешно получены и предзаполнены.'; - } else if (hasEmail && hasPhone) { - return 'Email и номер телефона получены. Дата рождения может быть недоступна из-за настроек приватности.'; - } else if (hasEmail) { - return 'Email получен. Номер телефона и дата рождения могут быть недоступны из-за настроек приватности.'; - } else if (hasPhone) { - return 'Номер телефона получен. Email и дата рождения могут быть недоступны из-за настроек приватности.'; - } else { - // Специальное сообщение для VK - if (widget.provider == 'vk') { - return 'Основные данные получены. Email запрашивается автоматически. Номер телефона получается через VK API. Если номер замаскирован, это означает, что пользователь ограничил доступ к нему в настройках приватности.'; - } else { - return 'Основные данные получены. Email, номер телефона и дата рождения могут быть недоступны из-за настроек приватности.'; - } - } - } - @override void dispose() { firstNameController.dispose(); @@ -208,15 +159,14 @@ class _RegisterViaSocialScreenState extends State { return BlocProvider( create: (context) => CubitRegistrations.sl(), child: Scaffold( - appBar: AppBar(toolbarHeight: 0), + appBar: AppBar(), body: BlocBuilder( builder: (context, state) { return SingleChildScrollView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 24), const Center( child: Text( 'Регистрация', diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 4cab0962..140da37b 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -119,7 +119,6 @@ class SocialAuthFacade { 'response_type': 'code', 'client_id': clientId, 'redirect_uri': redirectUri, - 'scope': 'login:info login:email', 'force_confirm': 'true', }).toString(); diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 1055e664..0ccdeac3 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,10 +28,9 @@ class S { static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { - final name = - (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); + final name = (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; From e073bd3f970e52835f1057fe1004a9946b7e3b68 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 15 Oct 2025 14:21:43 +0300 Subject: [PATCH 45/49] feat(socail): finished registration and ligin via social media --- lib/core/app_config.dart | 6 +- .../auth/entities/auth_result_entity.dart | 1 + .../auth/models/oauth_login_request.dart | 3 +- lib/features/auth/models/oauth_model.dart | 7 +- .../bloc/auth_cubit/auth_cubit.dart | 15 - .../bloc/oauth_cubit/oauth_cubit.dart | 23 +- .../auth/presentation/pages/auth_screen.dart | 275 +++++++++--------- .../pages/register_via_social_screen.dart | 5 +- .../auth/repositories/auth_repository.dart | 29 +- lib/features/auth/sdk/social_auth_facade.dart | 14 +- lib/features/auth/services/auth_service.dart | 8 +- lib/main.dart | 2 +- 12 files changed, 187 insertions(+), 201 deletions(-) diff --git a/lib/core/app_config.dart b/lib/core/app_config.dart index 03513645..45ad250b 100644 --- a/lib/core/app_config.dart +++ b/lib/core/app_config.dart @@ -15,10 +15,10 @@ import 'package:flutter_localizations/flutter_localizations.dart'; /// Конфигурация приложения class AppConfig { - AppConfig({required this.buildType}){ + AppConfig({required this.buildType}) { _baseUrl = switch (buildType) { AppBuildType.prod => EnvConfig.baseUrl, - AppBuildType.tests => EnvConfig.devBaseUrl, + AppBuildType.tests => EnvConfig.devBaseUrl, AppBuildType.dev => EnvConfig.devBaseUrl, }; } @@ -30,7 +30,7 @@ class AppConfig { void setBaseUrl(String baseUrls) { _baseUrl = baseUrls; } - + /// Список глобальных BlocProvider'ов List get globalBlocProviders => [ BlocProvider(create: (context) => sl()), diff --git a/lib/features/auth/entities/auth_result_entity.dart b/lib/features/auth/entities/auth_result_entity.dart index 2f296e5b..a8d08ff4 100644 --- a/lib/features/auth/entities/auth_result_entity.dart +++ b/lib/features/auth/entities/auth_result_entity.dart @@ -8,6 +8,7 @@ abstract class AuthResultEntity with _$AuthResultEntity { const factory AuthResultEntity({ required String accessToken, required String userId, + required String clientId, String? refreshToken, String? firstName, String? lastName, diff --git a/lib/features/auth/models/oauth_login_request.dart b/lib/features/auth/models/oauth_login_request.dart index 43da454b..97145a4d 100644 --- a/lib/features/auth/models/oauth_login_request.dart +++ b/lib/features/auth/models/oauth_login_request.dart @@ -6,9 +6,10 @@ part 'oauth_login_request.g.dart'; @freezed abstract class OAuthLoginRequest with _$OAuthLoginRequest { const factory OAuthLoginRequest({ - required String provider, + @JsonKey(name: 'client_id') required String clientId, @JsonKey(name: 'access_token') required String accessToken, @JsonKey(name: 'refresh_token') required String refreshToken, + required String provider, }) = _OAuthLoginRequest; factory OAuthLoginRequest.fromJson(Map json) => _$OAuthLoginRequestFromJson(json); diff --git a/lib/features/auth/models/oauth_model.dart b/lib/features/auth/models/oauth_model.dart index aff79b5f..0156e7aa 100644 --- a/lib/features/auth/models/oauth_model.dart +++ b/lib/features/auth/models/oauth_model.dart @@ -6,15 +6,16 @@ part 'oauth_model.g.dart'; @freezed abstract class OAuthModel with _$OAuthModel { const factory OAuthModel({ - required String provider, - required String id, @JsonKey(name: 'access_token') required String accessToken, @JsonKey(name: 'refresh_token') required String refreshToken, @JsonKey(name: 'first_name') required String firstName, @JsonKey(name: 'last_name') required String lastName, + @JsonKey(name: 'date_of_birth') required String dateOfBirth, + @JsonKey(name: 'client_id') required String clientId, + required String provider, + required String id, required String email, required String phone, - @JsonKey(name: 'date_of_birth') required String dateOfBirth, required String gender, required int weight, required int height, 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 7b58a4b0..6f1a59f5 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -25,19 +25,4 @@ class AuthCubit extends Cubit { return false; } } - - Future getTokenBySocial({required String provider, required String accessToken}) async { - emit(const AuthState.loading()); - try { - final token = (await authRepository.getTokenBySocial(provider: provider, accessToken: accessToken)).token; - emit(AuthState.loaded(token: token)); - await TokenStorage.saveTokenInStorage(token); - return true; - } catch (e) { - log(e.toString()); - showErrorToast('Не удалось войти через $provider'); - emit(AuthState.error(errorMessage: '$e')); - return false; - } - } } diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart index 7d45b4b1..394abc4d 100644 --- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -8,24 +9,27 @@ class OAuthCubit extends Cubit { OAuthCubit({required this.authRepository}) : super(const OAuthState()); - Future getOAuthLogin({required String provider, required String accessToken, required String refreshToken}) async { + Future signInViaSocialMedia({required String provider, required String accessToken, required String refreshToken, required String clientId}) async { try { - log('OAuthCubit: Starting OAuth login for provider: $provider'); emit(const OAuthState.loading()); - log('OAuthCubit: Emitted loading state'); - final tokenModel = await authRepository.getOAuthLogin(provider: provider, accessToken: accessToken, refreshToken: refreshToken); + final tokenModel = await authRepository.signInViaSocialMedia( + provider: provider, + accessToken: accessToken, + refreshToken: refreshToken, + clientId: clientId, + ); + log('OAuthCubit: Got token model: ${tokenModel.token}'); + TokenStorage.saveTokenInStorage(tokenModel.token); emit(OAuthState.loaded(token: tokenModel.token)); - log('OAuthCubit: Emitted loaded state'); } catch (e) { log('OAuthCubit: Error occurred: $e'); emit(OAuthState.error(errorMessage: e.toString())); - log('OAuthCubit: Emitted error state'); } } - Future getOAuthToken({ + Future signUpViaSocialMedia({ required String provider, required String accessToken, required String id, @@ -38,10 +42,11 @@ class OAuthCubit extends Cubit { required String gender, required int weight, required int height, + required String clientId, }) async { try { emit(const OAuthState.loading()); - final token = await authRepository.getOAuthToken( + final token = await authRepository.signUpViaSocialMedia( provider: provider, accessToken: accessToken, id: id, @@ -54,7 +59,9 @@ class OAuthCubit extends Cubit { gender: gender, weight: weight, height: height, + clientId: clientId, ); + TokenStorage.saveTokenInStorage(token); emit(OAuthState.loaded(token: token)); } catch (e) { emit(OAuthState.error(errorMessage: e.toString())); diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index c21e77ea..f7484a87 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -1,5 +1,3 @@ -// ignore_for_file: unused_element, duplicate_ignore - import 'dart:async'; import 'dart:developer'; import 'package:eatvio_app/app_keys.dart'; @@ -95,6 +93,138 @@ class _AuthScreenState extends State { AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); } + void _onVkPressed() async { + try { + log('VK button pressed'); + final context = AppRouter.navigatorKey.currentContext; + if (context == null) { + log('Context is null'); + return; + } + + log('Getting VK auth result'); + final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context); + if (authResult == null) { + log('VK auth result is null'); + return; + } + + log('VK auth result received, trying OAuth login'); + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'vk'); + } else { + log('Context is not mounted'); + } + } catch (e) { + log('VK button error: $e'); + } + } + + void _onOkPressed() async { + try { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile(); + if (authResult == null) { + return; + } + + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'vk'); + } + } catch (e) { + log(e.toString()); + } + } + + void _onMailRuPressed() async { + try { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile(); + if (authResult == null) { + return; + } + + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'vk'); + } + } catch (e) { + log(e.toString()); + } + } + + void _onYandexPressed() async { + try { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithYandex(); + if (authResult == null) return; + + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'yandex'); + } + } catch (e) { + log(e.toString()); + } + } + + Future _tryOAuthLogin(BuildContext context, AuthResultEntity authResult, String provider) async { + log('Starting OAuth login for provider: $provider'); + + try { + final oauthCubit = CubitRegistrations.sl(); + StreamSubscription? subscription; + + subscription = oauthCubit.stream.listen((state) { + log('OAuth state changed: ${state.runtimeType}'); + + if (state is OAuthStateLoaded) { + log('OAuth login successful, navigating to main screen'); + subscription?.cancel(); + _reconnectAfterAuth(); + } else if (state is OAuthStateError) { + log('OAuth login failed: ${state.errorMessage}'); + subscription?.cancel(); + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); + } + }); + + log('Calling getOAuthLogin with provider: $provider'); + await oauthCubit.signInViaSocialMedia( + provider: provider, + accessToken: authResult.accessToken, + refreshToken: authResult.refreshToken ?? '', + clientId: authResult.clientId, + ); + } catch (e) { + log('OAuth login error in catch block: $e'); + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); + } + } + + Future _reconnectAfterAuth() async { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + try { + await sl().clear(); + await sl().initialize(); + await sl().getMyProfile(); + await sl().getChatRoomsWithLoading(); + await sl().connectToWebSocket(); + await sl().getNotifications(); + await sl().connectToWebSocket(); + await sl().registerPushToken(); + showSuccessToast('Успешный вход'); + } catch (e) { + log(e.toString()); + } + AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); + } + @override Widget build(BuildContext context) { var shortestSide = MediaQuery.sizeOf(context).shortestSide; @@ -129,7 +259,6 @@ class _AuthScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Social auth buttons at the top (outlined) const Center(child: Text('Авторизация через')), const SizedBox(height: 12), Row( @@ -405,143 +534,3 @@ class _SocialButton extends StatelessWidget { ); } } - -void _onVkPressed() async { - try { - log('VK button pressed'); - final context = AppRouter.navigatorKey.currentContext; - if (context == null) { - log('Context is null'); - return; - } - - log('Getting VK auth result'); - final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context); - if (authResult == null) { - log('VK auth result is null'); - return; - } - - log('VK auth result received, trying OAuth login'); - // Пробуем сначала авторизоваться через новый API - if (context.mounted) { - await _tryOAuthLogin(context, authResult, 'vk'); - } else { - log('Context is not mounted'); - } - } catch (e) { - log('VK button error: $e'); - } -} - -void _onOkPressed() async { - try { - final context = AppRouter.navigatorKey.currentContext; - if (context == null) return; - - final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile(); - if (authResult == null) { - return; - } - - // Пробуем сначала авторизоваться через новый API - if (context.mounted) { - await _tryOAuthLogin(context, authResult, 'ok'); - } - } catch (e) { - log(e.toString()); - } -} - -void _onMailRuPressed() async { - try { - final context = AppRouter.navigatorKey.currentContext; - if (context == null) return; - - final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile(); - if (authResult == null) { - return; - } - - // Пробуем сначала авторизоваться через новый API - if (context.mounted) { - await _tryOAuthLogin(context, authResult, 'mailru'); - } - } catch (e) { - log(e.toString()); - } -} - -void _onYandexPressed() async { - try { - final context = AppRouter.navigatorKey.currentContext; - if (context == null) return; - - final authResult = await SocialAuthFacade.signInWithYandex(); - if (authResult == null) return; - - // Пробуем сначала авторизоваться через новый API - if (context.mounted) { - await _tryOAuthLogin(context, authResult, 'yandex'); - } - } catch (e) { - log(e.toString()); - } -} - -/// Пробует авторизоваться через новый OAuth login API -/// Если получает ошибку (включая 422 - пользователь не найден), переходит к регистрации -Future _tryOAuthLogin(BuildContext context, AuthResultEntity authResult, String provider) async { - log('Starting OAuth login for provider: $provider'); - - try { - // Создаем OAuth cubit - final oauthCubit = CubitRegistrations.sl(); - StreamSubscription? subscription; - - // Слушаем изменения состояния - subscription = oauthCubit.stream.listen((state) { - log('OAuth state changed: ${state.runtimeType}'); - - if (state is OAuthStateLoaded) { - // Успешная авторизация - переходим на главный экран - log('OAuth login successful, navigating to main screen'); - subscription?.cancel(); - _reconnectAfterAuth(); - } else if (state is OAuthStateError) { - // Ошибка авторизации (включая 422 - пользователь не найден) - переходим к регистрации - log('OAuth login failed: ${state.errorMessage}'); - subscription?.cancel(); - AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); - } - }); - - // Пробуем авторизоваться - log('Calling getOAuthLogin with provider: $provider'); - await oauthCubit.getOAuthLogin(provider: provider, accessToken: authResult.accessToken, refreshToken: authResult.refreshToken ?? ''); - - // Ждем немного, чтобы состояние успело обновиться - await Future.delayed(const Duration(milliseconds: 100)); - } catch (e) { - // В случае любой ошибки (включая 422 - пользователь не найден) переходим к регистрации - log('OAuth login error in catch block: $e'); - AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); - } -} - -Future _reconnectAfterAuth() async { - final context = AppRouter.navigatorKey.currentContext; - if (context == null) return; - try { - await sl().clear(); - await sl().initialize(); - await sl().getMyProfile(); - await sl().getChatRoomsWithLoading(); - await sl().connectToWebSocket(); - await sl().getNotifications(); - await sl().connectToWebSocket(); - await sl().registerPushToken(); - showSuccessToast('Успешный вход'); - } catch (_) {} - AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); -} diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index c803f687..84a6f2ae 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -439,6 +439,7 @@ class _RegisterViaSocialScreenState extends State { } void _handleRegistration(BuildContext context) { + final clientID = widget.authResult?.clientId; // Валидация полей if (firstNameController.text.isEmpty || lastNameController.text.isEmpty || @@ -465,8 +466,9 @@ class _RegisterViaSocialScreenState extends State { // Очищаем номер телефона от маски для отправки final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), ''); + if (clientID == null) return; // Вызываем OAuth регистрацию - context.read().getOAuthToken( + context.read().signUpViaSocialMedia( provider: widget.provider ?? 'social', accessToken: widget.authResult?.accessToken ?? 'dummy_token', id: widget.authResult?.userId ?? 'dummy_id', @@ -479,6 +481,7 @@ class _RegisterViaSocialScreenState extends State { gender: isMaleSelected ? 'М' : 'Ж', weight: weight, height: height, + clientId: clientID, ); } } diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index 1f66c039..a65f309c 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -3,7 +3,6 @@ import 'package:eatvio_app/features/auth/models/oauth_model.dart'; import 'package:eatvio_app/features/auth/models/oauth_login_request.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; -import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; class AuthRepository { final AuthService authService; @@ -20,27 +19,23 @@ class AuthRepository { } } - Future getTokenBySocial({required String provider, required String accessToken}) async { + + Future signInViaSocialMedia({ + required String provider, + required String accessToken, + required String refreshToken, + required String clientId, + }) async { try { - return await authService.getTokenBySocial( - body: SocialExchangeRequest(provider: provider, accessToken: accessToken), + return await authService.signInViaSocialMedia( + body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId), ); } catch (e) { rethrow; } } - Future getOAuthLogin({required String provider, required String accessToken, required String refreshToken}) async { - try { - return await authService.getOAuthLogin( - body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken), - ); - } catch (e) { - rethrow; - } - } - - Future getOAuthToken({ + Future signUpViaSocialMedia({ required String provider, required String accessToken, required String id, @@ -53,9 +48,10 @@ class AuthRepository { required String gender, required int weight, required int height, + required String clientId, }) async { try { - return (await authService.getOAuthToken( + return (await authService.signUpViaSocialMedia( body: OAuthModel( provider: provider, accessToken: accessToken, @@ -69,6 +65,7 @@ class AuthRepository { gender: gender == 'М' ? 'male' : 'female', weight: weight, height: height, + clientId: clientId, ), ))['token']! as String; diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index b3543700..4d2e9745 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -8,6 +8,7 @@ import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart'; import 'package:flutter/material.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/env_config.dart'; +import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; import 'package:vk_id/vk_id.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -38,7 +39,7 @@ class SocialAuthFacade { if (redirectParams == null) { return null; } - return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier); + return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr); } catch (e) { return null; } @@ -48,6 +49,7 @@ class SocialAuthFacade { required Map redirectParams, required VkIDController controller, required String codeVerifier, + required String clientId, }) async { final code = redirectParams['code']; final deviceId = redirectParams['device_id']; @@ -88,6 +90,7 @@ class SocialAuthFacade { phone: profile.phone, gender: GenderEnum.fromVkGender(profile.parsedSex), birthDate: profile.bDayDt?.toString(), + clientId: clientId, ); } @@ -151,6 +154,7 @@ class SocialAuthFacade { phone: yandexProfileData.defaultPhone.number, gender: GenderEnum.fromString(yandexProfileData.sex), birthDate: yandexProfileData.birthday, + clientId: clientId, ); } catch (e) { log(e.toString()); @@ -188,7 +192,7 @@ class SocialAuthFacade { try { final controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email,]); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email]); final codeVerifier = authUri.key; final authUrl = authUri.value.toString(); final redirectScheme = 'vk$clientIdStr://'; @@ -199,7 +203,7 @@ class SocialAuthFacade { final deviceId = redirectParams['device_id']; if (code == null || deviceId == null) return null; - return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier); + return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr); } catch (e) { return null; } @@ -230,7 +234,9 @@ class SocialAuthFacade { } } } - Navigator.of(context, rootNavigator: true).pop(params); + if (GoRouter.of(context).canPop()) { + GoRouter.of(context).pop(params); + } return NavigationDecision.prevent; } return NavigationDecision.navigate; diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 02f8722a..41d35da7 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -3,7 +3,6 @@ import 'package:eatvio_app/features/auth/models/credentials_model.dart'; import 'package:eatvio_app/features/auth/models/oauth_model.dart'; import 'package:eatvio_app/features/auth/models/oauth_login_request.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; -import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; import 'package:retrofit/retrofit.dart'; part 'auth_service.g.dart'; @@ -15,12 +14,9 @@ abstract class AuthService { @POST('/api/login') Future getTokenByCredentials({@Body() required CredentialsModel credentials}); - @POST('/api/social/login') - Future getTokenBySocial({@Body() required SocialExchangeRequest body}); - @POST('/api/oauth/callback') - Future> getOAuthToken({@Body() required OAuthModel body}); + Future> signUpViaSocialMedia({@Body() required OAuthModel body}); @POST('/api/oauth/login') - Future getOAuthLogin({@Body() required OAuthLoginRequest body}); + Future signInViaSocialMedia({@Body() required OAuthLoginRequest body}); } diff --git a/lib/main.dart b/lib/main.dart index af6db638..b6171864 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,7 +9,7 @@ import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; Future main() async { - await AppInitializer.initialize(buildType: AppBuildType.prod); + await AppInitializer.initialize(buildType: AppBuildType.dev); await AppInitializer.runAppWithSentry(() => runApp(const MyApp())); } From b242eaa027bf998730e38b987fac2b3c44002a5d Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 15 Oct 2025 14:22:45 +0300 Subject: [PATCH 46/49] fix: fixed base url --- lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index b6171864..af6db638 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,7 +9,7 @@ import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; Future main() async { - await AppInitializer.initialize(buildType: AppBuildType.dev); + await AppInitializer.initialize(buildType: AppBuildType.prod); await AppInitializer.runAppWithSentry(() => runApp(const MyApp())); } From e865a11d149a688eea8143082d06c45629408ce5 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 15 Oct 2025 14:25:27 +0300 Subject: [PATCH 47/49] fix: fixed proj version --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- ios/Runner/Info.plist | 2 +- lib/generated/l10n.dart | 7 ++++--- pubspec.yaml | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8e9edd22..2fa79910 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = 74; + CURRENT_PROJECT_VERSION = 79; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -539,7 +539,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 79; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -578,7 +578,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 79; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 3275dbdb..2b41fbe1 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 74 + 79 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 0ccdeac3..1055e664 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,9 +28,10 @@ class S { static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { - final name = (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); + final name = + (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; diff --git a/pubspec.yaml b/pubspec.yaml index 0891053c..1a4b1db0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.14+74 +version: 1.3.0+79 publish_to: 'none' environment: From feee814bbb8630bc148a0b44a031da99c1736e37 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 15 Oct 2025 14:43:21 +0300 Subject: [PATCH 48/49] pod repod update for ci cd --- .gitea/workflows/test-flight-cd.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index c9649f0e..e9d5163b 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -61,6 +61,7 @@ jobs: # Configure Flutter for release mode flutter build ios --config-only --release cd ios + pod repo update pod update fastlane beta env: From 05bf90a3cf11d3abeaab7da844507a8177ee641d Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 15 Oct 2025 14:47:46 +0300 Subject: [PATCH 49/49] fixed pipline --- .gitea/workflows/test-flight-cd.yaml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index e9d5163b..4736a47f 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -1,4 +1,4 @@ -name: "Build Test Flight" +name: 'Build Test Flight' on: pull_request: branches: @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - + - name: Checkout env repository uses: actions/checkout@v2 with: @@ -39,8 +39,8 @@ jobs: run: | security unlock-keychain -p "$KEYCHAIN_PASSWORD" /Users/gitea_runner_ci/Library/Keychains/login.keychain-db env: - KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} - + KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} + - name: Setup API key run: | echo "${API_APPSTORE_KEY}" | tr -d '\n' | base64 --decode > ios/AuthKey.p8 @@ -58,18 +58,19 @@ jobs: flutter pub get flutter pub upgrade flutter pub run build_runner build --delete-conflicting-outputs - # Configure Flutter for release mode - flutter build ios --config-only --release cd ios pod repo update pod update + # Configure Flutter for release mode + flutter build ios --config-only --release + fastlane beta env: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 - MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} - FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }} + MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} + FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }} MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane FASTLANE_USER: eatvio2023@icloud.com - FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}