From 335e36dd180f296c463f956d210d3c79e52036b4 Mon Sep 17 00:00:00 2001 From: misha Date: Thu, 26 Mar 2026 18:06:20 +0300 Subject: [PATCH 01/18] feat(pushNotifications): fixed push notifications navigation --- lib/core/app_router.dart | 240 +++++++++++------- .../push_navigation_handler.dart | 117 +-------- lib/core/routing/main_navigation_routes.dart | 20 +- lib/core/routing/other_routes.dart | 11 +- .../chat/presentation/pages/chat_screen.dart | 46 +++- .../feed/presentation/pages/feed_screen.dart | 21 +- .../presentation/pages/profile_screen.dart | 28 +- lib/main.dart | 3 +- 8 files changed, 251 insertions(+), 235 deletions(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 8e2c94a8..6b5808f5 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,4 +1,5 @@ import 'dart:developer' as developer; +import 'package:eatvio_app/core/const.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'; @@ -14,8 +15,8 @@ import 'package:go_router/go_router.dart'; class AppRouter { static GlobalKey navigatorKey = GlobalKey(); - // Домены для deep links (в т.ч. ссылки из пушей dev2.eatvio.com) - static const List deepLinkHosts = ['eatvio.ru', 'eatvio.com']; + static const List deepLinkHosts = ['eatvio.ru', 'eatvio.com', 'dev2.eatvio.com']; + static const List metaSegments = ['ru', 'en', 'api', 'v2']; static GoRouter appRouter = GoRouter( initialLocation: RouteConstants.updateScreen, @@ -30,11 +31,111 @@ class AppRouter { ], ); + static String? _resolveRecipeDeepLink(List effectiveSegments) { + if (effectiveSegments.length != 2) return null; + final contentIdentifier = effectiveSegments[1]; + developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return '/about-recipes-screen?slug=$contentIdentifier'; + } + + static String? _resolveBlogPostDeepLink(List effectiveSegments) { + if (effectiveSegments.length != 2) return null; + final contentIdentifier = effectiveSegments[1]; + developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return '/blog-post-screen?id=$contentIdentifier'; + } + + static String? _resolvePlanDeepLink(List effectiveSegments) { + if (effectiveSegments.length != 2) return null; + final contentIdentifier = effectiveSegments[1]; + final planId = int.tryParse(contentIdentifier); + if (planId != null) { + developer.log('Deep link: diary', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return RouteConstants.diaryScreen; + } + developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return '/about-plan-screen?slug=$contentIdentifier'; + } + + static String? _resolveDietDeepLink(List effectiveSegments) { + if (effectiveSegments.length == 2) { + final contentIdentifier = effectiveSegments[1]; + final dietId = int.tryParse(contentIdentifier); + if (dietId != null) { + developer.log('Deep link: diet/id=$dietId', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return '/about-diet-screen?id=$dietId'; + } + developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return '/about-diet-screen?slug=$contentIdentifier'; + } + final initialTabIndex = hidePlans ? 3 : 4; + return '${RouteConstants.feedScreen}?initialTabIndex=$initialTabIndex'; + } + + static String? _resolveChatDeepLink(List effectiveSegments) { + if (effectiveSegments.length == 2) { + final contentIdentifier = effectiveSegments[1]; + developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return '${RouteConstants.chatScreen}?uuid=$contentIdentifier'; + } else if (effectiveSegments.length == 3 && effectiveSegments[1] == 'show') { + final contentIdentifier = effectiveSegments[2]; + developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return '${RouteConstants.chatScreen}?uuid=$contentIdentifier'; + } + + developer.log('Deep link: chat', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return RouteConstants.chatSelectScreen; + } + + static String? _resolveProfileDeepLink(List effectiveSegments) { + if (effectiveSegments.length == 2) { + final contentIdentifier = effectiveSegments[1]; + developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return '/profile-screen?slug=$contentIdentifier'; + } + return RouteConstants.myProfileScreen; + } + + static String? _resolveBadgeDeepLink(List effectiveSegments) { + return RouteConstants.myProfileScreen; + } + + static String? _resolveDiaryDeepLink(List effectiveSegments) { + developer.log('Deep link: diary', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return RouteConstants.diaryScreen; + } + + static String? _resolveConsultantDiaryCommentDeepLink(List effectiveSegments) { + if (effectiveSegments.length != 2) return null; + final contentIdentifier = effectiveSegments[1]; + developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return RouteConstants.diaryScreen; + } + + static String? _resolveConsultationsDeepLink(List effectiveSegments) { + if (effectiveSegments.length != 3 || effectiveSegments[1] != 'status') return RouteConstants.consultationsScreen; + final consultationId = effectiveSegments[2]; + developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter'); + FlutterNativeSplash.remove(); + return '${RouteConstants.consultationsScreen}?id=$consultationId'; + } + static String? _resolveDeepLinkUri(Uri uri) { final host = uri.host.toLowerCase(); final segments = uri.pathSegments; - // Проверяем, что хост заканчивается на один из разрешенных доменов final isDeepLink = deepLinkHosts.any((allowedHost) => host == allowedHost || host.endsWith('.$allowedHost')); if (!isDeepLink) { @@ -49,26 +150,7 @@ class AppRouter { return RouteConstants.feedScreen; } - // Обработка api ссылок (например, из пушей чата) - if (segments.length >= 4 && - segments[0] == 'api' && - segments[1] == 'v2' && - segments[2] == 'chat' && - segments[3] == 'show') { - developer.log('Deep link: api/v2/chat/show → chat-select-screen', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return RouteConstants.chatSelectScreen; - } - - // Определяем, есть ли в начале сегмент языка (ru/en). Если есть — пропускаем его. - int startIndex = 0; - if (segments[0] == 'ru' || segments[0] == 'en') { - startIndex = 1; - } else { - developer.log('Deep link without language segment: ${segments[0]}', name: 'AppRouter'); - } - - final effectiveSegments = segments.sublist(startIndex); + final effectiveSegments = segments.where((segment) => !metaSegments.contains(segment)).toList(); if (effectiveSegments.isEmpty) { developer.log('No effective segments after language check, redirecting to feed', name: 'AppRouter'); FlutterNativeSplash.remove(); @@ -77,76 +159,47 @@ class AppRouter { final type = effectiveSegments[0]; - if (effectiveSegments.length == 3 && type == 'consultations' && effectiveSegments[1] == 'status') { - final consultationId = effectiveSegments[2]; - developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return '${RouteConstants.consultationsScreen}?id=$consultationId'; - } - - if (effectiveSegments.length == 2) { - final contentIdentifier = effectiveSegments[1]; - - switch (type) { - case 'recipe': - developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return '/about-recipes-screen?slug=$contentIdentifier'; - case 'blog_post': - developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return '/blog-post-screen?id=$contentIdentifier'; - case 'plan': - developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return '/about-plan-screen?slug=$contentIdentifier'; - case 'diet': - { - final dietId = int.tryParse(contentIdentifier); - if (dietId != null) { - developer.log('Deep link: diet/id=$dietId', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return '/about-diet-screen?id=$dietId'; - } - developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return '/about-diet-screen?slug=$contentIdentifier'; - } - case 'chat': - developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return RouteConstants.chatSelectScreen; - case 'profile': - developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return '/profile-screen?slug=$contentIdentifier'; - case 'diary': - developer.log('Deep link: diary/$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return RouteConstants.diaryScreen; - case 'consultant_diary_comment': - developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return RouteConstants.diaryScreen; - } - } - - if (effectiveSegments.length == 1) { - if (type == 'chat') { - developer.log('Deep link: chat', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return RouteConstants.chatSelectScreen; - } - if (type == 'diary') { - developer.log('Deep link: diary', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return RouteConstants.diaryScreen; - } - if (type == 'badge') { - developer.log('Deep link: badge → my-profile tab', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return RouteConstants.myProfileScreen; - } + switch (type) { + case 'recipe': + final target = _resolveRecipeDeepLink(effectiveSegments); + if (target != null) return target; + break; + case 'blog_post': + final target = _resolveBlogPostDeepLink(effectiveSegments); + if (target != null) return target; + break; + case 'plan': + final target = _resolvePlanDeepLink(effectiveSegments); + if (target != null) return target; + break; + case 'diet': + final target = _resolveDietDeepLink(effectiveSegments); + if (target != null) return target; + break; + case 'chat': + final target = _resolveChatDeepLink(effectiveSegments); + if (target != null) return target; + break; + case 'profile': + final target = _resolveProfileDeepLink(effectiveSegments); + if (target != null) return target; + break; + case 'badge': + final target = _resolveBadgeDeepLink(effectiveSegments); + if (target != null) return target; + break; + case 'diary': + final target = _resolveDiaryDeepLink(effectiveSegments); + if (target != null) return target; + break; + case 'consultant_diary_comment': + final target = _resolveConsultantDiaryCommentDeepLink(effectiveSegments); + if (target != null) return target; + break; + case 'consultations': + final target = _resolveConsultationsDeepLink(effectiveSegments); + if (target != null) return target; + break; } developer.log('Unknown deep link format: ${effectiveSegments.length} effective segments, type=$type, redirecting to feed', name: 'AppRouter'); @@ -190,7 +243,6 @@ class AppRouter { } final target = _resolveDeepLinkUri(uri); - if (target != null) { appRouter.go(target); } else { diff --git a/lib/core/notifications/push_navigation_handler.dart b/lib/core/notifications/push_navigation_handler.dart index 1fdd236b..a8d1d5d0 100644 --- a/lib/core/notifications/push_navigation_handler.dart +++ b/lib/core/notifications/push_navigation_handler.dart @@ -1,23 +1,18 @@ import 'dart:developer' as developer; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -/// Отвечает за навигацию по диплинкам, пришедшим в пуш-уведомлениях. class PushNavigationHandler { static bool _initialized = false; static const MethodChannel _hmsChannel = MethodChannel('com.eatvioapp/hms_push'); - /// Инициализация обработчиков Firebase Messaging. - /// Должна вызываться один раз после старта приложения. static Future initialize() async { if (_initialized) return; _initialized = true; - // Сообщение, по которому запустили приложение из "убитого" состояния. try { final initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { @@ -26,18 +21,11 @@ class PushNavigationHandler { }); } } catch (e) { - developer.log( - 'PushNavigationHandler: error while getting initialMessage: $e', - name: 'PushNavigationHandler', - ); + developer.log('PushNavigationHandler: error while getting initialMessage: $e', name: 'PushNavigationHandler'); } - // Тап по уведомлению, когда приложение было в фоне. - FirebaseMessaging.onMessageOpenedApp.listen( - (message) => _handleMessageInternal(message, source: 'onMessageOpenedApp'), - ); + FirebaseMessaging.onMessageOpenedApp.listen((message) => _handleMessageInternal(message, source: 'onMessageOpenedApp')); - // Подписка на события от native-кода (HMS через MethodChannel). _hmsChannel.setMethodCallHandler((call) async { if (call.method == 'openActionUrl') { final url = call.arguments as String? ?? ''; @@ -46,118 +34,31 @@ class PushNavigationHandler { }); } - /// Входная точка для навигации из native-кода (например, HMS через MethodChannel). static void handleActionUrlFromNative(String url) { if (url.isEmpty) { - developer.log( - 'PushNavigationHandler.handleActionUrlFromNative: empty url', - name: 'PushNavigationHandler', - ); + developer.log('PushNavigationHandler.handleActionUrlFromNative: empty url', name: 'PushNavigationHandler'); return; } - developer.log( - 'PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url', - name: 'PushNavigationHandler', - ); + developer.log('PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url', name: 'PushNavigationHandler'); AppRouter.navigateByDeepLinkUrl(url); } - /// Публичный метод для единичной обработки сообщения (если понадобится где-то ещё). static void handleMessage(RemoteMessage message) { _handleMessageInternal(message, source: 'manualHandle'); } static void _handleMessageInternal(RemoteMessage message, {required String source}) { - developer.log( - 'PushNavigationHandler: received message. data=${message.data}, notification=${message.notification?.title}, source=$source', - name: 'PushNavigationHandler', - ); - final data = message.data; - - // Пытаемся взять URL из action_url или action (бэкенд шлёт и то и другое) - String? rawUrl = data['action_url'] as String?; - if (rawUrl == null || rawUrl.isEmpty) { - rawUrl = data['action'] as String?; - } - - if (rawUrl != null && rawUrl.isNotEmpty) { - developer.log( - 'PushNavigationHandler: manual navigation by url=$rawUrl, source=$source', - name: 'PushNavigationHandler', - ); - - // Если это URL, но без схемы, добавляем https - String finalUrl = rawUrl; - if (!finalUrl.startsWith('http') && finalUrl.contains('/')) { - finalUrl = 'https://${AppRouter.deepLinkHosts.first}/$finalUrl'.replaceAll('//', '/').replaceFirst('https:/', 'https://'); - } - - AppRouter.navigateByDeepLinkUrl(finalUrl); + final actionUrl = data['action_url'] as String?; + if (actionUrl == null || actionUrl.isEmpty) { + developer.log('PushNavigationHandler: no action_url in message data, source=$source, data=$data', name: 'PushNavigationHandler'); return; } - // Fallback: навигация по типу уведомления, если URL отсутствует. - final target = _resolveTargetByType(data); - if (target != null) { - developer.log( - 'PushNavigationHandler: fallback navigation by type, target=$target, source=$source, data=$data', - name: 'PushNavigationHandler', - ); - AppRouter.appRouter.go(target); - return; - } + developer.log('PushNavigationHandler: navigating by action_url=$actionUrl, source=$source', name: 'PushNavigationHandler'); - developer.log( - 'PushNavigationHandler: no action_url/action and no known type, source=$source, data=$data', - name: 'PushNavigationHandler', - ); - } - - /// Определяет маршрут по полям `type` / `action` из data, если URL отсутствует. - static String? _resolveTargetByType(Map data) { - final type = data['type'] as String? ?? ''; - final action = data['action'] as String? ?? ''; - final databaseType = data['database_type'] as String? ?? ''; - - // Чат - if (type == 'chat_message' || databaseType == 'chat-message') { - return RouteConstants.chatSelectScreen; - } - - // Дневник - if (type == 'diary_reminder' || action == 'open_diary' || databaseType == 'diary-reminder') { - return RouteConstants.diaryScreen; - } - - // Комментарий консультанта к дневнику - if (type == 'consultant_diary_comment' || databaseType == 'consultant-left-diary-comment') { - return RouteConstants.diaryScreen; - } - - // Консультации (plan-related) - if (type.startsWith('plan_') || type.startsWith('chosen_plan_') || databaseType.contains('plan')) { - return RouteConstants.consultationsScreen; - } - - // Диета - if (type == 'diet_started' || type == 'diet_cancelled' || databaseType.startsWith('diet-')) { - return RouteConstants.feedScreen; - } - - // Бейдж - if (type == 'badge_awarded' || databaseType == 'badge-awarded') { - return RouteConstants.myProfileScreen; - } - - // Мотивация - if (type == 'motivation') { - return RouteConstants.feedScreen; - } - - return null; + AppRouter.navigateByDeepLinkUrl(actionUrl); } } - diff --git a/lib/core/routing/main_navigation_routes.dart b/lib/core/routing/main_navigation_routes.dart index 2557ddcf..5875f7e1 100644 --- a/lib/core/routing/main_navigation_routes.dart +++ b/lib/core/routing/main_navigation_routes.dart @@ -33,10 +33,7 @@ class MainNavigationRoutes { }); } - return _MainShellWithNavScope( - currentLocation: state.matchedLocation, - child: child, - ); + return _MainShellWithNavScope(currentLocation: state.matchedLocation, child: child); }, routes: [ GoRoute( @@ -49,7 +46,10 @@ class MainNavigationRoutes { GoRoute( path: RouteConstants.feedScreen, name: 'feedScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()), + pageBuilder: (context, state) { + final initialTabIndex = int.tryParse(state.uri.queryParameters['initialTabIndex'] ?? '0'); + return NoTransitionPage(child: FeedScreen(initialTabIndex: initialTabIndex ?? 0)); + }, redirect: (context, state) async { try { await sl().initialize(); @@ -105,10 +105,7 @@ class MainNavigationRoutes { /// Wraps the shell with [BottomNavScope] so descendants (e.g. notifications) /// can switch tabs via [BottomNavController.switchToRoute] instead of pushing. class _MainShellWithNavScope extends StatefulWidget { - const _MainShellWithNavScope({ - required this.child, - required this.currentLocation, - }); + const _MainShellWithNavScope({required this.child, required this.currentLocation}); final Widget child; final String currentLocation; @@ -124,10 +121,7 @@ class _MainShellWithNavScopeState extends State<_MainShellWithNavScope> { Widget build(BuildContext context) { return BottomNavScope( controller: _navController, - child: BottomNavigationBarWidget( - currentLocation: widget.currentLocation, - child: widget.child, - ), + child: BottomNavigationBarWidget(currentLocation: widget.currentLocation, child: widget.child), ); } } diff --git a/lib/core/routing/other_routes.dart b/lib/core/routing/other_routes.dart index 1b03ce4c..9d5200e3 100644 --- a/lib/core/routing/other_routes.dart +++ b/lib/core/routing/other_routes.dart @@ -156,10 +156,7 @@ class OtherRoutes { builder: (context, state) { if (state.extra is Map) { final data = state.extra as Map; - return RecipeCreateScreen( - recipe: data['recipe'] as Recipe?, - autoValidate: data['autoValidate'] as bool? ?? false, - ); + return RecipeCreateScreen(recipe: data['recipe'] as Recipe?, autoValidate: data['autoValidate'] as bool? ?? false); } return RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null); }, @@ -212,6 +209,10 @@ class OtherRoutes { path: RouteConstants.chatScreen, name: 'chatScreen', builder: (context, state) { + final uuidFromQuery = state.uri.queryParameters['uuid']; + if (uuidFromQuery != null) { + return ChatScreen(uuid: uuidFromQuery, sendInitialMessage: false); + } final data = state.extra as Map; final uuid = data['uuid'] as String; final sendInitialMessage = data['sendInitialMessage'] as bool?; @@ -233,7 +234,7 @@ class OtherRoutes { final data = state.extra as Map; final slug = data['slug'] as String?; final isCanceled = data['isCanceled'] as bool?; - return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled,); + return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled); }, ), GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index 8f5e49d2..f5e4105b 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; @@ -96,11 +97,34 @@ class _ChatScreenState extends State { return _buildChat(context, state); } else if (state is ChatRoomStateError) { return Scaffold( - appBar: AppBar(), + appBar: AppBar( + leading: BackButton( + onPressed: () { + if (context.canPop()) { + GoRouter.of(context).pop(); + return; + } + GoRouter.of(context).go(RouteConstants.chatSelectScreen); + }, + ), + ), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)), ); } else { - return const Scaffold(body: Loader()); + return Scaffold( + body: const Loader(), + appBar: AppBar( + leading: BackButton( + onPressed: () { + if (context.canPop()) { + GoRouter.of(context).pop(); + return; + } + GoRouter.of(context).go(RouteConstants.chatSelectScreen); + }, + ), + ), + ); } }, ), @@ -148,8 +172,18 @@ class _ChatScreenState extends State { resizeToAvoidBottomInset: true, appBar: AppBar( titleSpacing: 0, + leading: BackButton( + onPressed: () { + if (context.canPop()) { + GoRouter.of(context).pop(); + return; + } + GoRouter.of(context).go(RouteConstants.chatSelectScreen); + }, + ), backgroundColor: appTheme.gray100, centerTitle: false, + title: GestureDetector( onTap: () { GoRouter.of(context).pushNamed('profileScreen', extra: state.chatRoom.users.receiver.slug); @@ -224,7 +258,7 @@ class _ChatScreenState extends State { final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId; final message = state.chatMessages[index]; final isPlanOrderMessage = message.type == 'plan_order'; - + PacketOptions? messageOption; if (isPlanOrderMessage && chosenPlan != null) { if (chosenPlan.optionType == 'options1') { @@ -235,7 +269,7 @@ class _ChatScreenState extends State { messageOption = chosenPlan.plan.packet3Options; } } - + return Column( children: [ _messageDate(message.updatedAt), @@ -284,7 +318,7 @@ class _ChatScreenState extends State { final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId; final message = state.chatMessages[index]; final isPlanOrderMessage = message.type == 'plan_order'; - + PacketOptions? messageOption; if (isPlanOrderMessage && chosenPlan != null) { if (chosenPlan.optionType == 'options1') { @@ -295,7 +329,7 @@ class _ChatScreenState extends State { messageOption = chosenPlan.plan.packet3Options; } } - + return Column( children: [ if (isPlanOrderMessage && chosenPlan != null && messageOption != null) diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index dfc3e9d4..f8c47cf5 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -32,8 +32,8 @@ import 'package:go_router/go_router.dart'; import '../widgets/recipes.dart'; class FeedScreen extends StatelessWidget { - const FeedScreen({super.key}); - + const FeedScreen({super.key, this.initialTabIndex = 0}); + final int initialTabIndex; @override Widget build(BuildContext context) { return MultiBlocProvider( @@ -44,14 +44,14 @@ class FeedScreen extends StatelessWidget { if (!hidePlans) BlocProvider(create: (context) => sl()..loadPlans()), BlocProvider(create: (context) => sl()..getConsultants()), ], - child: const FeedScreenContent(), + child: FeedScreenContent(initialTabIndex: initialTabIndex), ); } } class FeedScreenContent extends StatefulWidget { - const FeedScreenContent({super.key}); - + const FeedScreenContent({super.key, required this.initialTabIndex}); + final int initialTabIndex; @override State createState() => _FeedScreenContentState(); } @@ -64,7 +64,7 @@ class _FeedScreenContentState extends State { final consultantsController = TextEditingController(); late final List chipsLabelList; - int _selectedChipIndex = 0; + late int _selectedChipIndex; final blogPostsScrollController = ScrollController(); final recipesScrollController = ScrollController(); final dietsScrollController = ScrollController(); @@ -72,6 +72,7 @@ class _FeedScreenContentState extends State { final consultantsScrollController = ScrollController(); @override void initState() { + _selectedChipIndex = widget.initialTabIndex; chipsLabelList = hidePlans ? ['Все', 'Консультанты', 'Рецепты', 'Диеты'] : ['Все', 'Планы питания', 'Консультанты', 'Рецепты', 'Диеты']; consultantsScrollController.addListener(_onScrollConsultants); _checkRegistrationDate(context); @@ -88,6 +89,14 @@ class _FeedScreenContentState extends State { super.initState(); } + @override + void didUpdateWidget(FeedScreenContent oldWidget) { + super.didUpdateWidget(oldWidget); + setState(() { + _selectedChipIndex = widget.initialTabIndex; + }); + } + void _onScrollConsultants() { if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} } diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index f5bf3f94..7d4359f8 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -6,6 +6,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/routing/route_constants.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/bottom_sheet_with_blur.dart'; @@ -122,7 +123,17 @@ class _ProfileScreenState extends State { return state.isMyProfile ? _myProfileScreen(state) : _profileScreen(state); } else if (state is ProfileStateError) { return Scaffold( - appBar: AppBar(), + appBar: AppBar( + leading: BackButton( + onPressed: () { + if (context.canPop()) { + GoRouter.of(context).pop(); + return; + } + GoRouter.of(context).go(RouteConstants.feedScreen); + }, + ), + ), body: DataErrorWidget( onReload: () async { widget.slug == null ? await profileCubit.getMyProfile() : await profileCubit.getProfileBySlug(widget.slug!); @@ -130,7 +141,20 @@ class _ProfileScreenState extends State { ), ); } else { - return Scaffold(appBar: AppBar(), body: const Loader()); + return Scaffold( + appBar: AppBar( + leading: BackButton( + onPressed: () { + if (context.canPop()) { + GoRouter.of(context).pop(); + return; + } + GoRouter.of(context).go(RouteConstants.feedScreen); + }, + ), + ), + body: const Loader(), + ); } }, ), diff --git a/lib/main.dart b/lib/main.dart index 6513e685..3ab5cf6e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,14 +2,15 @@ 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:eatvio_app/core/notifications/push_navigation_handler.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; Future main() async { + WidgetsFlutterBinding.ensureInitialized(); await AppInitializer.initialize(buildType: AppBuildType.prod); await AppInitializer.runAppWithSentry(() => runApp(const MyApp())); } From 55b6b751342583f2599768b184eb646fe7d66448 Mon Sep 17 00:00:00 2001 From: misha Date: Mon, 30 Mar 2026 10:16:13 +0300 Subject: [PATCH 02/18] feat(ci/cd): env repo changed --- .gitea/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 27f86097..230fbf51 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -29,9 +29,8 @@ jobs: - name: Checkout env repository uses: https://github.com/actions/checkout@v3 with: - repository: mso7nueh/eatvio_app_env + repository: eatvio/eatvio_app_env path: env_repo - token: ${{ vars.MSO7NUEH_TOKEN }} ref: main - name: Copy .env file From c450a41715f87b330463972d977374d586e28d4c Mon Sep 17 00:00:00 2001 From: misha Date: Mon, 30 Mar 2026 10:33:46 +0300 Subject: [PATCH 03/18] feat(ci/cd): added token for repo --- .gitea/workflows/test.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 230fbf51..3a600ef5 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -1,17 +1,17 @@ name: Eatvio App Android Test -on: [ pull_request ] +on: [pull_request] env: - flutter_version: "3.41.2" - java_version: "21.x" - flutter_channel: "stable" - my_local_cache_dir: "/opt/hostedtoolcache/" - ANDROID_SDK_ROOT: "/opt/hostedtoolcache/Android/Sdk" + flutter_version: '3.41.2' + java_version: '21.x' + flutter_channel: 'stable' + my_local_cache_dir: '/opt/hostedtoolcache/' + ANDROID_SDK_ROOT: '/opt/hostedtoolcache/Android/Sdk' jobs: build: - runs-on: [ ubuntu-latest ] + runs-on: [ubuntu-latest] steps: - uses: https://github.com/actions/checkout@v3 @@ -31,6 +31,7 @@ jobs: with: repository: eatvio/eatvio_app_env path: env_repo + token: ${{ vars.MISHA_GITEA_TOKEN }} ref: main - name: Copy .env file @@ -65,7 +66,7 @@ jobs: - name: Cache Android SDK id: cache-sdk - uses: "https://github.com/actions/cache@v4" + uses: 'https://github.com/actions/cache@v4' with: path: ${{ env.my_local_cache_dir }} key: ${{ runner.os }}-cache-sdk @@ -73,14 +74,14 @@ jobs: - name: Set up JDK uses: https://github.com/actions/setup-java@v3 with: - distribution: "zulu" + distribution: 'zulu' java-version: ${{ env.java_version }} cache: gradle - name: Install utils for Android SDK run: | apt-get update - apt-get install -y openjdk-17-jdk wget jq clang cmake ninja-build libgtk-3-dev + apt-get install -y openjdk-17-jdk wget jq clang cmake ninja-build libgtk-3-dev - name: Install Android SDK if: steps.cache-sdk.outputs.cache-hit != 'true' @@ -99,15 +100,14 @@ jobs: sdkmanager "platform-tools" "platforms;android-35" "platforms;android-34" "platforms;android-33" "platforms;android-31" sdkmanager "build-tools;34.0.0" sdkmanager "ndk;25.1.8937393" - sdkmanager --install "cmake;3.22.1" + sdkmanager --install "cmake;3.22.1" - name: Set up Android environment variables - id: "android-env-setup" + id: 'android-env-setup' run: | echo "ANDROID_SDK_ROOT=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV echo "PATH=$PATH:${{ env.my_local_cache_dir }}Android/Sdk/cmdline-tools/latest/bin:${{ env.my_local_cache_dir }}Android/Sdk/platform-tools:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/30.0.3:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/34.0.0" >> $GITHUB_ENV - echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV - + echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV - name: Set up Flutter run: | @@ -146,7 +146,7 @@ jobs: if [ "$ARCH" = "x86_64" ]; then ARCH="x64"; fi git config --global --add safe.directory "${{ env.my_local_cache_dir }}flutter" flutter config --android-sdk="$ANDROID_HOME" - flutter doctor -v + flutter doctor -v - name: Get dependencies run: | @@ -235,7 +235,7 @@ jobs: APK_NAME="eatvio_app_${TIMESTAMP}.apk" mv build/app/outputs/flutter-apk/app-release.apk $APK_NAME echo "APK build finished $APK_NAME" - echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV + echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV - name: Upload APK artifact uses: https://github.com/actions/upload-artifact@v3 From ee2dd75a94d44fc8f5c942a4cc0bf688b6042c5c Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 31 Mar 2026 10:20:43 +0300 Subject: [PATCH 04/18] feat(ci/cd): added test coverage verification --- .gitea/workflows/test.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 3a600ef5..38d8541b 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -211,12 +211,11 @@ jobs: percentage = (covered / total) * 100 printf "✅ Coverage: %.2f%%\n", percentage print "" - if (percentage < 60) { - printf "⚠️ Warning: Coverage is below 60%%\n" - } else if (percentage < 80) { - printf "💛 Good: Coverage is above 60%%\n" + if (percentage < 70) { + printf "❌ FAIL: Coverage is below 70%%\n" + exit 1 } else { - printf "💚 Excellent: Coverage is above 80%%\n" + printf "💚 Excellent: Coverage is above 70%%\n" } } } From a13b623ca265598a3948eccbedd27063a26b00ac Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 1 Apr 2026 10:37:59 +0300 Subject: [PATCH 05/18] feat(feed): fixed comments state updating in feed, feat(consultantRegistration): fixed registration flow for users with unverified phone, feat(tests): fixed tests coverage --- lib/core/app_router.dart | 5 +- .../comments_cubit/comments_cubit.dart | 43 +- .../phone_verify_error_interceptor.dart | 22 +- lib/core/routing/main_navigation_routes.dart | 4 +- .../presentation/pages/blog_post_screen.dart | 16 +- .../register_consultant_cubit.dart | 69 +-- .../pages/become_consultant_screen.dart | 23 +- .../bloc/feed_cubit/feed_cubit.dart | 11 + .../feed/presentation/pages/feed_screen.dart | 11 +- .../presentation/cubit/about_plan_cubit.dart | 97 ++-- .../presentation/pages/about_plan_screen.dart | 4 +- test/core/app_router_test.dart | 38 ++ .../pages/blog_post_screen_smoke_test.dart | 49 ++ .../reporitories/recipe_repository_test.dart | 459 ++++++++++++------ .../dev_tools_screen_smoke_test.dart | 26 + .../dev_tools/test_cases_bulk_test.dart | 107 ++++ ...notification_model_fromjson_bulk_test.dart | 121 +++++ .../cubit/about_plan_cubit_test.dart | 38 +- .../pages/about_plan_screen_smoke_test.dart | 92 +++- .../pages/about_recipe_screen_smoke_test.dart | 57 +++ 20 files changed, 949 insertions(+), 343 deletions(-) create mode 100644 test/features/dev_tools/test_cases_bulk_test.dart create mode 100644 test/features/notifications/models/notification_model_fromjson_bulk_test.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 6b5808f5..13f139a1 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -74,7 +74,10 @@ class AppRouter { FlutterNativeSplash.remove(); return '/about-diet-screen?slug=$contentIdentifier'; } - final initialTabIndex = hidePlans ? 3 : 4; + const dietTabIndexWithHiddenPlansTab = 3; + const dietTabIndexWithoutHiddenPlansTab = 4; + + final initialTabIndex = hidePlans ? dietTabIndexWithHiddenPlansTab : dietTabIndexWithoutHiddenPlansTab; return '${RouteConstants.feedScreen}?initialTabIndex=$initialTabIndex'; } diff --git a/lib/core/comments/comments_cubit/comments_cubit.dart b/lib/core/comments/comments_cubit/comments_cubit.dart index 7e5df141..a65a9c6e 100644 --- a/lib/core/comments/comments_cubit/comments_cubit.dart +++ b/lib/core/comments/comments_cubit/comments_cubit.dart @@ -10,10 +10,7 @@ class CommentsCubit extends Cubit { final FeedRepository feedRepository; final CommentsRepository commentsRepository; - CommentsCubit({ - required this.feedRepository, - required this.commentsRepository, - }) : super(const CommentsState()); + CommentsCubit({required this.feedRepository, required this.commentsRepository}) : super(const CommentsState()); void setReplyId(int? replyId) { emit((state as CommentsStateLoaded).copyWith(replyId: replyId)); @@ -41,46 +38,27 @@ class CommentsCubit extends Cubit { } } - Future sendComment({ - required String id, - required String commentText, - required CommentSourceType commentSourceType, - int? parentCommentId, - }) async { + Future sendComment({required String id, required String commentText, required CommentSourceType commentSourceType, int? parentCommentId}) async { try { switch (commentSourceType) { case CommentSourceType.blogPost: - await feedRepository.sendBlogPostComment( - id: id, - commentText: commentText, - parentCommentId: parentCommentId, - ); + await feedRepository.sendBlogPostComment(id: id, commentText: commentText, parentCommentId: parentCommentId); break; case CommentSourceType.diet: - await feedRepository.sendDietsComment( - id: id, - commentText: commentText, - parentCommentId: parentCommentId, - ); + await feedRepository.sendDietsComment(id: id, commentText: commentText, parentCommentId: parentCommentId); break; case CommentSourceType.recipe: - await feedRepository.sendRecipesComment( - id: id, - commentText: commentText, - parentCommentId: parentCommentId, - ); + await feedRepository.sendRecipesComment(id: id, commentText: commentText, parentCommentId: parentCommentId); break; case CommentSourceType.plan: - await feedRepository.sendPlansComment( - slug: id, - commentText: commentText, - parentCommentId: parentCommentId, - ); + await feedRepository.sendPlansComment(slug: id, commentText: commentText, parentCommentId: parentCommentId); break; } getComments(id, commentSourceType); + return true; } catch (e) { emit(CommentsState.error(errorMessage: e.toString())); + return false; } } @@ -101,8 +79,9 @@ class CommentsCubit extends Cubit { Future updateComment({required int id, required String body}) async { final commentModel = (state as CommentsStateLoaded).comment; - final updatedCommentModel = - commentModel.copyWith(data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList()); + final updatedCommentModel = commentModel.copyWith( + data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList(), + ); try { await commentsRepository.updateComment(id: id, body: body); emit(CommentsState.loaded(comment: updatedCommentModel)); diff --git a/lib/core/interceptors/phone_verify_error_interceptor.dart b/lib/core/interceptors/phone_verify_error_interceptor.dart index 4073cb85..0649561a 100644 --- a/lib/core/interceptors/phone_verify_error_interceptor.dart +++ b/lib/core/interceptors/phone_verify_error_interceptor.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:go_router/go_router.dart'; @@ -12,18 +13,21 @@ class PhoneVerifyErrorInterceptor extends Interceptor { final data = err.response?.data; if (data is Map && data['type'] == 'unverificated_phone') { - final String phone = await sl().getMyPhone(); + final String phone = await sl().getMyPhone(); final String passKey = await sl().getPassKey(routeName: 'user_detailz.change-phone-verify', routeParams: {"phone": phone}); - await showErrorDialog(AppRouter.navigatorKey.currentContext!, - title: 'Привяжите номер телефона', description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона'); + await showErrorDialog( + AppRouter.navigatorKey.currentContext!, + title: 'Привяжите номер телефона', + description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона', + ); + final currentContext = AppRouter.navigatorKey.currentContext; + + if (currentContext != null && currentContext.mounted && GoRouter.of(currentContext).state.path == RouteConstants.stepperBecomeConsultantScreen) { + GoRouter.of(currentContext).pop(); + } AppRouter.navigatorKey.currentContext?.pushNamed('verifyScreen', extra: passKey); return handler.reject( - DioException( - requestOptions: err.requestOptions, - response: err.response, - error: 'Phone number not verified', - type: DioExceptionType.badResponse, - ), + DioException(requestOptions: err.requestOptions, response: err.response, error: 'Phone number not verified', type: DioExceptionType.badResponse), ); } } diff --git a/lib/core/routing/main_navigation_routes.dart b/lib/core/routing/main_navigation_routes.dart index 5875f7e1..7d06147f 100644 --- a/lib/core/routing/main_navigation_routes.dart +++ b/lib/core/routing/main_navigation_routes.dart @@ -47,8 +47,8 @@ class MainNavigationRoutes { path: RouteConstants.feedScreen, name: 'feedScreen', pageBuilder: (context, state) { - final initialTabIndex = int.tryParse(state.uri.queryParameters['initialTabIndex'] ?? '0'); - return NoTransitionPage(child: FeedScreen(initialTabIndex: initialTabIndex ?? 0)); + final initialTabIndex = int.tryParse(state.uri.queryParameters['initialTabIndex'] ?? ''); + return NoTransitionPage(child: FeedScreen(initialTabIndex: initialTabIndex)); }, redirect: (context, state) async { try { 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 bf50518a..ce691fb9 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -78,9 +78,7 @@ class _BlogPostScreenState extends State { return Scaffold( resizeToAvoidBottomInset: true, bottomSheet: Container( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom + 8, - ), + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8), decoration: BoxDecoration( color: Colors.white, border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))), @@ -147,14 +145,18 @@ class _BlogPostScreenState extends State { ), const SizedBox(width: 8), GestureDetector( - onTap: () { + onTap: () async { if (textEditingController.text.length > 2) { - commentsCubit.sendComment( + final isSent = await commentsCubit.sendComment( id: widget.id.toString(), commentText: textEditingController.text, parentCommentId: replyId, commentSourceType: CommentSourceType.blogPost, ); + if (!context.mounted) return; + + if (isSent) context.read().incrementCommentsState(widget.id); + context.read().setReplyId(null); setState(() { textEditingController.clear(); @@ -237,9 +239,7 @@ class _BlogPostScreenState extends State { if (blogPostState.blogPostModel?.description.isNotEmpty ?? false) Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: MarkdownHtmlWidget( - markdownText: blogPostState.blogPostModel!.description, - ), + child: MarkdownHtmlWidget(markdownText: blogPostState.blogPostModel!.description), ), const SizedBox(height: 8), if (blogPostState.blogPostModel?.image.isNotEmpty ?? false) diff --git a/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart b/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart index 6cc5e417..76c2fd69 100644 --- a/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart +++ b/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart'; @@ -20,45 +21,53 @@ class RegisterConsultantCubit extends Cubit { ); Future getMyRequestStatus(String slug) async { - _update = true; - final myRequestStatus = await consultantRepository.getMyRequestStatus(slug); - final certPhotos = await consultantRepository.getConsultantCertPhotos(); + try { + _update = true; + final myRequestStatus = await consultantRepository.getMyRequestStatus(slug); + final certPhotos = await consultantRepository.getConsultantCertPhotos(); - List? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) { - final course = courseMap.values.first; + List? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) { + final course = courseMap.values.first; - return CertificatesModel(title: course.title, date: course.date); - }).toList(); + return CertificatesModel(title: course.title, date: course.date); + }).toList(); - String? sanitizeLink(String? link) { - return (link != null && link.isNotEmpty) ? link : null; - } + String? sanitizeLink(String? link) { + return (link != null && link.isNotEmpty) ? link : null; + } - emit( - RegisterConsultantState( - consultant: ConsultantProfileModel( - certificates: certificates, - socialLinks: SocialLinksModel( - vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink), - tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink), - fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink), - instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink), - personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink), + emit( + RegisterConsultantState( + consultant: ConsultantProfileModel( + certificates: certificates, + socialLinks: SocialLinksModel( + vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink), + tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink), + fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink), + instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink), + personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink), + ), + hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1, ), - hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1, + acceptOffer: false, + loading: false, + requestModel: myRequestStatus, + certPhotos: certPhotos, + isUpdating: _update, ), - acceptOffer: false, - loading: false, - requestModel: myRequestStatus, - certPhotos: certPhotos, - isUpdating: _update - ), - ); + ); + } catch (e) { + log(e.toString()); + } } Future disableLoading() async { - final certPhotos = await consultantRepository.getConsultantCertPhotos(); - emit(state.copyWith(loading: false, certPhotos: certPhotos)); + try { + final certPhotos = await consultantRepository.getConsultantCertPhotos(); + emit(state.copyWith(loading: false, certPhotos: certPhotos)); + } catch (e) { + log(e.toString()); + } } void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) { diff --git a/lib/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart b/lib/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart index ef9f5cec..40d4a352 100644 --- a/lib/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart +++ b/lib/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart @@ -17,18 +17,13 @@ class _BecomeConsultantScreenState extends State { title: const Text('Стать консультантом'), automaticallyImplyLeading: false, centerTitle: false, - leading: IconButton( - onPressed: () => GoRouter.of(context).pop(), - icon: const Icon(Icons.close), - ), + leading: IconButton(onPressed: () => GoRouter.of(context).pop(), icon: const Icon(Icons.close)), ), body: const Padding( padding: EdgeInsets.all(16), child: Text( 'Чтобы стать консультантом необходимо пройти короткую регистрацию. Подготовьте свои дипломы и/или сертификаты в формате jpg, они Вам пригодятся для проверки и подтверждения. Также необходимо будет указать ссылки на Ваши соц. сети и указать тип лица (физ. лицо или юр. лицо). На данный момент доступна регистрация в качестве физ. лица.\n\nВнутри Итвио разница между обычным пользователем и консультантом заключается в дополнительных возможностях последнего создавать публичные диеты, рецепты и планы питания; участвовать в ежемесячных рейтингах.', - style: TextStyle( - fontSize: 16, - ), + style: TextStyle(fontSize: 16), ), ), bottomSheet: _startRegistrationButton(), @@ -37,24 +32,16 @@ class _BecomeConsultantScreenState extends State { InkWell _startRegistrationButton() { return InkWell( - onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen'), + onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen', extra: {'slug': null}), borderRadius: BorderRadius.circular(20), child: Ink( height: 52, width: MediaQuery.of(context).size.width - 32, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), child: const Center( child: Text( 'Начать регистрацию', - style: TextStyle( - color: Colors.white, - height: 1, - fontWeight: FontWeight.w600, - fontSize: 18, - ), + style: TextStyle(color: Colors.white, height: 1, fontWeight: FontWeight.w600, fontSize: 18), ), ), ), 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 d1bf399f..c749f6d4 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -26,6 +26,17 @@ class FeedCubit extends Cubit { } } + void incrementCommentsState(int id) { + final List feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList]; + final int index = feedBlogPostList.indexWhere((e) => e.id == id); + if (index == -1) return; + final current = feedBlogPostList[index]; + feedBlogPostList[index] = current.copyWith(totalComments: current.totalComments + 1); + if (!isClosed) { + emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading)); + } + } + Future loadFeedBlogPostList({String? query}) async { try { _query = query; diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index f8c47cf5..5eb4f454 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -33,7 +33,7 @@ import '../widgets/recipes.dart'; class FeedScreen extends StatelessWidget { const FeedScreen({super.key, this.initialTabIndex = 0}); - final int initialTabIndex; + final int? initialTabIndex; @override Widget build(BuildContext context) { return MultiBlocProvider( @@ -51,7 +51,7 @@ class FeedScreen extends StatelessWidget { class FeedScreenContent extends StatefulWidget { const FeedScreenContent({super.key, required this.initialTabIndex}); - final int initialTabIndex; + final int? initialTabIndex; @override State createState() => _FeedScreenContentState(); } @@ -72,7 +72,7 @@ class _FeedScreenContentState extends State { final consultantsScrollController = ScrollController(); @override void initState() { - _selectedChipIndex = widget.initialTabIndex; + _selectedChipIndex = widget.initialTabIndex ?? 0; chipsLabelList = hidePlans ? ['Все', 'Консультанты', 'Рецепты', 'Диеты'] : ['Все', 'Планы питания', 'Консультанты', 'Рецепты', 'Диеты']; consultantsScrollController.addListener(_onScrollConsultants); _checkRegistrationDate(context); @@ -92,8 +92,11 @@ class _FeedScreenContentState extends State { @override void didUpdateWidget(FeedScreenContent oldWidget) { super.didUpdateWidget(oldWidget); + final initialTabIndex = widget.initialTabIndex; setState(() { - _selectedChipIndex = widget.initialTabIndex; + if (initialTabIndex != null) { + _selectedChipIndex = initialTabIndex; + } }); } diff --git a/lib/features/plans/presentation/cubit/about_plan_cubit.dart b/lib/features/plans/presentation/cubit/about_plan_cubit.dart index a45b3bc1..85d26b0f 100644 --- a/lib/features/plans/presentation/cubit/about_plan_cubit.dart +++ b/lib/features/plans/presentation/cubit/about_plan_cubit.dart @@ -13,9 +13,12 @@ class AboutPlanCubit extends Cubit { final PlanRepository planRepository; AboutPlanCubit({required this.planRepository}) : super(AboutPlanState.initial()); - Future getPlan(String slug) async { + Future getPlan({required String slug, int? index}) async { try { final plan = await planRepository.getPlan(slug); + if (index != null) { + planRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: plan.totalComments)); + } emit(state.copyWith(planModel: plan, error: null)); } catch (e, stackTrace) { log(e.toString(), stackTrace: stackTrace); @@ -27,13 +30,7 @@ class AboutPlanCubit extends Cubit { try { emit(state.copyWith(isLoading: true)); final plan = await planRepository.getPlan(slug); - emit( - state.copyWith( - planModel: plan, - error: null, - isLoading: false, - ), - ); + emit(state.copyWith(planModel: plan, error: null, isLoading: false)); } catch (e, stackTrace) { log(e.toString(), stackTrace: stackTrace); emit(state.copyWith(error: e.toString(), isLoading: false)); @@ -59,21 +56,17 @@ class AboutPlanCubit extends Cubit { } } - EasyDebounce.debounce( - 'like_plan_$slug', - const Duration(seconds: 1), - () async { - try { - await planRepository.likePlan(slug); - } catch (e) { - log(e.toString()); - if (index != null) { - planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentPlan.wasLikedByMe, totalLikes: currentPlan.totalLikes, index: index)); - } - emit(state.copyWith(planModel: currentPlan, error: null)); + EasyDebounce.debounce('like_plan_$slug', const Duration(seconds: 1), () async { + try { + await planRepository.likePlan(slug); + } catch (e) { + log(e.toString()); + if (index != null) { + planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentPlan.wasLikedByMe, totalLikes: currentPlan.totalLikes, index: index)); } - }, - ); + emit(state.copyWith(planModel: currentPlan, error: null)); + } + }); } Future addToFavorite({required String slug, required int? index}) async { @@ -84,22 +77,18 @@ class AboutPlanCubit extends Cubit { if (index != null) { planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: true, index: index)); } - EasyDebounce.debounce( - 'add_or_remove_plan_to_favorites', - const Duration(seconds: 1), - () async { - try { - await planRepository.addToFavorite(slug); - } catch (e) { - log(e.toString()); - showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - if (index != null) { - planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index)); - } - emit(state.copyWith(planModel: currentPlan, error: null)); + EasyDebounce.debounce('add_or_remove_plan_to_favorites', const Duration(seconds: 1), () async { + try { + await planRepository.addToFavorite(slug); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + if (index != null) { + planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index)); } - }, - ); + emit(state.copyWith(planModel: currentPlan, error: null)); + } + }); } Future removeFromFavorite({required String slug, required int? index}) async { @@ -110,22 +99,18 @@ class AboutPlanCubit extends Cubit { if (index != null) { planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: false, index: index)); } - EasyDebounce.debounce( - 'add_or_remove_plan_to_favorites', - const Duration(seconds: 1), - () async { - try { - await planRepository.removeFromFavorite(slug); - } catch (e) { - log(e.toString()); - showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - if (index != null) { - planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index)); - } - emit(state.copyWith(planModel: currentPlan, error: null)); + EasyDebounce.debounce('add_or_remove_plan_to_favorites', const Duration(seconds: 1), () async { + try { + await planRepository.removeFromFavorite(slug); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + if (index != null) { + planRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentPlan.isFavorite, index: index)); } - }, - ); + emit(state.copyWith(planModel: currentPlan, error: null)); + } + }); } Future<(ChosenPlanModel?, String?)> chosePlan() async { @@ -149,8 +134,8 @@ class AboutPlanCubit extends Cubit { } String _getOptionNameFromIndex(int index) => switch (index) { - 1 => 'options2', - 2 => 'options3', - _ => 'options1', - }; + 1 => 'options2', + 2 => 'options3', + _ => 'options1', + }; } diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index 283ef0fc..2dcbd91b 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -154,7 +154,7 @@ class _AboutPlanScreenState extends State { const SizedBox(width: 16), ], ), - body: DataErrorWidget(onReload: () => context.read().getPlan(widget.slug)), + body: DataErrorWidget(onReload: () => context.read().getPlan(slug: widget.slug)), ); } return Scaffold( @@ -295,7 +295,7 @@ class _AboutPlanScreenState extends State { Future.delayed(const Duration(milliseconds: 500), () { if (context.mounted) { - context.read().getPlan(widget.slug); + context.read().getPlan(slug: widget.slug, index: widget.index); } }); FocusManager.instance.primaryFocus?.unfocus(); diff --git a/test/core/app_router_test.dart b/test/core/app_router_test.dart index 407145e8..19c61dab 100644 --- a/test/core/app_router_test.dart +++ b/test/core/app_router_test.dart @@ -112,5 +112,43 @@ void main() { expect(() => AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'vk'), returnsNormally); expect(() => AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'google'), returnsNormally); }); + + test('navigateByDeepLinkUrl handles empty and invalid urls', () { + expect(() => AppRouter.navigateByDeepLinkUrl(''), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://unknown-host.example/foo'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('%%%'), returnsNormally); + }); + + test('navigateByDeepLinkUrl handles supported recipe and blog links', () { + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/recipe/test-recipe'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.com/en/blog_post/123'), returnsNormally); + }); + + test('navigateByDeepLinkUrl handles plan links', () { + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/plan/plan-slug'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/plan/42'), returnsNormally); + }); + + test('navigateByDeepLinkUrl handles diet links', () { + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/diet/99'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/diet/diet-slug'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/diet'), returnsNormally); + }); + + test('navigateByDeepLinkUrl handles chat links', () { + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/chat/uuid-1'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/chat/show/uuid-2'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/chat'), returnsNormally); + }); + + test('navigateByDeepLinkUrl handles profile and misc links', () { + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/profile/some-user'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/profile'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/badge/1'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/diary'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/consultant_diary_comment/7'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/consultations/status/11'), returnsNormally); + expect(() => AppRouter.navigateByDeepLinkUrl('https://eatvio.ru/ru/consultations'), returnsNormally); + }); }); } diff --git a/test/features/blog_post/presentation/pages/blog_post_screen_smoke_test.dart b/test/features/blog_post/presentation/pages/blog_post_screen_smoke_test.dart index 027ae523..f194ea0c 100644 --- a/test/features/blog_post/presentation/pages/blog_post_screen_smoke_test.dart +++ b/test/features/blog_post/presentation/pages/blog_post_screen_smoke_test.dart @@ -205,5 +205,54 @@ void main() { expect(find.text('Test Post Title'), findsOneWidget); }); }); + + testWidgets('loaded state tapping like invokes toggleLike', (tester) async { + await mockNetworkImagesFor(() async { + final blogPostCubit = MockBlogPostCubit(); + final feedCubit = MockFeedCubit(); + final commentsCubit = MockCommentsCubit(); + final blogPostModel = FeedBlogPostModel( + id: 1, + title: 'Test Post Title', + description: 'Test post description', + image: 'https://example.com/image.jpg', + state: 'published', + author: const Author(authorSlug: 'author-1', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false), + videoUrl: '', + createdAt: DateTime(2024, 1, 15), + updatedAt: DateTime(2024, 1, 15), + totalLikes: 10, + totalViews: 100, + totalComments: 5, + wasLikedByMe: false, + ); + final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null); + + when(blogPostCubit.state).thenReturn(state); + when(blogPostCubit.stream).thenAnswer((_) => Stream.value(state)); + when(blogPostCubit.close()).thenAnswer((_) async => Future.value()); + when(blogPostCubit.getBlogPost(any)).thenAnswer((_) async => Future.value()); + when(blogPostCubit.toggleLike()).thenAnswer((_) async => Future.value()); + when(feedCubit.viewPost(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.updateFeedBlogPostList(any, any)).thenReturn(null); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(blogPostCubit, commentsCubit); + + await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, const BlogPostScreen(id: 1))); + await tester.pump(); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.favorite_border_rounded)); + await tester.pump(); + + verify(blogPostCubit.toggleLike()).called(1); + }); + }); }); } diff --git a/test/features/create_recipe/reporitories/recipe_repository_test.dart b/test/features/create_recipe/reporitories/recipe_repository_test.dart index 8392d3b7..c4d57c22 100644 --- a/test/features/create_recipe/reporitories/recipe_repository_test.dart +++ b/test/features/create_recipe/reporitories/recipe_repository_test.dart @@ -1,4 +1,9 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/models/pagination_model.dart'; import 'package:eatvio_app/features/create_recipe/models/ingredient.dart'; import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart'; @@ -8,31 +13,123 @@ import 'package:eatvio_app/features/create_recipe/models/step.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:flutter_test/flutter_test.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:mocktail/mocktail.dart'; import 'package:retrofit/retrofit.dart'; class MockCreateRecipeService extends Mock implements CreateRecipeService {} -final mockHttpResponse = HttpResponse( - null, - Response(requestOptions: RequestOptions(path: ''), statusCode: 200), -); +final mockHttpResponse = HttpResponse(null, Response(requestOptions: RequestOptions(path: ''), statusCode: 200)); void main() { setUpAll(() { + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + if (sl.isRegistered()) { + sl.unregister(); + } + sl.registerSingleton(AppConfig(buildType: AppBuildType.tests)); registerFallbackValue(FormData()); }); group('RecipeRepository', () { late RecipeRepository repository; late MockCreateRecipeService mockService; + late Directory tempDir; + late File recipeImage; + late File stepImage; setUp(() { mockService = MockCreateRecipeService(); repository = RecipeRepository(recipeService: mockService); }); + setUp(() async { + tempDir = await Directory.systemTemp.createTemp('eatvio_recipe_repo_test_'); + recipeImage = File('${tempDir.path}/recipe.png'); + stepImage = File('${tempDir.path}/step.png'); + + // Valid 1x1 transparent PNG to satisfy ImageCompressUtility decoding. + const png1x1 = [ + 0x89, + 0x50, + 0x4E, + 0x47, + 0x0D, + 0x0A, + 0x1A, + 0x0A, + 0x00, + 0x00, + 0x00, + 0x0D, + 0x49, + 0x48, + 0x44, + 0x52, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x01, + 0x08, + 0x06, + 0x00, + 0x00, + 0x00, + 0x1F, + 0x15, + 0xC4, + 0x89, + 0x00, + 0x00, + 0x00, + 0x0A, + 0x49, + 0x44, + 0x41, + 0x54, + 0x78, + 0x9C, + 0x63, + 0x00, + 0x01, + 0x00, + 0x00, + 0x05, + 0x00, + 0x01, + 0x0D, + 0x0A, + 0x2D, + 0xB4, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4E, + 0x44, + 0xAE, + 0x42, + 0x60, + 0x82, + ]; + + await recipeImage.writeAsBytes(png1x1, flush: true); + await stepImage.writeAsBytes(png1x1, flush: true); + }); + + tearDown(() async { + if (tempDir.existsSync()) { + await tempDir.delete(recursive: true); + } + }); + test('inputProduct - возвращает Map с продуктами', () async { const name = 'test-product'; const expected = { @@ -177,12 +274,12 @@ void main() { test('updateRecipe - вызывает updateRecipe с правильными параметрами', () async { const slug = 'recipe-slug'; - const recipe = RecipeModel( + final recipe = RecipeModel( type: 'main', title: 'Updated Recipe', description: 'Updated description', videoUrl: null, - image: '/test/path/image.jpg', + image: recipeImage.path, ingredients: null, steps: null, tags: null, @@ -192,27 +289,29 @@ void main() { energyPerServing: 300, ); - // Мокируем ImageCompressUtility.compressFile - // Это сложно, так как это статический метод, но мы можем проверить, что вызывается updateRecipe - when(() => mockService.updateRecipe(slug: slug, formData: any(named: 'formData'))) - .thenAnswer((_) async => Future.value(mockHttpResponse)); + when( + () => mockService.updateRecipe( + slug: slug, + formData: any(named: 'formData'), + ), + ).thenAnswer((_) async => Future.value(mockHttpResponse)); - // Этот тест может упасть из-за ImageCompressUtility, но мы проверяем логику - try { - await repository.publishRecipe(recipe, true, slug); - verify(() => mockService.updateRecipe(slug: slug, formData: any(named: 'formData'))).called(1); - } catch (e) { - // Игнорируем ошибки, связанные с ImageCompressUtility в тестах - } + await repository.publishRecipe(recipe, true, slug); + verify( + () => mockService.updateRecipe( + slug: slug, + formData: any(named: 'formData'), + ), + ).called(1); }); test('publishRecipe - обрабатывает рецепт с тегами', () async { - const recipe = RecipeModel( + final recipe = RecipeModel( type: 'main', title: 'Test Recipe', description: 'Test description', videoUrl: null, - image: '/test/path/image.jpg', + image: recipeImage.path, ingredients: null, steps: null, tags: ['tag1', 'tag2'], @@ -222,24 +321,19 @@ void main() { energyPerServing: 300, ); - when(() => mockService.publishRecipe(formData: any(named: 'formData'))) - .thenAnswer((_) async => Future.value(mockHttpResponse)); + when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse)); - try { - await repository.publishRecipe(recipe); - verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); - } catch (e) { - // Игнорируем ошибки, связанные с ImageCompressUtility в тестах - } + await repository.publishRecipe(recipe); + verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); }); test('publishRecipe - обрабатывает рецепт с videoUrl', () async { - const recipe = RecipeModel( + final recipe = RecipeModel( type: 'main', title: 'Test Recipe', description: 'Test description', videoUrl: 'https://example.com/video.mp4', - image: '/test/path/image.jpg', + image: recipeImage.path, ingredients: null, steps: null, tags: null, @@ -249,24 +343,19 @@ void main() { energyPerServing: 300, ); - when(() => mockService.publishRecipe(formData: any(named: 'formData'))) - .thenAnswer((_) async => Future.value(mockHttpResponse)); + when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse)); - try { - await repository.publishRecipe(recipe); - verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); - } catch (e) { - // Игнорируем ошибки, связанные с ImageCompressUtility в тестах - } + await repository.publishRecipe(recipe); + verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); }); test('publishRecipe - обрабатывает рецепт без videoUrl (null)', () async { - const recipe = RecipeModel( + final recipe = RecipeModel( type: 'main', title: 'Test Recipe', description: 'Test description', videoUrl: null, - image: '/test/path/image.jpg', + image: recipeImage.path, ingredients: null, steps: null, tags: null, @@ -276,24 +365,19 @@ void main() { energyPerServing: 300, ); - when(() => mockService.publishRecipe(formData: any(named: 'formData'))) - .thenAnswer((_) async => Future.value(mockHttpResponse)); + when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse)); - try { - await repository.publishRecipe(recipe); - verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); - } catch (e) { - // Игнорируем ошибки, связанные с ImageCompressUtility в тестах - } + await repository.publishRecipe(recipe); + verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); }); test('publishRecipe - обрабатывает рецепт с пустым videoUrl', () async { - const recipe = RecipeModel( + final recipe = RecipeModel( type: 'main', title: 'Test Recipe', description: 'Test description', videoUrl: '', - image: '/test/path/image.jpg', + image: recipeImage.path, ingredients: null, steps: null, tags: null, @@ -303,24 +387,19 @@ void main() { energyPerServing: 300, ); - when(() => mockService.publishRecipe(formData: any(named: 'formData'))) - .thenAnswer((_) async => Future.value(mockHttpResponse)); + when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse)); - try { - await repository.publishRecipe(recipe); - verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); - } catch (e) { - // Игнорируем ошибки, связанные с ImageCompressUtility в тестах - } + await repository.publishRecipe(recipe); + verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); }); test('publishRecipe - пробрасывает исключение при ошибке', () async { - const recipe = RecipeModel( + final recipe = RecipeModel( type: 'main', title: 'Test Recipe', description: 'Test description', videoUrl: null, - image: '/test/path/image.jpg', + image: recipeImage.path, ingredients: null, steps: null, tags: null, @@ -330,37 +409,21 @@ void main() { energyPerServing: 300, ); - when(() => mockService.publishRecipe(formData: any(named: 'formData'))) - .thenThrow(Exception('Error')); + when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenThrow(Exception('Error')); - try { - await repository.publishRecipe(recipe); - fail('Should have thrown exception'); - } catch (e) { - expect(e, isA()); - } + expect(() => repository.publishRecipe(recipe), throwsA(isA())); }); test('publishRecipe - обрабатывает рецепт с ингредиентами', () async { - const recipe = RecipeModel( + final recipe = RecipeModel( type: 'main', title: 'Test Recipe', description: 'Test description', videoUrl: null, - image: '/test/path/image.jpg', + image: recipeImage.path, ingredients: [ - Ingredient( - name: 'Apple', - unit: '100', - portion: 'grams', - productId: 1, - ), - Ingredient( - name: 'Banana', - unit: '50', - portion: 'grams', - productId: 2, - ), + const Ingredient(name: 'Apple', unit: '100', portion: 'grams', productId: 1), + const Ingredient(name: 'Banana', unit: '50', portion: 'grams', productId: 2), ], steps: null, tags: null, @@ -370,31 +433,21 @@ void main() { energyPerServing: 300, ); - when(() => mockService.publishRecipe(formData: any(named: 'formData'))) - .thenAnswer((_) async => Future.value(mockHttpResponse)); + when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse)); - try { - await repository.publishRecipe(recipe); - verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); - } catch (e) { - // Игнорируем ошибки, связанные с ImageCompressUtility в тестах - } + await repository.publishRecipe(recipe); + verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); }); test('publishRecipe - обрабатывает рецепт с null ингредиентами', () async { - const recipe = RecipeModel( + final recipe = RecipeModel( type: 'main', title: 'Test Recipe', description: 'Test description', videoUrl: null, - image: '/test/path/image.jpg', + image: recipeImage.path, ingredients: [ - Ingredient( - name: 'Apple', - unit: '100', - portion: 'grams', - productId: 1, - ), + const Ingredient(name: 'Apple', unit: '100', portion: 'grams', productId: 1), null, // null ингредиент должен быть отфильтрован ], steps: null, @@ -405,34 +458,23 @@ void main() { energyPerServing: 300, ); - when(() => mockService.publishRecipe(formData: any(named: 'formData'))) - .thenAnswer((_) async => Future.value(mockHttpResponse)); + when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse)); - try { - await repository.publishRecipe(recipe); - verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); - } catch (e) { - // Игнорируем ошибки, связанные с ImageCompressUtility в тестах - } + await repository.publishRecipe(recipe); + verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); }); test('publishRecipe - обрабатывает рецепт со шагами без изображений', () async { - const recipe = RecipeModel( + final recipe = RecipeModel( type: 'main', title: 'Test Recipe', description: 'Test description', videoUrl: null, - image: '/test/path/image.jpg', + image: recipeImage.path, ingredients: null, steps: [ - Step( - description: 'Step 1 description', - image: null, - ), - Step( - description: 'Step 2 description', - image: '', - ), + const Step(description: 'Step 1 description', image: null), + const Step(description: 'Step 2 description', image: ''), ], tags: null, totalCookingTime: 30, @@ -441,35 +483,24 @@ void main() { energyPerServing: 300, ); - when(() => mockService.publishRecipe(formData: any(named: 'formData'))) - .thenAnswer((_) async => Future.value(mockHttpResponse)); + when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse)); - try { - await repository.publishRecipe(recipe); - verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); - } catch (e) { - // Игнорируем ошибки, связанные с ImageCompressUtility в тестах - } + await repository.publishRecipe(recipe); + verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); }); test('publishRecipe - обрабатывает рецепт со смешанными шагами (с null)', () async { - const recipe = RecipeModel( + final recipe = RecipeModel( type: 'main', title: 'Test Recipe', description: 'Test description', videoUrl: null, - image: '/test/path/image.jpg', + image: recipeImage.path, ingredients: null, steps: [ - Step( - description: 'Step 1 description', - image: null, - ), + const Step(description: 'Step 1 description', image: null), null, // null шаг должен быть отфильтрован - Step( - description: 'Step 3 description', - image: '', - ), + const Step(description: 'Step 3 description', image: ''), ], tags: null, totalCookingTime: 30, @@ -478,24 +509,19 @@ void main() { energyPerServing: 300, ); - when(() => mockService.publishRecipe(formData: any(named: 'formData'))) - .thenAnswer((_) async => Future.value(mockHttpResponse)); + when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse)); - try { - await repository.publishRecipe(recipe); - verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); - } catch (e) { - // Игнорируем ошибки, связанные с ImageCompressUtility в тестах - } + await repository.publishRecipe(recipe); + verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); }); test('publishRecipe - удаляет пустые значения из formMap', () async { - const recipe = RecipeModel( + final recipe = RecipeModel( type: 'main', title: 'Test Recipe', description: '', // Пустое описание должно быть удалено videoUrl: '', // Пустой videoUrl должен быть удален - image: '/test/path/image.jpg', + image: recipeImage.path, ingredients: null, steps: null, tags: null, @@ -505,25 +531,20 @@ void main() { energyPerServing: null, ); - when(() => mockService.publishRecipe(formData: any(named: 'formData'))) - .thenAnswer((_) async => Future.value(mockHttpResponse)); + when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse)); - try { - await repository.publishRecipe(recipe); - verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); - } catch (e) { - // Игнорируем ошибки, связанные с ImageCompressUtility в тестах - } + await repository.publishRecipe(recipe); + verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); }); test('updateRecipe - пробрасывает исключение при ошибке', () async { const slug = 'recipe-slug'; - const recipe = RecipeModel( + final recipe = RecipeModel( type: 'main', title: 'Test Recipe', description: 'Test description', videoUrl: null, - image: '/test/path/image.jpg', + image: recipeImage.path, ingredients: null, steps: null, tags: null, @@ -533,16 +554,132 @@ void main() { energyPerServing: 300, ); - when(() => mockService.updateRecipe(slug: slug, formData: any(named: 'formData'))) - .thenThrow(Exception('Error')); + when( + () => mockService.updateRecipe( + slug: slug, + formData: any(named: 'formData'), + ), + ).thenThrow(Exception('Error')); - try { - await repository.publishRecipe(recipe, true, slug); - fail('Should have thrown exception'); - } catch (e) { - expect(e, isA()); - } + expect(() => repository.publishRecipe(recipe, true, slug), throwsA(isA())); }); + test('publishRecipe - собирает steps с изображением (MultipartFile)', () async { + final recipe = RecipeModel( + type: 'main', + title: 'Test Recipe', + description: 'Test description', + videoUrl: null, + image: recipeImage.path, + ingredients: null, + steps: [Step(description: 'Step with image', image: stepImage.path)], + tags: null, + totalCookingTime: 30, + activeCookTime: 15, + servings: 4, + energyPerServing: 300, + ); + + when(() => mockService.publishRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse)); + + await repository.publishRecipe(recipe); + verify(() => mockService.publishRecipe(formData: any(named: 'formData'))).called(1); + }); + + test('saveAsDraft - создает draft (createDraftedRecipe)', () async { + final recipe = RecipeModel( + type: 'main', + title: 'Draft title', + description: 'Draft desc', + videoUrl: null, + image: recipeImage.path, + ingredients: null, + steps: null, + tags: const ['tag1'], + totalCookingTime: 30, + activeCookTime: 15, + servings: 2, + energyPerServing: 200, + ); + + when(() => mockService.createDraftedRecipe(formData: any(named: 'formData'))).thenAnswer((_) async => Future.value(mockHttpResponse)); + + await repository.saveAsDraft(recipe); + verify(() => mockService.createDraftedRecipe(formData: any(named: 'formData'))).called(1); + verifyNever( + () => mockService.updateRecipe( + slug: any(named: 'slug'), + formData: any(named: 'formData'), + ), + ); + }); + + test('saveAsDraft - обновляет draft (updateRecipe)', () async { + const slug = 'draft-slug'; + final recipe = RecipeModel( + type: 'main', + title: 'Draft title', + description: 'Draft desc', + videoUrl: null, + image: recipeImage.path, + ingredients: null, + steps: null, + tags: null, + totalCookingTime: 30, + activeCookTime: 15, + servings: 2, + energyPerServing: 200, + ); + + when( + () => mockService.updateRecipe( + slug: slug, + formData: any(named: 'formData'), + ), + ).thenAnswer((_) async => Future.value(mockHttpResponse)); + + await repository.saveAsDraft(recipe, true, slug); + verify( + () => mockService.updateRecipe( + slug: slug, + formData: any(named: 'formData'), + ), + ).called(1); + }); + + test('saveAsDraft - update=true без slug кидает исключение', () async { + final recipe = RecipeModel( + type: 'main', + title: 'Draft title', + description: 'Draft desc', + videoUrl: null, + image: recipeImage.path, + ingredients: null, + steps: null, + tags: null, + totalCookingTime: 30, + activeCookTime: 15, + servings: 2, + energyPerServing: 200, + ); + + // slug! должен упасть + expect(() => repository.saveAsDraft(recipe, true, null), throwsA(anything)); + }); + + test('draftExistingRecipe - вызывает draftRecipe', () async { + const id = 123; + when(() => mockService.draftRecipe(id: id)).thenAnswer((_) async => Future.value()); + + await repository.draftExistingRecipe(id); + verify(() => mockService.draftRecipe(id: id)).called(1); + }); + + test('draftExistingRecipe - пробрасывает исключение при ошибке', () async { + const id = 123; + when(() => mockService.draftRecipe(id: id)).thenThrow(Exception('Error')); + + expect(() => repository.draftExistingRecipe(id), throwsA(isA())); + }); }); } diff --git a/test/features/dev_tools/presentation/dev_tools_screen_smoke_test.dart b/test/features/dev_tools/presentation/dev_tools_screen_smoke_test.dart index 7995d87e..4f7e70ec 100644 --- a/test/features/dev_tools/presentation/dev_tools_screen_smoke_test.dart +++ b/test/features/dev_tools/presentation/dev_tools_screen_smoke_test.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -16,6 +17,7 @@ void main() { setUp(() async { await sl.reset(); sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + sl.registerLazySingleton(() => Dio(BaseOptions(baseUrl: 'http://localhost'))); }); Widget wrap(Widget child) { @@ -39,5 +41,29 @@ void main() { expect(find.byType(DevToolsScreenContent), findsOneWidget); }); + + testWidgets('updates base url from input by done button', (tester) async { + await tester.pumpWidget(wrap(const DevToolsScreen())); + await tester.pump(); + + await tester.enterText(find.byType(TextFormField).first, 'HTTP://EXAMPLE.COM'); + await tester.tap(find.byIcon(Icons.done)); + await tester.pump(); + + expect(find.byType(DevToolsScreenContent), findsOneWidget); + }); + + testWidgets('toggle base url button is tappable', (tester) async { + await tester.pumpWidget(wrap(const DevToolsScreen())); + await tester.pump(); + + final toggleButton = find.text('Set production base url'); + expect(toggleButton, findsOneWidget); + + await tester.tap(toggleButton); + await tester.pump(); + + expect(find.byType(DevToolsScreenContent), findsOneWidget); + }); }); } diff --git a/test/features/dev_tools/test_cases_bulk_test.dart b/test/features/dev_tools/test_cases_bulk_test.dart new file mode 100644 index 00000000..da212b8f --- /dev/null +++ b/test/features/dev_tools/test_cases_bulk_test.dart @@ -0,0 +1,107 @@ +import 'package:eatvio_app/features/dev_tools/test_cases.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('DevTools TestCases bulk', () { + test('all case names are readable', () { + final cases = [ + CreateRecipeTask(), + Test850task(), + GetChatRoomPreviews(), + GetChatRoom(), + SendMessage(), + GetChatMessages(), + GetChatRoomIdByUserSlug(), + GetBlogPostList(), + GetRecipes(), + GetDiets(), + GetPlans(), + GetBlogPostComments(), + SendBlogPostComment(), + GetDietsComments(), + SendDietsComment(), + GetPlansComments(), + SendPlansComment(), + GetRecipesComments(), + AuthServiceTest(), + DiaryServiceTest(), + BlockedUserServiceTest(), + PlanServiceTest(), + AboutRecipeServiceTest(), + BlogPostServiceTest(), + CalendarServiceTest(), + CardsServiceTest(), + ConsultantServiceTest(), + ConsultationServiceTest(), + CreateDietServiceTest(), + CreatePlanServiceTest(), + GlossaryServiceTest(), + MyDietsServiceTest(), + MyPlansServiceTest(), + MyRecipesServiceTest(), + NotificationsServiceTest(), + PlansServiceTest(), + ProfileServiceTest(), + RegistrationServiceTest(), + ResetPasswordServiceTest(), + SettingsServiceTest(), + WsServiceTest(), + ]; + + for (final c in cases) { + expect(c.name, isNotEmpty); + } + }); + + test('run() does not throw for all cases', () async { + final cases = [ + CreateRecipeTask(), + Test850task(), + GetChatRoomPreviews(), + GetChatRoom(), + SendMessage(), + GetChatMessages(), + GetChatRoomIdByUserSlug(), + GetBlogPostList(), + GetRecipes(), + GetDiets(), + GetPlans(), + GetBlogPostComments(), + SendBlogPostComment(), + GetDietsComments(), + SendDietsComment(), + GetPlansComments(), + SendPlansComment(), + GetRecipesComments(), + AuthServiceTest(), + DiaryServiceTest(), + BlockedUserServiceTest(), + PlanServiceTest(), + AboutRecipeServiceTest(), + BlogPostServiceTest(), + CalendarServiceTest(), + CardsServiceTest(), + ConsultantServiceTest(), + ConsultationServiceTest(), + CreateDietServiceTest(), + CreatePlanServiceTest(), + GlossaryServiceTest(), + MyDietsServiceTest(), + MyPlansServiceTest(), + MyRecipesServiceTest(), + NotificationsServiceTest(), + PlansServiceTest(), + ProfileServiceTest(), + RegistrationServiceTest(), + ResetPasswordServiceTest(), + SettingsServiceTest(), + WsServiceTest(), + ]; + + for (final c in cases) { + final result = await c.run(baseUrl: 'http://localhost'); + expect(result, isA()); + } + }); + }); +} diff --git a/test/features/notifications/models/notification_model_fromjson_bulk_test.dart b/test/features/notifications/models/notification_model_fromjson_bulk_test.dart new file mode 100644 index 00000000..8ee7fe97 --- /dev/null +++ b/test/features/notifications/models/notification_model_fromjson_bulk_test.dart @@ -0,0 +1,121 @@ +import 'package:eatvio_app/features/notifications/models/notification_model.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Smoke coverage for [fromJson] factories not exercised elsewhere — improves line coverage on large model file. +void main() { + const c = '2024-06-01T12:00:00Z'; + final base = {'created_at': c}; + + final planAndUser = {'created_at': c, 'plan_name': 'p', 'user_name': 'u', 'renewal_date': '2024-06-02'}; + + final scoreLike = { + 'created_at': c, + 'score_key': 'k', + 'scorable_id': 1, + 'scorable_type': 'Type', + 'score_value': 2, + }; + + final commentReply = { + 'created_at': c, + 'entity_name': 'e', + 'commented_entity_id': 1, + 'commented_entity_type': 'diet', + 'author': 'a', + 'author_id': 2, + 'parrent_comment_id': 3, + }; + + final entityCommented = { + 'created_at': c, + 'entity_name': 'e', + 'entity_id': 1, + 'entity_type': 'diet', + 'entity_traslated_type': 't', + 'entity_url': 'u', + 'author_name': 'a', + 'author_id': 2, + 'author_slug': 's', + }; + + group('CommentedEntityType.fromJson', () { + test('maps known and legacy keys', () { + expect(CommentedEntityType.fromJson('diet'), CommentedEntityType.diet); + expect(CommentedEntityType.fromJson('Modules\\UserGeneratedPublicContent\\Models\\Diet'), CommentedEntityType.diet); + expect(CommentedEntityType.fromJson('blog-post'), CommentedEntityType.blogPost); + expect(CommentedEntityType.fromJson('Modules\\UserGeneratedPublicContent\\Models\\BlogPost'), CommentedEntityType.blogPost); + expect(CommentedEntityType.fromJson('other'), CommentedEntityType.unknown); + }); + }); + + group('notification fromJson bulk smoke', () { + test('executes remaining model factories with minimal JSON', () { + PlanSubscriptionExpiredNotification.fromJson(planAndUser, NotificationType.planSubscriptionExpired); + PlanSubscriptionRenewedNotification.fromJson(planAndUser, NotificationType.planSubscriptionRenewed); + ChosenPlanStartedToConsultantNotification.fromJson(base, NotificationType.chosenPlanStartedToConsultant); + ChosenPlanTomorrowReminderToConsultantNotification.fromJson(base, NotificationType.chosenPlanTomorrowReminderToConsultant); + PlanSubscriptionPaidToAuthorNotification.fromJson(base, NotificationType.planSubscriptionPaidToAuthor); + ChosenPlanFinishedToConsultantNotification.fromJson(base, NotificationType.chosenPlanFinishedToConsultant); + ChosenPlanLastDayFinishedForUser.fromJson(base, NotificationType.chosenPlanLastDayFinishedForUser); + ChosenPlanConsultantLastDayStarted.fromJson(base, NotificationType.chosenPlanConsultantLastDayStarted); + ComplaintToModerationForUser.fromJson(base, NotificationType.complaintToModerationForUser); + CommentReplied.fromJson(commentReply, NotificationType.commentReplied); + ScoreAdded.fromJson(scoreLike, NotificationType.scoreAdded); + ScoreSubstractedNotification.fromJson(scoreLike, NotificationType.scoreSubstracted); + EntityCommented.fromJson(entityCommented, NotificationType.entityCommented); + UnknownNotification.fromJson(base, NotificationType.unknown); + + ConsultantConfirmPlanSubscriptionToSubscriberNotification.fromJson( + base, + NotificationType.consultantConfirmPlanSubscriptionToSubscriber, + ); + ClientPaidPlanSubscriptionToAuthorNotification.fromJson(base, NotificationType.clientPaidPlanSubscriptionToAuthor); + ConsultantLeftDiaryCommentNotification.fromJson(base, NotificationType.consultantLeftDiaryComment); + ConsultantConfirmPlanSubscriptionToAuthorNotification.fromJson(base, NotificationType.consultantConfirmPlanSubscriptionToAuthor); + ClientPaidPlanSubscriptionToSubscriberNotification.fromJson(base, NotificationType.clientPaidPlanSubscriptionToSubscriber); + ClientStartPlanSubscriptionToAuthorNotification.fromJson(base, NotificationType.clientStartPlanSubscriptionToAuthor); + ClientCancelPlanSubscriptionToAuthorNotification.fromJson(base, NotificationType.clientCancelPlanSubscriptionToAuthor); + ClientCancelPlanSubscriptionToSubscriberNotification.fromJson(base, NotificationType.clientCancelPlanSubscriptionToSubscriber); + UserDailyReportCompletedNotification.fromJson(base, NotificationType.userDailyReportCompleted); + StayOnTrackToCustomerNotification.fromJson(base, NotificationType.stayOnTrackToCustomer); + PhoneChangedNotification.fromJson(base, NotificationType.phoneChanged); + SubscriptionNotification.fromJson(base, NotificationType.subscription); + BadgeAwardedNotification.fromJson(base, NotificationType.badgeAwarded); + BadgeRevokedNotification.fromJson(base, NotificationType.badgeRevoked); + ConsultantRequestStartedNotification.fromJson(base, NotificationType.consultantRequestStarted); + ClientCloseComplaintSuccessNotification.fromJson(base, NotificationType.clientCloseComplaintSuccess); + ClientRefundSuccessNotification.fromJson(base, NotificationType.clientRefundSuccess); + ConsultantRefundSuccessNotification.fromJson(base, NotificationType.consultantRefundSuccess); + ClientRejectSuccessNotification.fromJson(base, NotificationType.clientRejectSuccess); + ConsultantRejectSuccessNotification.fromJson(base, NotificationType.consultantRejectSuccess); + ProductStatusUpdatedForAuthorNotification.fromJson(base, NotificationType.productStatusUpdatedForAuthor); + DiaryReminderForUsersNotification.fromJson(base, NotificationType.diaryReminderForUsers); + DiaryReminderForUsersNotification.fromJson({'created_at': c, 'data': {}}, NotificationType.diaryReminderForUsers); + PersonalDietIsDefinedToCustomerNotification.fromJson(base, NotificationType.personalDietIsDefinedToCustomer); + ConsultantEducationUpdatedNotification.fromJson(base, NotificationType.consultantEducationUpdated); + ComplaintRegisteredNotification.fromJson(base, NotificationType.complaintRegistered); + ContentUnpublishedNotification.fromJson(base, NotificationType.contentUnpublished); + FinanceQuestionCreatedNotification.fromJson(base, NotificationType.financeQuestionCreated); + NewChatMessageNotification.fromJson(base, NotificationType.newChatMessage); + CompleteRegistrationNotification.fromJson(base, NotificationType.completeRegistration); + ChosenDietExpiredNotification.fromJson(base, NotificationType.chosenDietExpired); + MotivateUserNotification.fromJson(base, NotificationType.motivateUser); + CommentComplaintNotification.fromJson(base, NotificationType.commentComplaint); + CommentDeletedNotification.fromJson(base, NotificationType.commentDeleted); + UserSubscribedNotification.fromJson(base, NotificationType.userSubscribed); + }); + + test('executes additional factories used by legacy and plan flows', () { + ClientStartPlanSubscriptionToSubscriberNotification.fromJson(base, NotificationType.clientStartPlanSubscriptionToSubscriber); + ChosenPlanFinishedToClientNotification.fromJson(base, NotificationType.chosenPlanFinishedToClient); + PlanSubscriptionCanceledToCustomerNotification.fromJson(base, NotificationType.planSubscriptionCanceledToCustomer); + PlanSubscriptionCanceledToAuthorNotification.fromJson(planAndUser, NotificationType.planSubscriptionCanceledToAuthor); + ChosenPlanStartedToClientNotification.fromJson(planAndUser, NotificationType.chosenPlanStartedToClient); + ChosenPlanTomorrowReminderToClientNotification.fromJson(planAndUser, NotificationType.chosenPlanTomorrowReminderToClient); + DiaryReminderNotification.fromJson(base, NotificationType.diaryReminderNotification); + ConsultantLeftCommentNotification.fromJson(base, NotificationType.consultantLeftComment); + PersonalizedDietIsDefinedNotification.fromJson(base, NotificationType.personalizedDietIsDefined); + PlanSubscriptionStartedToAuthorNotification.fromJson(planAndUser, NotificationType.planSubscriptionStartedToAuthor); + }); + }); +} diff --git a/test/features/plans/presentation/cubit/about_plan_cubit_test.dart b/test/features/plans/presentation/cubit/about_plan_cubit_test.dart index 52b9bc0b..ccc1b39a 100644 --- a/test/features/plans/presentation/cubit/about_plan_cubit_test.dart +++ b/test/features/plans/presentation/cubit/about_plan_cubit_test.dart @@ -71,7 +71,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); expect(aboutPlanCubit!.state.planModel, isNotNull); expect(aboutPlanCubit!.state.planModel!.id, 1); @@ -157,7 +157,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => {}); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); await aboutPlanCubit!.likePlan(index: null, slug: 'test-plan'); await Future.delayed(const Duration(milliseconds: 1200)); @@ -170,7 +170,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenThrow(Exception('Ошибка загрузки')); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); expect(aboutPlanCubit!.state.error, isNotNull); expect(aboutPlanCubit!.state.isLoading, isFalse); @@ -220,7 +220,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => {}); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); await aboutPlanCubit!.likePlan(index: null, slug: 'test-plan'); expect(aboutPlanCubit!.state.planModel!.wasLikedByMe, isFalse); @@ -263,7 +263,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.addToFavorite('test-plan')).thenAnswer((_) async => Future.value()); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); await aboutPlanCubit!.addToFavorite(slug: 'test-plan', index: null); expect(aboutPlanCubit!.state.planModel!.isFavorite, true); @@ -305,7 +305,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.removeFromFavorite('test-plan')).thenAnswer((_) async => Future.value()); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); await aboutPlanCubit!.removeFromFavorite(slug: 'test-plan', index: null); expect(aboutPlanCubit!.state.planModel!.isFavorite, false); @@ -357,7 +357,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.chosePlan('test-plan', any)).thenAnswer((_) async => chosenPlan); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); aboutPlanCubit!.choseOption(0); final result = await aboutPlanCubit!.chosePlan(); @@ -405,7 +405,7 @@ void main() { ), ); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); aboutPlanCubit!.choseOption(0); final result = await aboutPlanCubit!.chosePlan(); @@ -447,7 +447,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => {}); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); await aboutPlanCubit!.likePlan(index: 5, slug: 'test-plan'); expect(aboutPlanCubit!.state.planModel!.wasLikedByMe, isTrue); @@ -490,7 +490,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.likePlan('test-plan')).thenAnswer((_) async => {}); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); await aboutPlanCubit!.likePlan(index: 5, slug: 'test-plan'); expect(aboutPlanCubit!.state.planModel!.wasLikedByMe, isFalse); @@ -533,7 +533,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.likePlan('test-plan')).thenThrow(Exception('Ошибка')); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); await aboutPlanCubit!.likePlan(index: 5, slug: 'test-plan'); // Проверяем, что состояние изменилось сразу после вызова @@ -583,7 +583,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.addToFavorite('test-plan')).thenAnswer((_) async => Future.value()); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); await aboutPlanCubit!.addToFavorite(slug: 'test-plan', index: 5); expect(aboutPlanCubit!.state.planModel!.isFavorite, true); @@ -624,7 +624,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.addToFavorite('test-plan')).thenThrow(Exception('Ошибка')); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); await aboutPlanCubit!.addToFavorite(slug: 'test-plan', index: 5); // Проверяем, что состояние изменилось сразу после вызова @@ -673,7 +673,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.removeFromFavorite('test-plan')).thenAnswer((_) async => Future.value()); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); await aboutPlanCubit!.removeFromFavorite(slug: 'test-plan', index: 5); expect(aboutPlanCubit!.state.planModel!.isFavorite, false); @@ -714,7 +714,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.removeFromFavorite('test-plan')).thenThrow(Exception('Ошибка')); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); await aboutPlanCubit!.removeFromFavorite(slug: 'test-plan', index: 5); // Проверяем, что состояние изменилось сразу после вызова @@ -773,7 +773,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.chosePlan('test-plan', any)).thenAnswer((_) async => chosenPlan); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); aboutPlanCubit!.choseOption(1); final result = await aboutPlanCubit!.chosePlan(); @@ -826,7 +826,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.chosePlan('test-plan', any)).thenAnswer((_) async => chosenPlan); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); aboutPlanCubit!.choseOption(2); final result = await aboutPlanCubit!.chosePlan(); @@ -884,7 +884,7 @@ void main() { ), ); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); aboutPlanCubit!.choseOption(0); final result = await aboutPlanCubit!.chosePlan(); @@ -926,7 +926,7 @@ void main() { when(mockPlanRepository.getPlan('test-plan')).thenAnswer((_) async => testPlan); when(mockPlanRepository.chosePlan('test-plan', any)).thenThrow(Exception('Общая ошибка')); - await aboutPlanCubit!.getPlan('test-plan'); + await aboutPlanCubit!.getPlan(slug: 'test-plan'); aboutPlanCubit!.choseOption(0); final result = await aboutPlanCubit!.chosePlan(); diff --git a/test/features/plans/presentation/pages/about_plan_screen_smoke_test.dart b/test/features/plans/presentation/pages/about_plan_screen_smoke_test.dart index 4fa46bb3..040271d1 100644 --- a/test/features/plans/presentation/pages/about_plan_screen_smoke_test.dart +++ b/test/features/plans/presentation/pages/about_plan_screen_smoke_test.dart @@ -4,6 +4,7 @@ import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; @@ -167,7 +168,7 @@ void main() { when(planCubit.state).thenReturn(state); when(planCubit.stream).thenAnswer((_) => const Stream.empty()); when(planCubit.close()).thenAnswer((_) async => Future.value()); - when(planCubit.getPlan(any)).thenAnswer((_) async => Future.value()); + when(planCubit.getPlan(slug: anyNamed('slug'), index: anyNamed('index'))).thenAnswer((_) async => Future.value()); when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future.value()); when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); when(feedCubit.close()).thenAnswer((_) async => Future.value()); @@ -183,6 +184,95 @@ void main() { expect(find.byType(AboutPlanScreen), findsOneWidget); }); + testWidgets('error state try again calls getPlan', (tester) async { + final planCubit = MockAboutPlanCubit(); + final feedCubit = MockFeedCubit(); + final commentsCubit = MockCommentsCubit(); + const state = AboutPlanState(isLoading: false, error: 'Error loading plan', planModel: testPlan, chosenOptionIndex: 0); + + when(planCubit.state).thenReturn(state); + when(planCubit.stream).thenAnswer((_) => const Stream.empty()); + when(planCubit.close()).thenAnswer((_) async => Future.value()); + when(planCubit.getPlan(slug: anyNamed('slug'), index: anyNamed('index'))).thenAnswer((_) async => Future.value()); + when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(planCubit, commentsCubit); + + await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null))); + await tester.pump(); + + await tester.tap(find.text('Попробовать снова')); + await tester.pump(); + + verify(planCubit.getPlan(slug: 'test', index: null)).called(1); + }); + + testWidgets('loading state back button does not throw', (tester) async { + final planCubit = MockAboutPlanCubit(); + final feedCubit = MockFeedCubit(); + final commentsCubit = MockCommentsCubit(); + const state = AboutPlanState(isLoading: true, error: null, planModel: null, chosenOptionIndex: 0); + + when(planCubit.state).thenReturn(state); + when(planCubit.stream).thenAnswer((_) => const Stream.empty()); + when(planCubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(planCubit, commentsCubit); + + await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null))); + await tester.pump(); + + await tester.tap(find.byType(BackButton)); + await tester.pump(); + + expect(find.byType(AboutPlanScreen), findsOneWidget); + }); + + testWidgets('loaded state bookmark tap calls addToFavorite when not favorite', (tester) async { + final planCubit = MockAboutPlanCubit(); + final feedCubit = MockFeedCubit(); + final commentsCubit = MockCommentsCubit(); + const state = AboutPlanState(isLoading: false, planModel: testPlan, chosenOptionIndex: 0, error: null); + + when(planCubit.state).thenReturn(state); + when(planCubit.stream).thenAnswer((_) => const Stream.empty()); + when(planCubit.close()).thenAnswer((_) async => Future.value()); + when(planCubit.addToFavorite(slug: anyNamed('slug'), index: anyNamed('index'))).thenAnswer((_) async => Future.value()); + when(feedCubit.viewPlan(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(planCubit, commentsCubit); + + await mockNetworkImagesFor(() async { + await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test-plan', navigateToComments: false, index: 7, highlightedCommentId: null))); + await tester.pump(); + await tester.pump(); + }); + + await tester.tap(find.byType(BookmarkButton)); + await tester.pump(); + + verify(planCubit.addToFavorite(slug: 'test-plan', index: 7)).called(1); + }); + testWidgets('renders with navigateToComments true', (tester) async { final planCubit = MockAboutPlanCubit(); final feedCubit = MockFeedCubit(); diff --git a/test/features/recipe/presentation/pages/about_recipe_screen_smoke_test.dart b/test/features/recipe/presentation/pages/about_recipe_screen_smoke_test.dart index 9bc22016..5b1eb97a 100644 --- a/test/features/recipe/presentation/pages/about_recipe_screen_smoke_test.dart +++ b/test/features/recipe/presentation/pages/about_recipe_screen_smoke_test.dart @@ -194,6 +194,63 @@ void main() { expect(find.byType(AboutRecipeScreen), findsOneWidget); }); + testWidgets('error state try again calls getRecipe', (tester) async { + final recipeCubit = MockAboutRecipeCubit(); + final feedCubit = MockFeedCubit(); + final commentsCubit = MockCommentsCubit(); + const state = AboutRecipeState.error(message: 'Error loading recipe'); + + when(recipeCubit.state).thenReturn(state); + when(recipeCubit.stream).thenAnswer((_) => const Stream.empty()); + when(recipeCubit.close()).thenAnswer((_) async => Future.value()); + when(recipeCubit.getRecipe(any, index: anyNamed('index'))).thenAnswer((_) async => Future.value()); + when(feedCubit.viewRecipe(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(recipeCubit, commentsCubit); + + await tester.pumpWidget(wrap(recipeCubit, feedCubit, commentsCubit, const AboutRecipeScreen(recipeSlug: 'test', index: 3, navigateToComments: false))); + await tester.pump(); + + await tester.tap(find.text('Попробовать снова')); + await tester.pump(); + + verify(recipeCubit.getRecipe('test', index: 3)).called(1); + }); + + testWidgets('loading state back button does not throw', (tester) async { + final recipeCubit = MockAboutRecipeCubit(); + final feedCubit = MockFeedCubit(); + final commentsCubit = MockCommentsCubit(); + const state = AboutRecipeState.loading(); + + when(recipeCubit.state).thenReturn(state); + when(recipeCubit.stream).thenAnswer((_) => const Stream.empty()); + when(recipeCubit.close()).thenAnswer((_) async => Future.value()); + when(feedCubit.viewRecipe(any)).thenAnswer((_) async => Future.value()); + when(feedCubit.stream).thenAnswer((_) => const Stream.empty()); + when(feedCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.state).thenReturn(const CommentsState()); + when(commentsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(commentsCubit.close()).thenAnswer((_) async => Future.value()); + when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future.value()); + + registerMocks(recipeCubit, commentsCubit); + + await tester.pumpWidget(wrap(recipeCubit, feedCubit, commentsCubit, const AboutRecipeScreen(recipeSlug: 'test', index: null, navigateToComments: false))); + await tester.pump(); + + await tester.tap(find.byType(BackButton)); + await tester.pump(); + + expect(find.byType(AboutRecipeScreen), findsOneWidget); + }); + testWidgets('renders with navigateToComments true', (tester) async { final recipeCubit = MockAboutRecipeCubit(); final feedCubit = MockFeedCubit(); From 966bc23f0b13a1f4a5d282df5d20ad5661febdb1 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 1 Apr 2026 10:49:50 +0300 Subject: [PATCH 06/18] feat(CI/CD): removed proxy server --- .gitea/workflows/test.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 38d8541b..6c637035 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -16,15 +16,15 @@ jobs: steps: - uses: https://github.com/actions/checkout@v3 - - name: Set proxy server - env: - PROXY_USER: ${{ secrets.PROXY_USER }} - PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }} - run: | - echo "Setting up proxy..." - cat </etc/apt/apt.conf.d/proxy.conf - Acquire::http::Proxy "socks5h://${PROXY_USER}:${PROXY_PASSWORD}@10.55.5.202:19843/"; - EOF + # - name: Set proxy server + # env: + # PROXY_USER: ${{ secrets.PROXY_USER }} + # PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }} + # run: | + # echo "Setting up proxy..." + # cat </etc/apt/apt.conf.d/proxy.conf + # Acquire::http::Proxy "socks5h://${PROXY_USER}:${PROXY_PASSWORD}@10.55.5.202:19843/"; + # EOF - name: Checkout env repository uses: https://github.com/actions/checkout@v3 From 6629286bf184210c33449e47018145b4af622509 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 1 Apr 2026 10:59:54 +0300 Subject: [PATCH 07/18] feat(CI/CD): changed gitea user token name --- .gitea/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 6c637035..b27aed6c 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -16,6 +16,7 @@ jobs: steps: - uses: https://github.com/actions/checkout@v3 + # Пока что не нужно проксирование # - name: Set proxy server # env: # PROXY_USER: ${{ secrets.PROXY_USER }} @@ -31,7 +32,8 @@ jobs: with: repository: eatvio/eatvio_app_env path: env_repo - token: ${{ vars.MISHA_GITEA_TOKEN }} + # Здесь может быть токен любого юзера с доступом в gitea + token: ${{ vars.READ_USER_GITEA_TOKEN }} ref: main - name: Copy .env file From b6b13b1db572b9fa6f690915c1425633c14c6617 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 1 Apr 2026 13:21:19 +0300 Subject: [PATCH 08/18] version update --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 42018d66..0af67d5b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.3.19+99 +version: 1.3.20+100 publish_to: 'none' environment: From aac085def473f8f8df689e9bd49ed7245b4ce44f Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 7 Apr 2026 10:05:11 +0300 Subject: [PATCH 09/18] feat(app): refactoring of sentry using, feat(become_consultant): fixed possible infinite loading, feat(push_notifications): fixed push navigation from paid plan push --- lib/core/app_initializer.dart | 11 +- lib/core/app_router.dart | 27 +- .../comments_cubit/comments_cubit.dart | 9 +- .../repositories/comments_repository.dart | 17 +- lib/core/cubit/marketing/marketing_cubit.dart | 6 +- lib/core/di/external_registrations.dart | 8 +- lib/core/di/service_registrations.dart | 2 +- .../error_reporting/error_report_policy.dart | 80 +++ lib/core/error_reporting/error_reporter.dart | 9 + .../error_reporting/no_op_error_reporter.dart | 14 + lib/core/error_reporting/report_from.dart | 17 + .../sentry_error_reporter.dart | 33 ++ lib/core/firebase_api.dart | 13 +- .../internet_connection_cubit.dart | 17 +- lib/core/locator_service.dart | 2 +- lib/core/routing/other_routes.dart | 7 +- lib/core/views/cubit/views_cubit.dart | 12 +- .../views/repository/views_repository.dart | 9 +- lib/core/websockets/ws_manager.dart | 10 +- .../bloc/auth_cubit/auth_cubit.dart | 30 +- .../bloc/oauth_cubit/oauth_cubit.dart | 17 +- .../reset_password_cubit.dart | 7 +- .../auth/repositories/auth_repository.dart | 142 ++--- .../reset_password_repository.dart | 59 +- lib/features/auth/sdk/social_auth_facade.dart | 9 +- .../repositories/barcode_repository.dart | 75 ++- .../presentation/cubit/blog_post_cubit.dart | 3 + .../blog_post_create_cubit.dart | 16 +- .../repositories/blog_post_repository.dart | 69 +-- .../profile_avatar_cubit.dart | 12 +- .../presentation/bloc/calendar_cubit.dart | 4 +- .../repositories/calendar_repository.dart | 9 +- .../cubit/cancel_plan_cubit.dart | 2 + .../repository/cancel_plan_repository.dart | 9 +- .../bloc/chat_room_cubit/chat_room_cubit.dart | 44 +- .../bloc/chats_cubit/chats_cubit.dart | 68 ++- .../chat/repositories/chat_repository.dart | 154 ++--- .../presentation/cubit/complaining_cubit.dart | 2 + .../repositories/complaining_repository.dart | 49 +- .../register_consultant_cubit.dart | 55 +- .../register_consultant_state.dart | 1 + .../stepper_become_consultant_screen.dart | 45 +- .../repositories/consultant_repository.dart | 91 +-- .../cubit/consultant_rating_cubit.dart | 3 + .../consultant_rating_repository.dart | 19 +- .../cubit/consultations_cubit.dart | 2 + .../consultations_repository.dart | 50 +- .../presentation/cubit/create_diet_cubit.dart | 52 +- .../repositories/create_diet_repository.dart | 96 ++-- .../presentation/cubit/create_plan_cubit.dart | 4 +- .../repositories/create_plan_repository.dart | 11 +- .../create_recipe_cubit.dart | 111 ++-- .../reporitories/recipe_repository.dart | 75 +-- .../presentation/dev_tools_screen.dart | 6 +- lib/features/dev_tools/test_cases.dart | 5 +- .../consultant_comments_cubit.dart | 2 + .../diary/presentation/bloc/diary_cubit.dart | 203 +++++-- .../diary/repositories/diary_repository.dart | 418 ++++---------- .../presentation/cubit/about_diet_cubit.dart | 26 +- .../diets/repositories/diet_repository.dart | 67 +-- .../consultants_cubit/consultants_cubit.dart | 2 + .../bloc/diets_cubit/diets_cubit.dart | 17 +- .../bloc/feed_cubit/feed_cubit.dart | 51 +- .../bloc/plans_cubit/plans_cubit.dart | 25 +- .../bloc/profile_cubit/profile_cubit.dart | 2 + .../bloc/recipes_cubit/recipes_cubit.dart | 17 +- .../feed/repositories/feed_repository.dart | 169 ++---- .../archive/consultations_archive_cubit.dart | 7 +- .../consultant_finances_cubit.dart | 4 +- .../user_purchases/user_purchases_cubit.dart | 4 +- .../repositories/glossary_repository.dart | 9 +- .../bloc/menu_cubit/menu_cubit.dart | 4 +- .../notification_settings_cubit.dart | 3 + .../blocked_users_cubit.dart | 18 +- .../menu/presentation/pages/menu_screen.dart | 4 +- .../blocked_users_repository.dart | 45 +- .../notification_settings_repository.dart | 17 +- .../presentation/cubit/my_diets_cubit.dart | 13 +- .../repositories/my_diets_repository.dart | 31 +- .../user_cards_cubit/user_cards_cubit.dart | 6 + .../repositories/cards_repository.dart | 41 +- .../presentation/cubit/my_plans_cubit.dart | 13 +- .../repositories/my_diets_repository.dart | 28 +- .../notifications_cubit.dart | 27 +- .../repository/notifications_repository.dart | 19 +- .../order_info/cubit/order_info_cubit.dart | 19 +- .../presentation/info_by_order_screen.dart | 28 +- .../cubit/personal_diet_suggestion_cubit.dart | 5 + .../personal_diet_suggestion_repository.dart | 25 +- .../presentation/cubit/about_plan_cubit.dart | 14 +- .../plans/repositories/plan_repository.dart | 41 +- .../plans/repositories/plans_repository.dart | 11 +- .../bloc/plans_cubit/plans_cubit.dart | 4 +- .../bloc/profile_cubit/profile_cubit.dart | 40 +- .../bloc/sub_cubit/sub_cubit.dart | 19 +- .../repositories/profile_repository.dart | 178 ++---- .../cubit/about_recipe_cubit.dart | 16 +- .../my_recipes_cubit/my_recipes_cubit.dart | 32 +- .../widgets/my_recipe_widget.dart | 11 +- .../repositories/about_recipe_repository.dart | 41 +- .../repositories/my_recipes_repository.dart | 9 +- .../registration_cubit.dart | 20 +- .../verify_email_cubit.dart | 26 +- .../registration_error_message.dart | 35 ++ .../repositories/registration_repository.dart | 107 +--- .../cubit/registration_cubit.dart | 537 +++++++++--------- .../registration_repository_v2.dart | 11 +- .../bloc/settings_cubit/settings_cubit.dart | 35 +- .../repositories/service_repository.dart | 49 +- .../cubit/user_details_cubit.dart | 5 +- .../cubit/marketing/marketing_cubit_test.dart | 5 +- .../error_report_policy_test.dart | 114 ++++ test/core/firebase_api_test.dart | 9 +- test/core/websockets/ws_manager_test.dart | 43 +- .../bloc/oauth_cubit/oauth_cubit_test.dart | 35 +- .../reset_password_repository_test.dart | 2 +- .../repositories/barcode_repository_test.dart | 48 +- .../repositories/chat_repository_test.dart | 9 +- .../glossary_repository_test.dart | 150 ++--- .../blocked_users_repository_test.dart | 2 +- ...sonal_diet_suggestion_repository_test.dart | 2 +- .../presentation/cubit/plans_cubit_test.dart | 6 +- .../profile_cubit/profile_cubit_test.dart | 2 +- .../repositories/profile_repository_test.dart | 10 +- .../cubit/my_recipes_cubit_test.dart | 3 +- .../registration_cubit_test.dart | 10 +- .../verify_email_cubit_test.dart | 2 +- .../registration_repository_test.dart | 5 +- test/fixtures/doubles/auth_doubles.dart | 3 +- test/fixtures/doubles/blog_post_doubles.dart | 3 +- .../fixtures/doubles/create_diet_doubles.dart | 6 +- .../fixtures/doubles/create_plan_doubles.dart | 3 +- test/fixtures/doubles/profile_doubles.dart | 9 +- 133 files changed, 2344 insertions(+), 2456 deletions(-) create mode 100644 lib/core/error_reporting/error_report_policy.dart create mode 100644 lib/core/error_reporting/error_reporter.dart create mode 100644 lib/core/error_reporting/no_op_error_reporter.dart create mode 100644 lib/core/error_reporting/report_from.dart create mode 100644 lib/core/error_reporting/sentry_error_reporter.dart create mode 100644 lib/features/registration/registration_error_message.dart create mode 100644 test/core/error_reporting/error_report_policy_test.dart diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart index a4e91210..e9e3e9f6 100644 --- a/lib/core/app_initializer.dart +++ b/lib/core/app_initializer.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'dart:io' as io; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/env_config.dart'; +import 'package:eatvio_app/core/error_reporting/error_report_policy.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'; @@ -16,6 +17,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; /// Класс для инициализации приложения class AppInitializer { + /// Инициализация всех сервисов приложения static Future initialize({required AppBuildType buildType}) async { await _initializeEnvironment(); @@ -79,11 +81,12 @@ class AppInitializer { options.dsn = EnvConfig.sentryDsn; options.tracesSampleRate = 1.0; options.debug = false; + options.beforeSend = (event, hint) { + if (kDebugMode) return null; + if (!ErrorReportPolicy.allowSentryEvent(event, hint)) return null; + return event; + }; }, appRunner: appRunner); - FlutterError.onError = (details) async { - if (kDebugMode) return; - await Sentry.captureException(details.exception, stackTrace: details.stack); - }; } } diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 13f139a1..2b32db51 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -7,6 +7,7 @@ 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:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:go_router/go_router.dart'; @@ -35,7 +36,6 @@ class AppRouter { if (effectiveSegments.length != 2) return null; final contentIdentifier = effectiveSegments[1]; developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); return '/about-recipes-screen?slug=$contentIdentifier'; } @@ -43,7 +43,6 @@ class AppRouter { if (effectiveSegments.length != 2) return null; final contentIdentifier = effectiveSegments[1]; developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); return '/blog-post-screen?id=$contentIdentifier'; } @@ -53,11 +52,9 @@ class AppRouter { final planId = int.tryParse(contentIdentifier); if (planId != null) { developer.log('Deep link: diary', name: 'AppRouter'); - FlutterNativeSplash.remove(); return RouteConstants.diaryScreen; } developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); return '/about-plan-screen?slug=$contentIdentifier'; } @@ -67,11 +64,9 @@ class AppRouter { final dietId = int.tryParse(contentIdentifier); if (dietId != null) { developer.log('Deep link: diet/id=$dietId', name: 'AppRouter'); - FlutterNativeSplash.remove(); return '/about-diet-screen?id=$dietId'; } developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); return '/about-diet-screen?slug=$contentIdentifier'; } const dietTabIndexWithHiddenPlansTab = 3; @@ -85,17 +80,14 @@ class AppRouter { if (effectiveSegments.length == 2) { final contentIdentifier = effectiveSegments[1]; developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); return '${RouteConstants.chatScreen}?uuid=$contentIdentifier'; } else if (effectiveSegments.length == 3 && effectiveSegments[1] == 'show') { final contentIdentifier = effectiveSegments[2]; developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); return '${RouteConstants.chatScreen}?uuid=$contentIdentifier'; } developer.log('Deep link: chat', name: 'AppRouter'); - FlutterNativeSplash.remove(); return RouteConstants.chatSelectScreen; } @@ -103,7 +95,6 @@ class AppRouter { if (effectiveSegments.length == 2) { final contentIdentifier = effectiveSegments[1]; developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter'); - FlutterNativeSplash.remove(); return '/profile-screen?slug=$contentIdentifier'; } return RouteConstants.myProfileScreen; @@ -115,7 +106,6 @@ class AppRouter { static String? _resolveDiaryDeepLink(List effectiveSegments) { developer.log('Deep link: diary', name: 'AppRouter'); - FlutterNativeSplash.remove(); return RouteConstants.diaryScreen; } @@ -123,7 +113,6 @@ class AppRouter { if (effectiveSegments.length != 2) return null; final contentIdentifier = effectiveSegments[1]; developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter'); - FlutterNativeSplash.remove(); return RouteConstants.diaryScreen; } @@ -131,8 +120,7 @@ class AppRouter { if (effectiveSegments.length != 3 || effectiveSegments[1] != 'status') return RouteConstants.consultationsScreen; final consultationId = effectiveSegments[2]; developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter'); - FlutterNativeSplash.remove(); - return '${RouteConstants.consultationsScreen}?id=$consultationId'; + return '${InfoByOrderScreen.path}?planId=$consultationId'; } static String? _resolveDeepLinkUri(Uri uri) { @@ -149,14 +137,12 @@ class AppRouter { if (segments.isEmpty) { developer.log('Empty segments in deep link, redirecting to feed', name: 'AppRouter'); - FlutterNativeSplash.remove(); return RouteConstants.feedScreen; } final effectiveSegments = segments.where((segment) => !metaSegments.contains(segment)).toList(); if (effectiveSegments.isEmpty) { developer.log('No effective segments after language check, redirecting to feed', name: 'AppRouter'); - FlutterNativeSplash.remove(); return RouteConstants.feedScreen; } @@ -206,7 +192,6 @@ class AppRouter { } developer.log('Unknown deep link format: ${effectiveSegments.length} effective segments, type=$type, redirecting to feed', name: 'AppRouter'); - FlutterNativeSplash.remove(); return RouteConstants.feedScreen; } @@ -246,12 +231,14 @@ class AppRouter { } final target = _resolveDeepLinkUri(uri); + print('target: $target'); + FlutterNativeSplash.remove(); if (target != null) { appRouter.go(target); - } else { - developer.log('navigateByDeepLinkUrl: unresolved url=$url (normalized=${uri.toString()}), redirecting to feed', name: 'AppRouter'); - appRouter.go(RouteConstants.feedScreen); + return; } + developer.log('navigateByDeepLinkUrl: unresolved url=$url (normalized=${uri.toString()}), redirecting to feed', name: 'AppRouter'); + appRouter.go(RouteConstants.feedScreen); } static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) { diff --git a/lib/core/comments/comments_cubit/comments_cubit.dart b/lib/core/comments/comments_cubit/comments_cubit.dart index a65a9c6e..71adc23a 100644 --- a/lib/core/comments/comments_cubit/comments_cubit.dart +++ b/lib/core/comments/comments_cubit/comments_cubit.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/comments/repositories/comments_repository.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class CommentsCubit extends Cubit { final FeedRepository feedRepository; @@ -33,7 +34,8 @@ class CommentsCubit extends Cubit { emit(CommentsState.loaded(comment: await feedRepository.getPlansComments(slug: id))); break; } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(CommentsState.error(errorMessage: e.toString())); } } @@ -56,7 +58,8 @@ class CommentsCubit extends Cubit { } getComments(id, commentSourceType); return true; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(CommentsState.error(errorMessage: e.toString())); return false; } @@ -71,6 +74,7 @@ class CommentsCubit extends Cubit { emit(CommentsState.loaded(comment: updatedCommentModel)); return true; } catch (e, st) { + await reportFrom(e, st, this); emit(CommentsState.loaded(comment: commentModel)); log(e.toString(), stackTrace: st); } @@ -87,6 +91,7 @@ class CommentsCubit extends Cubit { emit(CommentsState.loaded(comment: updatedCommentModel)); return true; } catch (e, st) { + await reportFrom(e, st, this); emit(CommentsState.loaded(comment: commentModel)); log(e.toString(), stackTrace: st); } diff --git a/lib/core/comments/repositories/comments_repository.dart b/lib/core/comments/repositories/comments_repository.dart index 9ad676c9..4a0b91a7 100644 --- a/lib/core/comments/repositories/comments_repository.dart +++ b/lib/core/comments/repositories/comments_repository.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/core/comments/models/comment_update_model.dart'; import 'package:eatvio_app/core/comments/services/comments_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class CommentsRepository { final CommentsService service; @@ -8,20 +7,12 @@ class CommentsRepository { CommentsRepository({required this.service}); Future deleteComment({required int id}) async { - try { - return service.deleteComment(id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return service.deleteComment(id); + } Future updateComment({required int id, required String body}) async { - try { - return service.updateComment(id, CommentUpdateModel(body: body)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return service.updateComment(id, CommentUpdateModel(body: body)); + } } diff --git a/lib/core/cubit/marketing/marketing_cubit.dart b/lib/core/cubit/marketing/marketing_cubit.dart index cf5b0c13..7f058a47 100644 --- a/lib/core/cubit/marketing/marketing_cubit.dart +++ b/lib/core/cubit/marketing/marketing_cubit.dart @@ -2,6 +2,7 @@ import 'package:eatvio_app/core/cubit/marketing/marketing_cubit_state.dart'; import 'package:eatvio_app/core/prefs.dart'; import 'package:eatvio_app/core/prefs_keys.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class MarketingCubit extends Cubit { final Prefs prefs; @@ -22,8 +23,9 @@ class MarketingCubit extends Cubit { await prefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true); } return needToShowMarketingBottomSheet; - } catch (e) { - rethrow; + } catch (e, st) { + await reportFrom(e, st, this); + return false; } } } diff --git a/lib/core/di/external_registrations.dart b/lib/core/di/external_registrations.dart index 34f93aa6..cb285f40 100644 --- a/lib/core/di/external_registrations.dart +++ b/lib/core/di/external_registrations.dart @@ -1,10 +1,13 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/constants/app_constants.dart'; +import 'package:eatvio_app/core/error_reporting/error_reporter.dart'; +import 'package:eatvio_app/core/error_reporting/sentry_error_reporter.dart'; import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; import 'package:eatvio_app/core/prefs.dart'; import 'package:get_it/get_it.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sentry_dio/sentry_dio.dart'; @@ -14,6 +17,7 @@ class ExternalRegistrations { /// Регистрация всех внешних зависимостей static void registerAll() { + sl.registerLazySingleton(SentryErrorReporter.new); _registerDio(); _registerPrefs(); } @@ -30,7 +34,9 @@ class ExternalRegistrations { sendTimeout: AppConstants.pendingDelay, ), )..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]) - ..addSentry(), + ..addSentry( + failedRequestStatusCodes: const [SentryStatusCode.defaultRange()], + ), ); } diff --git a/lib/core/di/service_registrations.dart b/lib/core/di/service_registrations.dart index 62439f4a..dd1371b1 100644 --- a/lib/core/di/service_registrations.dart +++ b/lib/core/di/service_registrations.dart @@ -72,7 +72,7 @@ class ServiceRegistrations { sl.registerLazySingleton(() => CancelPlanService(sl())); sl.registerLazySingleton(() => CreatePlanService(sl())); sl.registerLazySingleton(() => NotificationsService(sl())); - sl.registerLazySingleton(() => FirebaseApi()); + sl.registerLazySingleton(() => FirebaseApi(errorReporter: sl())); sl.registerLazySingleton(() => ResetPasswordService(sl())); sl.registerLazySingleton(() => BlockedUserService(sl())); sl.registerLazySingleton(() => CommentsService(sl())); diff --git a/lib/core/error_reporting/error_report_policy.dart b/lib/core/error_reporting/error_report_policy.dart new file mode 100644 index 00000000..5e813fd8 --- /dev/null +++ b/lib/core/error_reporting/error_report_policy.dart @@ -0,0 +1,80 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +/// Rules for which errors are sent to crash reporting. +/// +/// [shouldReport] is used by [ErrorReporter]. [allowSentryEvent] is used in +/// [SentryFlutter.init] as a last-line filter (Dio failed-request capture, etc.). +class ErrorReportPolicy { + ErrorReportPolicy._(); + + /// When `false`, the error is not sent via [ErrorReporter]. + static bool shouldReport(Object? error) { + if (error == null) return false; + if (error is String) return false; + if (error is SocketException) return false; + + if (error is DioException) { + return _shouldReportDio(error); + } + + return true; + } + + /// Used in SDK [SentryOptions.beforeSend]. Return `false` to drop the event. + /// + /// For [DioException], rules differ from [shouldReport]: events from `sentry_dio` + /// (e.g. 5xx) must pass through here, while [shouldReport] returns false for 5xx + /// so [SentryErrorReporter] does not duplicate them after `catch` in cubits / manual [report] calls. + static bool allowSentryEvent(SentryEvent event, Hint hint) { + final t = event.throwable; + if (t == null) return true; + if (t is DioException) { + return _allowSentrySdkDio(t); + } + return shouldReport(t); + } + + /// Cancel, timeouts, socket / connection errors — do not send (noisy, often user environment). + static bool _isTransientDioFailure(DioException e) { + switch (e.type) { + case DioExceptionType.cancel: + case DioExceptionType.connectionTimeout: + case DioExceptionType.sendTimeout: + case DioExceptionType.receiveTimeout: + case DioExceptionType.connectionError: + return true; + case DioExceptionType.unknown: + case DioExceptionType.badResponse: + case DioExceptionType.badCertificate: + break; + } + return e.error is SocketException; + } + + /// Noise to drop for any Dio-originated SDK event; do not use for manual [report]. + static bool _allowSentrySdkDio(DioException e) { + if (_isTransientDioFailure(e)) return false; + final code = e.response?.statusCode; + if (code != null && code >= 400 && code < 500) return false; + return true; + } + + static bool _shouldReportDio(DioException e) { + if (_isTransientDioFailure(e)) return false; + + final code = e.response?.statusCode; + if (code != null) { + if (code >= 500 && code <= 599) { + return false; + } + if (code >= 400 && code < 500) { + return false; + } + } + + return true; + } +} diff --git a/lib/core/error_reporting/error_reporter.dart b/lib/core/error_reporting/error_reporter.dart new file mode 100644 index 00000000..e61183e9 --- /dev/null +++ b/lib/core/error_reporting/error_reporter.dart @@ -0,0 +1,9 @@ +/// Application-wide error reporting (e.g. Sentry). +abstract class ErrorReporter { + Future report( + Object error, + StackTrace? stackTrace, { + String? origin, + Map? extras, + }); +} diff --git a/lib/core/error_reporting/no_op_error_reporter.dart b/lib/core/error_reporting/no_op_error_reporter.dart new file mode 100644 index 00000000..d9b2fd67 --- /dev/null +++ b/lib/core/error_reporting/no_op_error_reporter.dart @@ -0,0 +1,14 @@ +import 'package:eatvio_app/core/error_reporting/error_reporter.dart'; + +/// Reporting sink that does nothing (dev tooling, tests, diagnostics). +class NoOpErrorReporter implements ErrorReporter { + const NoOpErrorReporter(); + + @override + Future report( + Object error, + StackTrace? stackTrace, { + String? origin, + Map? extras, + }) async {} +} diff --git a/lib/core/error_reporting/report_from.dart b/lib/core/error_reporting/report_from.dart new file mode 100644 index 00000000..831953cd --- /dev/null +++ b/lib/core/error_reporting/report_from.dart @@ -0,0 +1,17 @@ +import 'package:eatvio_app/core/error_reporting/error_reporter.dart'; +import 'package:eatvio_app/core/error_reporting/no_op_error_reporter.dart'; +import 'package:eatvio_app/core/locator_service.dart'; + +/// Отправка в [ErrorReporter] с тегом origin = [context.runtimeType] (имя кубита/виджета и т.д.). +/// +/// Если [ErrorReporter] не зарегистрирован (например, после [GetIt.reset] в тестах), используется [NoOpErrorReporter]. +Future reportFrom(Object error, StackTrace? stackTrace, Object context) { + final reporter = sl.isRegistered() + ? sl() + : const NoOpErrorReporter(); + return reporter.report( + error, + stackTrace, + origin: context.runtimeType.toString(), + ); +} diff --git a/lib/core/error_reporting/sentry_error_reporter.dart b/lib/core/error_reporting/sentry_error_reporter.dart new file mode 100644 index 00000000..e0bd8192 --- /dev/null +++ b/lib/core/error_reporting/sentry_error_reporter.dart @@ -0,0 +1,33 @@ +import 'package:eatvio_app/core/error_reporting/error_report_policy.dart'; +import 'package:eatvio_app/core/error_reporting/error_reporter.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +class SentryErrorReporter implements ErrorReporter { + SentryErrorReporter(); + + @override + Future report( + Object error, + StackTrace? stackTrace, { + String? origin, + Map? extras, + }) async { + if (!ErrorReportPolicy.shouldReport(error)) return; + + await Sentry.captureException( + error, + stackTrace: stackTrace, + withScope: (scope) { + if (origin != null && origin.isNotEmpty) { + scope.setTag('origin', origin); + } + if (extras != null && extras.isNotEmpty) { + scope.setContexts( + 'report_extras', + extras.map((k, v) => MapEntry(k, v?.toString())), + ); + } + }, + ); + } +} diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index 1d09d7a3..2c8c64c8 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -11,11 +11,14 @@ 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:flutter/services.dart' show MethodChannel, PlatformException; -import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:eatvio_app/core/error_reporting/error_reporter.dart'; class FirebaseApi { + final ErrorReporter _errorReporter; final _firebaseMessaging = FirebaseMessaging.instance; + FirebaseApi({required ErrorReporter errorReporter}) : _errorReporter = errorReporter; + Future getToken() async { String? token; @@ -61,7 +64,7 @@ class FirebaseApi { ); } catch (e, st) { log(e.toString(), stackTrace: st); - await Sentry.captureException(e, stackTrace: st); + await _errorReporter.report(e, st, origin: 'FirebaseApi'); // Не пробрасываем ошибку дальше, так как это не критичная операция } } @@ -86,7 +89,7 @@ class FirebaseApi { ); } catch (e, st) { log(e.toString(), stackTrace: st); - await Sentry.captureException(e, stackTrace: st); + await _errorReporter.report(e, st, origin: 'FirebaseApi'); // Не пробрасываем ошибку дальше, так как это не критичная операция } } @@ -112,12 +115,12 @@ class FirebaseApi { } on PlatformException catch (e, st) { showErrorToast('$e $st'); log(e.toString(), stackTrace: st); - await Sentry.captureException(e, stackTrace: st); + await _errorReporter.report(e, st, origin: 'FirebaseApi'); return null; } catch (e, st) { showErrorToast('$e $st'); log(e.toString(), stackTrace: st); - await Sentry.captureException(e, stackTrace: st); + await _errorReporter.report(e, st, origin: 'FirebaseApi'); return null; } } diff --git a/lib/core/internet_connection_cubit/internet_connection_cubit.dart b/lib/core/internet_connection_cubit/internet_connection_cubit.dart index 67427c6c..ef77c185 100644 --- a/lib/core/internet_connection_cubit/internet_connection_cubit.dart +++ b/lib/core/internet_connection_cubit/internet_connection_cubit.dart @@ -8,6 +8,7 @@ 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:eatvio_app/core/error_reporting/report_from.dart'; import 'package:http/http.dart' as http; class InternetConnectionCubit extends Cubit { @@ -33,7 +34,8 @@ class InternetConnectionCubit extends Cubit { try { result = await _connectivity.checkConnectivity(); - } on PlatformException catch (e) { + } on PlatformException catch (e, st) { + await reportFrom(e, st, this); log('Couldn\'t check connectivity status', error: e); return; } @@ -42,9 +44,13 @@ class InternetConnectionCubit extends Cubit { } Future _updateAccessStatus(List result) async { - final hasInternet = await _checkInternetAccess(result); - if (!hasInternet) showInternetConnectionToast(); - emit(state.copyWith(isInternetAvailable: hasInternet)); + try { + final hasInternet = await _checkInternetAccess(result); + if (!hasInternet) showInternetConnectionToast(); + emit(state.copyWith(isInternetAvailable: hasInternet)); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future _checkInternetAccess(List result) async { @@ -57,7 +63,8 @@ class InternetConnectionCubit extends Cubit { try { final response = await http.get(uri).timeout(const Duration(seconds: 3)); return response.statusCode >= 100 && response.statusCode < 600; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); continue; } } diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 2f457269..6f06c668 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -32,5 +32,5 @@ Future init({required AppBuildType buildType}) async { CubitRegistrations.registerAll(); // Регистрация WebSocket менеджера - sl.registerLazySingleton(() => WsManager(wsService: sl())); + sl.registerLazySingleton(() => WsManager(wsService: sl(), errorReporter: sl())); } diff --git a/lib/core/routing/other_routes.dart b/lib/core/routing/other_routes.dart index 9d5200e3..58369798 100644 --- a/lib/core/routing/other_routes.dart +++ b/lib/core/routing/other_routes.dart @@ -211,7 +211,7 @@ class OtherRoutes { builder: (context, state) { final uuidFromQuery = state.uri.queryParameters['uuid']; if (uuidFromQuery != null) { - return ChatScreen(uuid: uuidFromQuery, sendInitialMessage: false); + return ChatScreen(uuid: uuidFromQuery, sendInitialMessage: false); } final data = state.extra as Map; final uuid = data['uuid'] as String; @@ -268,6 +268,11 @@ class OtherRoutes { path: InfoByOrderScreen.path, name: InfoByOrderScreen.name, builder: (context, state) { + final queryParams = state.uri.queryParameters; + final planIdFromQuery = int.tryParse(queryParams['planId'] ?? ''); + if (planIdFromQuery != null) { + return InfoByOrderScreen(planId: planIdFromQuery, successfulPayment: null, customerSlug: null, isConsultantView: true); + } final data = state.extra as Map; final planId = data['planId'] as int; final successfulPayment = data['successfulPayment'] as bool?; diff --git a/lib/core/views/cubit/views_cubit.dart b/lib/core/views/cubit/views_cubit.dart index ee15af22..64cfefe2 100644 --- a/lib/core/views/cubit/views_cubit.dart +++ b/lib/core/views/cubit/views_cubit.dart @@ -1,4 +1,6 @@ import 'dart:async'; + +import 'package:eatvio_app/core/error_reporting/report_from.dart'; 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'; @@ -86,9 +88,13 @@ class ViewsCubit extends Cubit { } Future _viewContent({required ViewableContentType type, required List ids}) async { - await viewsRepository.viewContent( - viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids), - ); + try { + await viewsRepository.viewContent( + viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids), + ); + } catch (e, st) { + await reportFrom(e, st, this); + } } void _scheduleDebouncedSend() { diff --git a/lib/core/views/repository/views_repository.dart b/lib/core/views/repository/views_repository.dart index 869819ca..398f8703 100644 --- a/lib/core/views/repository/views_repository.dart +++ b/lib/core/views/repository/views_repository.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/core/views/service/views_service.dart'; import 'package:eatvio_app/features/feed/models/view_model.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class ViewsRepository { final ViewsService viewsService; @@ -8,11 +7,7 @@ class ViewsRepository { ViewsRepository({required this.viewsService}); Future viewContent({required ViewModel viewModel}) async { - try { - await viewsService.viewContent(viewModel: viewModel); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await viewsService.viewContent(viewModel: viewModel); + } } diff --git a/lib/core/websockets/ws_manager.dart b/lib/core/websockets/ws_manager.dart index cf84354c..cb90f4f8 100644 --- a/lib/core/websockets/ws_manager.dart +++ b/lib/core/websockets/ws_manager.dart @@ -9,14 +9,16 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:eatvio_app/core/error_reporting/error_reporter.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'; class WsManager { final WsService wsService; + final ErrorReporter _errorReporter; - WsManager({required this.wsService}); + WsManager({required this.wsService, required ErrorReporter errorReporter}) + : _errorReporter = errorReporter; PusherChannelsClient? client; int? userId; @@ -114,7 +116,7 @@ class WsManager { _subscribe(); await _connectCubitsToStreams(); } catch (e, st) { - await Sentry.captureException(e, stackTrace: st); + await _errorReporter.report(e, st, origin: 'WsManager'); log(e.toString(), stackTrace: st); } } @@ -130,7 +132,7 @@ class WsManager { await sl().connectToWebSocket(); } catch (e, st) { log('WsManager._connectCubitsToStreams: $e', stackTrace: st); - await Sentry.captureException(e, stackTrace: st); + await _errorReporter.report(e, st, origin: 'WsManager'); } } 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 942b3191..f36f43cf 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -9,6 +9,7 @@ import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; class AuthCubit extends Cubit { @@ -26,7 +27,8 @@ class AuthCubit extends Cubit { emit(AuthState.loaded(token: token)); await TokenStorage.saveTokenInStorage(token); return true; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Неверный адрес электронной почты или пароль'); emit(AuthState.error(errorMessage: '$e')); @@ -37,22 +39,30 @@ class AuthCubit extends Cubit { Future initializeUser() async { try { await registerAnonymousPushToken(); - FlutterNativeSplash.remove(); - } catch (e) { - log('Error in initializeUser: $e'); - FlutterNativeSplash.remove(); + FlutterNativeSplash.remove(); + } catch (e, st) { + log('Error in initializeUser: $e', stackTrace: st); + FlutterNativeSplash.remove(); } } Future registerAnonymousPushToken() async { - final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false; - if (!hasAnonymousTokenBeenSent) { - await firebaseApi.registerAnonymousPushToken(); - prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true); + try { + final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false; + if (!hasAnonymousTokenBeenSent) { + await firebaseApi.registerAnonymousPushToken(); + prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true); + } + } catch (e, st) { + await reportFrom(e, st, this); } } Future registerPushToken() async { - await firebaseApi.registerPushToken(); + try { + await firebaseApi.registerPushToken(); + } catch (e, st) { + await reportFrom(e, st, this); + } } } 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 a066c959..45d00f13 100644 --- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/features/auth/models/oauth_verification_request_respo 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'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class OAuthCubit extends Cubit with RegistrationDateCacheMixin { final AuthRepository authRepository; @@ -29,7 +30,8 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin { log('OAuthCubit: Got token model: ${tokenModel.token}'); TokenStorage.saveTokenInStorage(tokenModel.token); emit(OAuthState.loaded(token: tokenModel.token)); - } on DioException catch (e) { + } on DioException catch (e, st) { + await reportFrom(e, st, this); log('OAuthCubit: Error occurred: $e'); // Проверяем, является ли ошибка ошибкой валидации email/phone final errors = e.response?.data['errors'] as Map?; @@ -62,7 +64,8 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin { } else { emit(OAuthState.error(errorMessage: e.toString())); } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log('OAuthCubit: Error occurred: $e'); emit(OAuthState.error(errorMessage: e.toString())); } @@ -103,7 +106,8 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin { TokenStorage.saveTokenInStorage(token); cacheRegistrationDate(DateTime.now()); emit(OAuthState.loaded(token: token)); - } on DioException catch (e) { + } on DioException catch (e, st) { + await reportFrom(e, st, this); emit(OAuthState.error(errorMessage: e.toString())); final errors = e.response?.data['errors'] as Map?; final emailErrors = errors?['email'] as List?; @@ -153,7 +157,8 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin { // Сбрасываем состояние после успешного запроса кода emit(const OAuthState()); return response; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log('OAuthCubit: Error requesting verification code: $e'); emit(OAuthState.error(errorMessage: e.toString())); return null; @@ -200,10 +205,10 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin { log('OAuthCubit: OAuth verification confirmed successfully, token received'); TokenStorage.saveTokenInStorage(response.token); emit(OAuthState.loaded(token: response.token)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log('OAuthCubit: Error confirming OAuth verification: $e'); emit(OAuthState.error(errorMessage: e.toString())); - rethrow; // Пробрасываем ошибку дальше, чтобы UI мог её обработать } } } diff --git a/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart b/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart index 136ca9b8..bb6be7fe 100644 --- a/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart +++ b/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart'; import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class ResetPasswordCubit extends Cubit { final ResetPasswordRepository resetPasswordRepository; @@ -15,7 +16,8 @@ class ResetPasswordCubit extends Cubit { } else { emit(const ResetPasswordState.codeSendError()); } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(const ResetPasswordState.codeSendError()); } } @@ -28,7 +30,8 @@ class ResetPasswordCubit extends Cubit { } else { emit(const ResetPasswordState.resetPasswordError()); } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(const ResetPasswordState.resetPasswordError()); } } diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index 6c2b4c15..eec5b30a 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -7,7 +7,6 @@ import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_respo import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class AuthRepository { final AuthService authService; @@ -15,14 +14,10 @@ class AuthRepository { AuthRepository({required this.authService}); Future getTokenByCredentials(String email, String password) async { - try { - return await authService.getTokenByCredentials( - credentials: CredentialsModel(email: email, password: password), - ); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await authService.getTokenByCredentials( + credentials: CredentialsModel(email: email, password: password), + ); + } Future signInViaSocialMedia({ @@ -31,14 +26,10 @@ class AuthRepository { required String refreshToken, required String clientId, }) async { - try { - return await authService.signInViaSocialMedia( - body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId), - ); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await authService.signInViaSocialMedia( + body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId), + ); + } Future signUpViaSocialMedia({ @@ -56,47 +47,39 @@ class AuthRepository { required int height, required String clientId, }) async { - try { - return (await authService.signUpViaSocialMedia( - 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, - clientId: clientId, - ), - ))['token']! - as String; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await authService.signUpViaSocialMedia( + 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, + clientId: clientId, + ), + ))['token']! + as String; + } Future requestOAuthVerification({String? email, String? phone}) async { - try { - Map? body; - if (email != null) { - body = {'email': email}; - } else if (phone != null) { - body = {'phone': phone}; - } - if (body != null) { - return await authService.requestOAuthVerification(body: body); - } - return null; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + Map? body; + if (email != null) { + body = {'email': email}; + } else if (phone != null) { + body = {'phone': phone}; } + if (body != null) { + return await authService.requestOAuthVerification(body: body); + } + return null; + } Future confirmOAuthVerification({ @@ -116,33 +99,28 @@ class AuthRepository { required int weight, required int height, }) async { - try { - final request = OAuthVerificationConfirmRequest( - provider: provider, - accessToken: accessToken, - refreshToken: refreshToken, - clientId: clientId, - id: id, - email: email, - phone: phone, - code: code, - verificationType: verificationType, - firstName: firstName, - lastName: lastName, - dateOfBirth: dateOfBirth, - gender: gender, - weight: weight, - height: height, - ); - // Логируем запрос перед отправкой - log( - 'AuthRepository: Sending OAuth verification confirm request: provider=$provider, verificationType=$verificationType, code=$code, email=$email, phone=$phone', - ); - return await authService.confirmOAuthVerification(body: request); - } catch (e, s) { - log('AuthRepository: Error in confirmOAuthVerification: $e'); - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + final request = OAuthVerificationConfirmRequest( + provider: provider, + accessToken: accessToken, + refreshToken: refreshToken, + clientId: clientId, + id: id, + email: email, + phone: phone, + code: code, + verificationType: verificationType, + firstName: firstName, + lastName: lastName, + dateOfBirth: dateOfBirth, + gender: gender, + weight: weight, + height: height, + ); + // Логируем запрос перед отправкой + log( + 'AuthRepository: Sending OAuth verification confirm request: provider=$provider, verificationType=$verificationType, code=$code, email=$email, phone=$phone', + ); + return await authService.confirmOAuthVerification(body: request); + } } diff --git a/lib/features/auth/repositories/reset_password_repository.dart b/lib/features/auth/repositories/reset_password_repository.dart index d04270cb..2cccf86a 100644 --- a/lib/features/auth/repositories/reset_password_repository.dart +++ b/lib/features/auth/repositories/reset_password_repository.dart @@ -1,5 +1,4 @@ import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class ResetPasswordRepository { final ResetPasswordService resetPasswordService; @@ -9,22 +8,18 @@ class ResetPasswordRepository { }); Future sendEmailCodeForPasswordChange(String email) async { - try { - final response = await resetPasswordService.sendEmailCodeForPasswordChange( - data: { - 'email': email, - }, - ); - int statusCode = response.response.statusCode ?? 0; - if (statusCode >= 200 && statusCode < 300) { - return true; - } else { - return false; - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + final response = await resetPasswordService.sendEmailCodeForPasswordChange( + data: { + 'email': email, + }, + ); + int statusCode = response.response.statusCode ?? 0; + if (statusCode >= 200 && statusCode < 300) { + return true; + } else { + return false; } + } Future confirmPasswordChange( @@ -33,24 +28,20 @@ class ResetPasswordRepository { String passwordConfirmation, String email, ) async { - try { - final response = await resetPasswordService.confirmPasswordChange( - data: { - 'code': code, - 'password': password, - 'password_confirmation': passwordConfirmation, - 'email': email, - }, - ); - int statusCode = response.response.statusCode ?? 0; - if (statusCode >= 200 && statusCode < 300) { - return true; - } else { - return false; - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + final response = await resetPasswordService.confirmPasswordChange( + data: { + 'code': code, + 'password': password, + 'password_confirmation': passwordConfirmation, + 'email': email, + }, + ); + int statusCode = response.response.statusCode ?? 0; + if (statusCode >= 200 && statusCode < 300) { + return true; + } else { + return false; } + } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index af787e61..341e98a2 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -8,10 +8,11 @@ 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:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/error_reporting/error_reporter.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; -import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:vk_id/vk_id.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -43,7 +44,7 @@ class SocialAuthFacade { } return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr); } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); + await sl().report(e, s, origin: 'SocialAuthFacade'); return null; } } @@ -161,7 +162,7 @@ class SocialAuthFacade { ); } catch (e, s) { log(e.toString()); - await Sentry.captureException(e, stackTrace: s); + await sl().report(e, s, origin: 'SocialAuthFacade'); } return null; } @@ -209,7 +210,7 @@ class SocialAuthFacade { return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr); } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); + await sl().report(e, s, origin: 'SocialAuthFacade'); return null; } } diff --git a/lib/features/barcode/repositories/barcode_repository.dart b/lib/features/barcode/repositories/barcode_repository.dart index 030eca7e..4b03212e 100644 --- a/lib/features/barcode/repositories/barcode_repository.dart +++ b/lib/features/barcode/repositories/barcode_repository.dart @@ -1,7 +1,6 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart'; import 'package:eatvio_app/features/barcode/services/barcode_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class BarcodeRepository { final BarcodeService barcodeService; @@ -15,49 +14,46 @@ class BarcodeRepository { /// Получить информацию о продукте по штрихкоду /// Возвращает либо данные продукта, либо статус задачи обработки Future getProductByBarcode(String barcode) async { - try { - final response = await dio.get('/api/gsone/barcode/$barcode'); - - if (response.statusCode == 200) { - // Данные найдены в БД - final product = BarcodeProductModel.fromJson(response.data as Map); - return BarcodeResult.success(product); - } else if (response.statusCode == 202) { - // Запущена задача обработки - final taskStatus = BarcodeTaskStatusModel.fromJson(response.data as Map); - return BarcodeResult.processing(taskStatus.taskId ?? ''); - } - - throw Exception('Unexpected status code: ${response.statusCode}'); - } on DioException catch (e, s) { - if (e.response?.statusCode == 202) { - // Если получен статус 202, значит запущена задача обработки - final taskStatus = BarcodeTaskStatusModel.fromJson(e.response!.data as Map); - return BarcodeResult.processing(taskStatus.taskId ?? ''); - } - await Sentry.captureException(e, stackTrace: s); - rethrow; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + final response = await dio.get>( + '/api/gsone/barcode/$barcode', + options: Options(validateStatus: (_) => true), + ); + + final code = response.statusCode ?? 0; + final data = response.data; + if (data is! Map) { + throw DioException( + requestOptions: response.requestOptions, + response: response, + message: 'Invalid barcode response', + ); } + if (code == 200) { + final product = BarcodeProductModel.fromJson(data); + return BarcodeResult.success(product); + } + if (code == 202) { + final taskStatus = BarcodeTaskStatusModel.fromJson(data); + return BarcodeResult.processing(taskStatus.taskId ?? ''); + } + + throw DioException( + requestOptions: response.requestOptions, + response: response, + message: 'Unexpected status code: $code', + ); } /// Получить статус задачи обработки Future getTaskStatus(String taskId) async { - try { - final statusModel = await barcodeService.getTaskStatus(taskId: taskId); - - if (statusModel.status == 'completed' && statusModel.data != null) { - return BarcodeTaskStatus.completed(statusModel.data!); - } else if (statusModel.status == 'failed') { - return BarcodeTaskStatus.failed(); - } else { - return BarcodeTaskStatus.processing(); - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + final statusModel = await barcodeService.getTaskStatus(taskId: taskId); + + if (statusModel.status == 'completed' && statusModel.data != null) { + return BarcodeTaskStatus.completed(statusModel.data!); + } else if (statusModel.status == 'failed') { + return BarcodeTaskStatus.failed(); + } else { + return BarcodeTaskStatus.processing(); } } } @@ -109,4 +105,3 @@ enum BarcodeTaskStatusType { processing, failed, } - 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 844c8043..42cc5006 100644 --- a/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart +++ b/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class BlogPostCubit extends Cubit { final FeedRepository feedRepository; @@ -16,6 +17,7 @@ class BlogPostCubit extends Cubit { emit(state.copyWith(blogPostModel: postModel, isLoading: false)); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(state.copyWith(isLoading: false)); } @@ -37,6 +39,7 @@ class BlogPostCubit extends Cubit { await feedRepository.likeBlogPost(id: current.id); } } catch (e, st) { + await reportFrom(e, st, this); // rollback on error log(e.toString(), stackTrace: st); emit(state.copyWith(blogPostModel: current)); 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 2617b664..5d04bdaf 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,9 +1,12 @@ -import 'package:eatvio_app/core/cubit/base_cubit.dart'; +import 'dart:developer'; + +import 'package:eatvio_app/core/error_reporting/report_from.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 BaseCubit { +class BlogPostCreateCubit extends Cubit { final BlogPostRepository blogPostRepository; BlogPostCreateCubit({required this.blogPostRepository}) : super(const BlogPostCreateState()); @@ -15,7 +18,8 @@ class BlogPostCreateCubit extends BaseCubit { emit(const BlogPostCreateState.loaded()); return true; } catch (error, stackTrace) { - handleError(error, stackTrace); + log('BlogPostCreateCubit: $error', stackTrace: stackTrace); + await reportFrom(error, stackTrace, this); emit(BlogPostCreateState.error(errorMessage: error.toString())); return false; } @@ -28,7 +32,8 @@ class BlogPostCreateCubit extends BaseCubit { emit(const BlogPostCreateState.loaded()); return true; } catch (error, stackTrace) { - handleError(error, stackTrace); + log('BlogPostCreateCubit: $error', stackTrace: stackTrace); + await reportFrom(error, stackTrace, this); emit(BlogPostCreateState.error(errorMessage: error.toString())); return false; } @@ -39,7 +44,8 @@ class BlogPostCreateCubit extends BaseCubit { await blogPostRepository.deleteBlogPost(id); return true; } catch (error, stackTrace) { - handleError(error, stackTrace); + log('BlogPostCreateCubit: $error', stackTrace: stackTrace); + await reportFrom(error, stackTrace, this); return false; } } 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 7f1fc49b..337854dc 100644 --- a/lib/features/blog_post_create/repositories/blog_post_repository.dart +++ b/lib/features/blog_post_create/repositories/blog_post_repository.dart @@ -1,5 +1,5 @@ -import 'dart:developer'; import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/locator_service.dart'; @@ -8,7 +8,6 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class BlogPostRepository { final BlogPostService blogPostService; @@ -23,48 +22,30 @@ class BlogPostRepository { String? description, XFile? image, ) async { - FormData formData = FormData.fromMap({ + final formData = FormData.fromMap({ if (title != null && title.isNotEmpty) 'title': title, if (description != null && description.isNotEmpty) 'description_md': description, if (videoUrl != null && videoUrl.isNotEmpty) 'video_url': videoUrl, if (image != null) 'image': await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name), }); - try { - return await blogPostService.createBlogPost( - formData: formData, - ); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return blogPostService.createBlogPost( + formData: formData, + ); } Future downloadImage(String imageUrl) async { - try { - // Загрузка изображения - final response = await http.get(Uri.parse('${sl().baseUrl}/storage/$imageUrl')); + final response = await http.get(Uri.parse('${sl().baseUrl}/storage/$imageUrl')); - if (response.statusCode == 200) { - // Получение временной директории - final directory = await getTemporaryDirectory(); - // Создание файла в временной директории - final filePath = '${directory.path}/image.jpg'; // Вы можете изменить название файла - final file = File(filePath); - // Запись данных в файл - await file.writeAsBytes(response.bodyBytes); + if (response.statusCode == 200) { + final directory = await getTemporaryDirectory(); + final filePath = '${directory.path}/image.jpg'; + final file = File(filePath); + await file.writeAsBytes(response.bodyBytes); - // Возвращаем объект XFile - return XFile(file.path); - } else { - log('Ошибка загрузки изображения: ${response.statusCode}'); - return null; - } - } catch (e, s) { - log('Произошла ошибка: $e'); - await Sentry.captureException(e, stackTrace: s); - return null; + return XFile(file.path); } + return null; } Future updateBlogPost( @@ -75,9 +56,9 @@ class BlogPostRepository { XFile? image, String? oldImageUrl, ) async { - FormData formData = FormData.fromMap({ + final formData = FormData.fromMap({ 'title': title, - 'description_md': description, + 'description_md': description, 'video_url': videoUrl, 'image': image != null ? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name) @@ -86,23 +67,13 @@ class BlogPostRepository { : null, }); - try { - return await blogPostService.updateBlogPost( - id: id, - formData: formData, - ); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return blogPostService.updateBlogPost( + id: id, + formData: formData, + ); } Future deleteBlogPost(int id) async { - try { - await blogPostService.deleteBlogPost(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await blogPostService.deleteBlogPost(id: id); } } diff --git a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart index a0910fcb..2a51eca4 100644 --- a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart +++ b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart @@ -1,8 +1,11 @@ +import 'dart:async'; + import 'package:appmetrica_plugin/appmetrica_plugin.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; import 'package:go_router/go_router.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -28,7 +31,8 @@ class ProfileAvatarCubit extends Cubit { return age; } - } catch (e) { + } catch (e, st) { + unawaited(reportFrom(e, st, this)); return 0; } return 0; @@ -51,10 +55,12 @@ class ProfileAvatarCubit extends Cubit { ]); AppMetrica.setUserProfileID(profile.slug); AppMetrica.reportUserProfile(userProfile); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); // Игнорируем ошибки плагинов, профиль уже загружен } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); AppRouter.navigatorKey.currentContext?.go('/auth-screen'); emit(ProfileAvatarState.error(errorMessage: e.toString())); } diff --git a/lib/features/calendar/presentation/bloc/calendar_cubit.dart b/lib/features/calendar/presentation/bloc/calendar_cubit.dart index 84fa9f0e..055d133a 100644 --- a/lib/features/calendar/presentation/bloc/calendar_cubit.dart +++ b/lib/features/calendar/presentation/bloc/calendar_cubit.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_state.dart'; import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class CalendarCubit extends Cubit { final CalendarRepository calendarRepository; @@ -12,7 +13,8 @@ class CalendarCubit extends Cubit { try { final calendarList = await calendarRepository.getCalendar(); emit(CalendarState.loaded(calendarList: calendarList)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(CalendarState.error(errorMessage: e.toString())); } } diff --git a/lib/features/calendar/repositories/calendar_repository.dart b/lib/features/calendar/repositories/calendar_repository.dart index 87af6b20..4c8178a3 100644 --- a/lib/features/calendar/repositories/calendar_repository.dart +++ b/lib/features/calendar/repositories/calendar_repository.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/features/calendar/models/calendar_model.dart'; import 'package:eatvio_app/features/calendar/services/calendar_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class CalendarRepository { final CalendarService calendarService; @@ -8,11 +7,7 @@ class CalendarRepository { CalendarRepository({required this.calendarService}); Future> getCalendar() async { - try { - return await calendarService.getCalendar(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await calendarService.getCalendar(); + } } diff --git a/lib/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart b/lib/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart index aa6689a1..2d16bb09 100644 --- a/lib/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart +++ b/lib/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart @@ -4,6 +4,7 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_s import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class CancelPlanCubit extends Cubit { final CancelPlanRepository cancelPlanRepository; @@ -18,6 +19,7 @@ class CancelPlanCubit extends Cubit { await cancelPlanRepository.cancelChosenPlan(id: id, body: body); return true; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); } return false; diff --git a/lib/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart b/lib/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart index 2a7ccc66..1b34e117 100644 --- a/lib/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart +++ b/lib/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class CancelPlanRepository { final CancelPlanService cancelService; @@ -9,11 +8,7 @@ class CancelPlanRepository { required int id, CancelationModel? body }) async { - try { - await cancelService.cancelChosenPlan(id: id, body: body); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await cancelService.cancelChosenPlan(id: id, body: body); + } } diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index d77eacc7..d61f8ddc 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -20,6 +20,7 @@ import 'package:eatvio_app/features/my_finance/models/card_model.dart'; import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; import 'package:uuid/uuid.dart'; class ChatRoomCubit extends Cubit { @@ -64,7 +65,8 @@ class ChatRoomCubit extends Cubit { ); currentPage++; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных')); } } @@ -75,6 +77,7 @@ class ChatRoomCubit extends Cubit { final cards = await cardsRepository.getCards(); _setCard(cards: cards); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); } } @@ -111,7 +114,8 @@ class ChatRoomCubit extends Cubit { if (chosenPlan?.consultantId == chatRoom.users.sender.id) { await checkCards(); } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); typingSubscription?.cancel(); messagesSubscription?.cancel(); @@ -121,13 +125,17 @@ class ChatRoomCubit extends Cubit { } Future updateChosenPlanState() async { - final currentState = state; - if (currentState is ChatRoomStateLoaded) { - final chosenPlanId = currentState.chatRoom.chosenPlanId; - if (chosenPlanId != null) { - final chosenPlan = await chatRepository.getChosenPlan(id: chosenPlanId); - emit(currentState.copyWith.chatRoom(chosenPlan: chosenPlan)); + try { + final currentState = state; + if (currentState is ChatRoomStateLoaded) { + final chosenPlanId = currentState.chatRoom.chosenPlanId; + if (chosenPlanId != null) { + final chosenPlan = await chatRepository.getChosenPlan(id: chosenPlanId); + emit(currentState.copyWith.chatRoom(chosenPlan: chosenPlan)); + } } + } catch (e, st) { + await reportFrom(e, st, this); } } @@ -142,6 +150,7 @@ class ChatRoomCubit extends Cubit { emit((state as ChatRoomStateLoaded).copyWith.chatRoom(chosenPlan: chosenPlan)); return true; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); return false; } @@ -151,7 +160,8 @@ class ChatRoomCubit extends Cubit { try { await chatRepository.cancelChosenPlan(id: planId); emit((state as ChatRoomStateLoaded).copyWith.chatRoom(chosenPlan: null)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных')); } } @@ -238,7 +248,8 @@ class ChatRoomCubit extends Cubit { }).toList(), ), ); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit( (state as ChatRoomStateLoaded).copyWith( chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) { @@ -253,6 +264,7 @@ class ChatRoomCubit extends Cubit { sl().getChatRooms(); return true; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); return false; } @@ -274,6 +286,7 @@ class ChatRoomCubit extends Cubit { } return false; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); return false; } @@ -286,6 +299,7 @@ class ChatRoomCubit extends Cubit { final paymentModel = await chatRepository.getPaymentLink(id: planId); return paymentModel.paymentUrl; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); return null; } @@ -300,7 +314,8 @@ class ChatRoomCubit extends Cubit { body: PlanComplainModel(description: description), ); return true; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); return false; } @@ -312,7 +327,8 @@ class ChatRoomCubit extends Cubit { if (planId == null) return false; await chatRepository.finishChosenPlan(id: planId); return true; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); return false; } @@ -400,7 +416,8 @@ class ChatRoomCubit extends Cubit { }).toList(), ), ); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit( (state as ChatRoomStateLoaded).copyWith( chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) { @@ -414,6 +431,7 @@ class ChatRoomCubit extends Cubit { } sl().getChatRooms(); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); } } diff --git a/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart b/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart index 174108b3..52e1631e 100644 --- a/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart +++ b/lib/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart @@ -9,6 +9,7 @@ import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_sta import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class ChatsCubit extends Cubit { final ChatRepository chatRepository; @@ -31,10 +32,14 @@ class ChatsCubit extends Cubit { late int lastPage; Future connectToWebSocket() async { - await newIncomingMessagesSubscription?.cancel(); - newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) { - _handleNewMessageViaSocket(event.data); - }); + try { + await newIncomingMessagesSubscription?.cancel(); + newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) { + _handleNewMessageViaSocket(event.data); + }); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future getChatRoomsWithLoading({String? query}) async { @@ -53,7 +58,8 @@ class ChatsCubit extends Cubit { ), ); page++; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit( state.copyWith(errorMessage: e.toString(), state: BaseState.error), ); @@ -76,6 +82,7 @@ class ChatsCubit extends Cubit { ); page++; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit( state.copyWith(errorMessage: e.toString(), state: BaseState.error), @@ -99,6 +106,7 @@ class ChatsCubit extends Cubit { ); page++; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit( state.copyWith( @@ -132,33 +140,37 @@ class ChatsCubit extends Cubit { } Future _handleNewMessageViaSocket(dynamic data) async { - final json = jsonDecode(data); - final message = ChatMessageModel.fromJson(json['message']); + try { + final json = jsonDecode(data); + final message = ChatMessageModel.fromJson(json['message']); - final currentChatRooms = [...state.chatRooms]; + final currentChatRooms = [...state.chatRooms]; - bool wasChanged = false; - bool willIncrementUnreadChats = false; - final updatedChatRooms = currentChatRooms.map((chatRoom) { - if (chatRoom.id == message.roomId) { - wasChanged = true; - if (chatRoom.unreadMessagesCount == 0) { - willIncrementUnreadChats = true; + bool wasChanged = false; + bool willIncrementUnreadChats = false; + final updatedChatRooms = currentChatRooms.map((chatRoom) { + if (chatRoom.id == message.roomId) { + wasChanged = true; + if (chatRoom.unreadMessagesCount == 0) { + willIncrementUnreadChats = true; + } + return chatRoom.copyWith(lastMessage: message, unreadMessagesCount: chatRoom.unreadMessagesCount + 1); } - return chatRoom.copyWith(lastMessage: message, unreadMessagesCount: chatRoom.unreadMessagesCount + 1); - } - return chatRoom; - }).toList(); + return chatRoom; + }).toList(); - if (!wasChanged && _query == null) { - getChatRooms(); - } else { - emit( - state.copyWith( - chatRooms: updatedChatRooms, - chatsWithUnreadMessagesCount: willIncrementUnreadChats ? state.chatsWithUnreadMessagesCount + 1 : state.chatsWithUnreadMessagesCount, - ), - ); + if (!wasChanged && _query == null) { + getChatRooms(); + } else { + emit( + state.copyWith( + chatRooms: updatedChatRooms, + chatsWithUnreadMessagesCount: willIncrementUnreadChats ? state.chatsWithUnreadMessagesCount + 1 : state.chatsWithUnreadMessagesCount, + ), + ); + } + } catch (e, st) { + await reportFrom(e, st, this); } } diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index 36682421..fc079241 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/token_storage.dart'; @@ -12,7 +11,6 @@ import 'package:eatvio_app/features/chat/models/plan_complain_model.dart'; import 'package:eatvio_app/features/chat/services/chat_service.dart'; import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:http_parser/http_parser.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class ChatRepository { final ChatService chatService; @@ -34,160 +32,104 @@ class ChatRepository { String? data, List? files, }) async { - try { - final multipartFiles = []; + final multipartFiles = []; - if (files != null) { - for (final file in files) { - final fileBytes = await file.readAsBytes(); - final multipartFile = MultipartFile.fromBytes( - fileBytes, - filename: file.path.split('/').last, - contentType: MediaType('application', 'octet-stream'), - ); - multipartFiles.add(multipartFile); - } + if (files != null) { + for (final file in files) { + final fileBytes = await file.readAsBytes(); + final multipartFile = MultipartFile.fromBytes( + fileBytes, + filename: file.path.split('/').last, + contentType: MediaType('application', 'octet-stream'), + ); + multipartFiles.add(multipartFile); } - - // Если data пустая строка или null, не передаем её вообще - // Сервер требует либо непустую строку, либо отсутствие поля - final dataToSend = (data == null || data.trim().isEmpty) ? null : data; - - return await chatService.sendMessage( - slug: slug, - data: dataToSend, - mediaData: multipartFiles.isEmpty ? null : multipartFiles, - socketId: (await SocketIdStorage.getSocketIdFromStorage())!, - ); - } catch (e, st) { - log(e.toString(), stackTrace: st); - await Sentry.captureException(e, stackTrace: st); - rethrow; } + + // Если data пустая строка или null, не передаем её вообще + // Сервер требует либо непустую строку, либо отсутствие поля + final dataToSend = (data == null || data.trim().isEmpty) ? null : data; + + return await chatService.sendMessage( + slug: slug, + data: dataToSend, + mediaData: multipartFiles.isEmpty ? null : multipartFiles, + socketId: (await SocketIdStorage.getSocketIdFromStorage())!, + ); + } Future getChatRoom(String uuid) async { - try { - return (await chatService.getChatRoom(uuid: uuid)); - } catch (e, st) { - log(e.toString(), stackTrace: st); - await Sentry.captureException(e, stackTrace: st); - rethrow; - } + return (await chatService.getChatRoom(uuid: uuid)); + } Future getChatRooms({String? query, int page = 1}) async { - try { - return (await chatService.getChatRooms(query: query, page: page)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await chatService.getChatRooms(query: query, page: page)); + } Future getChatMessages(String id, int page) async { - try { - return (await chatService.getChatMessages(id: id, page: page)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await chatService.getChatMessages(id: id, page: page)); + } Future getChatRoomIdByUserSlug({required String slug}) async { - try { - return (await chatService.getChatRoomIdByUserSlug(user: slug)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await chatService.getChatRoomIdByUserSlug(user: slug)); + } Future markMessagesAsRead(String id, List messageIds) async { - try { - await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds}); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds}); + } Future cancelChosenPlan({ required int id, }) async { - try { - await chatService.cancelChosenPlan(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await chatService.cancelChosenPlan(id: id); + } Future choseStartPlanDate({required int id, required Map date}) async { - try { - await chatService.choseStartPlanDate(id: id, date: date); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await chatService.choseStartPlanDate(id: id, date: date); + } Future getPaymentLink({ required int id, }) async { - try { - return await chatService.getPaymentLink(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await chatService.getPaymentLink(id: id); + } Future getChosenPlan({ required int id, }) async { - try { - return await chatService.getChosenPlan(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await chatService.getChosenPlan(id: id); + } Future finishChosenPlan({ required int id, }) async { - try { - await chatService.finishChosenPlan(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await chatService.finishChosenPlan(id: id); + } Future complainAboutConsultation({ required int id, required PlanComplainModel body, }) async { - try { - await chatService.complainAboutConsultation(id: id, body: body); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await chatService.complainAboutConsultation(id: id, body: body); + } Future getChatByUserSlug(String slug) async { - try { - final response = await chatService.getChatByUserSlug(slug: slug); - if (response.response.statusCode == 404) { - return '404'; - } else { - return response.data['id']; - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - return 'error'; + final response = await chatService.getChatByUserSlug(slug: slug); + if (response.response.statusCode == 404) { + return '404'; } + return response.data['id']; } } diff --git a/lib/features/complaining/presentation/cubit/complaining_cubit.dart b/lib/features/complaining/presentation/cubit/complaining_cubit.dart index 896fa6e9..b531e6b8 100644 --- a/lib/features/complaining/presentation/cubit/complaining_cubit.dart +++ b/lib/features/complaining/presentation/cubit/complaining_cubit.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_ import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart'; import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class ComplainingCubit extends Cubit { final ComplainingRepository repository; @@ -81,6 +82,7 @@ class ComplainingCubit extends Cubit { } } } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); } emit(state.copyWith(wasSentSuccessfully: false)); diff --git a/lib/features/complaining/repositories/complaining_repository.dart b/lib/features/complaining/repositories/complaining_repository.dart index 10a1cce3..d6a234c4 100644 --- a/lib/features/complaining/repositories/complaining_repository.dart +++ b/lib/features/complaining/repositories/complaining_repository.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/features/complaining/models/complain_model.dart'; import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class ComplainingRepository { final ComplainingService service; @@ -11,12 +10,8 @@ class ComplainingRepository { required String category, required ComplainModel body, }) async { - try { - await service.sendPlanComplain(slug: slug, category: category, body: body); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await service.sendPlanComplain(slug: slug, category: category, body: body); + } Future sendRecipeComplain({ @@ -24,12 +19,8 @@ class ComplainingRepository { required String category, required ComplainModel body, }) async { - try { - await service.sendRecipeComplain(slug: slug, category: category, body: body); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await service.sendRecipeComplain(slug: slug, category: category, body: body); + } Future sendBlogPostsComplain({ @@ -37,12 +28,8 @@ class ComplainingRepository { required String category, required ComplainModel body, }) async { - try { - await service.sendBlogPostsComplain(id: id, category: category, body: body); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await service.sendBlogPostsComplain(id: id, category: category, body: body); + } Future sendProfileComplain({ @@ -50,12 +37,8 @@ class ComplainingRepository { required String category, required ComplainModel body, }) async { - try { - await service.sendProfileComplain(slug: slug, category: category, body: body); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await service.sendProfileComplain(slug: slug, category: category, body: body); + } Future sendDietComplain({ @@ -63,12 +46,8 @@ class ComplainingRepository { required String category, required ComplainModel body, }) async { - try { - await service.sendDietComplain(id: id, category: category, body: body); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await service.sendDietComplain(id: id, category: category, body: body); + } Future sendCommentComplain({ @@ -76,11 +55,7 @@ class ComplainingRepository { required String category, required ComplainModel body, }) async { - try { - await service.sendCommentComplain(id: id, category: category, body: body); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await service.sendCommentComplain(id: id, category: category, body: body); + } } diff --git a/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart b/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart index 76c2fd69..0410ae04 100644 --- a/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart +++ b/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/features/consultant/become_consultant/models/consulta import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart'; import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class RegisterConsultantCubit extends Cubit { final ConsultantRepository consultantRepository; @@ -54,45 +55,73 @@ class RegisterConsultantCubit extends Cubit { requestModel: myRequestStatus, certPhotos: certPhotos, isUpdating: _update, + error: null, ), ); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); + emit(state.copyWith(loading: false, error: e.toString())); } } Future disableLoading() async { try { final certPhotos = await consultantRepository.getConsultantCertPhotos(); - emit(state.copyWith(loading: false, certPhotos: certPhotos)); - } catch (e) { + emit(state.copyWith(loading: false, certPhotos: certPhotos, error: null)); + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); + emit(state.copyWith(loading: false, error: e.toString())); + } + } + + /// Повторная загрузка после [DataErrorWidget], аналогично [AboutDietCubit.getDiet]. + Future reloadData(String? slug) async { + emit(state.copyWith(loading: true, error: null)); + if (slug != null) { + await getMyRequestStatus(slug); + } else { + await disableLoading(); } } void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) { - emit(state.copyWith(consultant: consultant, acceptOffer: acceptOffer, loading: false)); + emit(state.copyWith(consultant: consultant, acceptOffer: acceptOffer, loading: false, error: null)); } Future registerConsultant() async { - if (state.newAddedPhotos.isNotEmpty) { - await consultantRepository.storeConsultantCertPhotos(files: state.newAddedPhotos); - } - if (state.deletedPhotoIds.isNotEmpty) { - for (final int id in state.deletedPhotoIds) { - await consultantRepository.deletePhoto(id); + emit(state.copyWith(error: null)); + try { + if (state.newAddedPhotos.isNotEmpty) { + await consultantRepository.storeConsultantCertPhotos(files: state.newAddedPhotos); } + if (state.deletedPhotoIds.isNotEmpty) { + for (final int id in state.deletedPhotoIds) { + await consultantRepository.deletePhoto(id); + } + } + await consultantRepository.registerConsultant(consultant: state.consultant, update: _update); + } catch (e, st) { + await reportFrom(e, st, this); + log(e.toString()); + emit(state.copyWith(error: e.toString())); } - await consultantRepository.registerConsultant(consultant: state.consultant, update: _update); } Future addCertPhotos(List files) async { if (files.isEmpty) { return; } - final compressedFiles = await ImageCompressUtility.compressFilesList(files); + try { + final compressedFiles = await ImageCompressUtility.compressFilesList(files); - emit(state.copyWith(newAddedPhotos: [...state.newAddedPhotos, ...compressedFiles])); + emit(state.copyWith(newAddedPhotos: [...state.newAddedPhotos, ...compressedFiles], error: null)); + } catch (e, st) { + await reportFrom(e, st, this); + log(e.toString()); + emit(state.copyWith(error: e.toString())); + } } Future removeCertPhoto(int? id, String? path) async { diff --git a/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart b/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart index 45baa2c6..e9635eb6 100644 --- a/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart +++ b/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart @@ -18,5 +18,6 @@ abstract class RegisterConsultantState with _$RegisterConsultantState { @Default([]) List certPhotos, @Default([]) List newAddedPhotos, @Default([]) List deletedPhotoIds, + String? error, }) = _RegisterConsultantState; } diff --git a/lib/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart b/lib/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart index 515728c1..a27e5c67 100644 --- a/lib/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart +++ b/lib/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/core/utils/image_constant.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.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/presentation/bloc/register_consultant_cubit/register_consultant_state.dart'; @@ -9,6 +10,7 @@ import 'package:eatvio_app/features/consultant/become_consultant/presentation/pa import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/registration_as_screen.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/social_networks_screen.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:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -48,8 +50,37 @@ class _StepperBecomeConsultantScreenState extends State registrationConsultantCubit, child: BlocBuilder( builder: (context, state) { - if (state.loading == false) { + if (state.loading) { + return const Scaffold(body: Loader()); + } + if (state.error != null) { return Scaffold( + appBar: AppBar( + centerTitle: true, + leading: BackButton( + onPressed: () { + if (context.canPop()) { + GoRouter.of(context).pop(); + return; + } + GoRouter.of(context).go('/feed-screen'); + }, + ), + title: Text( + '', + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ), + body: DataErrorWidget( + onReload: () => context.read().reloadData(widget.slug), + ), + ); + } + return Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: Row( children: [ @@ -136,6 +167,9 @@ class _StepperBecomeConsultantScreenState extends State( context: context, builder: (context) => AlertDialog( actionsPadding: const EdgeInsets.symmetric(horizontal: 4), @@ -191,9 +225,7 @@ class _StepperBecomeConsultantScreenState extends State Navigator.of(context).pop(), ), ], ), @@ -282,9 +314,6 @@ class _StepperBecomeConsultantScreenState extends State registerConsultant({required ConsultantProfileModel consultant, bool update = false}) async { - try { - if (update) { - return await consultantService.consultantUpdate(consultantModel: consultant); - } else { - return await consultantService.consultantRegistration(consultantModel: consultant); - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + if (update) { + return await consultantService.consultantUpdate(consultantModel: consultant); + } else { + return await consultantService.consultantRegistration(consultantModel: consultant); } + } Future getConsultantRequestStatus(String slug) async { - try { - return await consultantService.getConsultantRequestStatus(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await consultantService.getConsultantRequestStatus(slug: slug); + } Future getMyRequestStatus(String slug) async { - try { - return await consultantService.getMyRequestStatus(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await consultantService.getMyRequestStatus(slug: slug); + } Future> getConsultantCertPhotos() async { - try { - return await consultantService.getConsultantCertPhotos(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await consultantService.getConsultantCertPhotos(); + } Future> storeConsultantCertPhotos({required List files}) async { - try { - final multipartFiles = []; + final multipartFiles = []; - for (final file in files) { - final fileBytes = await file.readAsBytes(); - final multipartFile = MultipartFile.fromBytes(fileBytes, filename: file.path.split('/').last, contentType: MediaType('application', 'octet-stream')); - multipartFiles.add(multipartFile); - } - - if (multipartFiles.isEmpty) { - return []; - } - - return await consultantService.storeConsultantCertPhotos(photos: multipartFiles); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + for (final file in files) { + final fileBytes = await file.readAsBytes(); + final multipartFile = MultipartFile.fromBytes(fileBytes, filename: file.path.split('/').last, contentType: MediaType('application', 'octet-stream')); + multipartFiles.add(multipartFile); } + + if (multipartFiles.isEmpty) { + return []; + } + + return await consultantService.storeConsultantCertPhotos(photos: multipartFiles); + } Future hideConsultantCertPhoto(int id) async { - try { - await consultantService.hideConsultantCertPhoto(payload: {'cert_photo_id': id}); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await consultantService.hideConsultantCertPhoto(payload: {'cert_photo_id': id}); + } Future unHideConsultantCertPhoto(int id) async { - try { - await consultantService.unHideConsultantCertPhoto(payload: {'cert_photo_id': id}); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await consultantService.unHideConsultantCertPhoto(payload: {'cert_photo_id': id}); + } Future deletePhoto(int id) async { - try { - await consultantService.deletePhoto(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await consultantService.deletePhoto(id: id); + } } 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 d3ff72a9..1580561c 100644 --- a/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart +++ b/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/features/consultant_rating/repositories/consultant_ra 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'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class ConsultantRatingCubit extends Cubit { final ConsultantRatingRepository repository; @@ -20,6 +21,7 @@ class ConsultantRatingCubit extends Cubit { 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) { + await reportFrom(e, st, this); emit(state.copyWith(isStatisticsLoading: true, error: e.toString())); log(e.toString(), stackTrace: st); } @@ -31,6 +33,7 @@ class ConsultantRatingCubit extends Cubit { final scoreLogs = await repository.getScoreLogs(date: date); emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null)); } catch (e, st) { + await reportFrom(e, st, this); emit(state.copyWith(isRatingLoading: true, error: e.toString())); log(e.toString(), stackTrace: st); } diff --git a/lib/features/consultant_rating/repositories/consultant_rating_repository.dart b/lib/features/consultant_rating/repositories/consultant_rating_repository.dart index 3d66e879..38bd5ec9 100644 --- a/lib/features/consultant_rating/repositories/consultant_rating_repository.dart +++ b/lib/features/consultant_rating/repositories/consultant_rating_repository.dart @@ -1,7 +1,6 @@ import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart'; import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class ConsultantRatingRepository { final ConsultantRatingService service; @@ -9,21 +8,13 @@ class ConsultantRatingRepository { ConsultantRatingRepository(this.service); Future getConsultantStatistics({String? date}) async { - try { - return await service.getConsultantStatistics(date: date); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await service.getConsultantStatistics(date: date); + } Future> getScoreLogs({String? date}) async { - try { - final data = (await service.getScoreLogs(date: date)).data; - return data; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + final data = (await service.getScoreLogs(date: date)).data; + return data; + } } diff --git a/lib/features/consultations/presentation/cubit/consultations_cubit.dart b/lib/features/consultations/presentation/cubit/consultations_cubit.dart index 5972e8d9..80f8b206 100644 --- a/lib/features/consultations/presentation/cubit/consultations_cubit.dart +++ b/lib/features/consultations/presentation/cubit/consultations_cubit.dart @@ -4,6 +4,7 @@ import 'package:eatvio_app/core/enums/base_state.dart'; import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart'; import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class ConsultationsCubit extends Cubit { final ConsultationsRepository consultationsRepository; @@ -14,6 +15,7 @@ class ConsultationsCubit extends Cubit { final consultations = await consultationsRepository.getConsultations(); emit(state.copyWith(consultations: consultations, state: BaseState.success)); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(state.copyWith(state: BaseState.error)); } diff --git a/lib/features/consultations/repositories/consultations_repository.dart b/lib/features/consultations/repositories/consultations_repository.dart index 06c2ee39..f29c792e 100644 --- a/lib/features/consultations/repositories/consultations_repository.dart +++ b/lib/features/consultations/repositories/consultations_repository.dart @@ -1,7 +1,6 @@ import 'package:eatvio_app/core/enums/filters_comparison_enum.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; import 'package:eatvio_app/features/consultations/services/consultation_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/models/consultant_finance_model.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_model.dart'; @@ -11,13 +10,8 @@ class ConsultationsRepository { ConsultationsRepository({required this.consultationService}); Future> getConsultations() async { - try { - final consultations = await consultationService.getConsultations(); - return consultations.data; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + final consultations = await consultationService.getConsultations(); + return consultations.data; } Future> getFilteredConsultations({ @@ -26,44 +20,24 @@ class ConsultationsRepository { FiltersComparisonEnum? stateComparison, FiltersComparisonEnum? dateComparison, }) async { - try { - final model = await consultationService.getFilteredConsultations( - state: state, - date: date, - stateComparison: stateComparison, - dateComparison: dateComparison, - ); - return model.data; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + final model = await consultationService.getFilteredConsultations( + state: state, + date: date, + stateComparison: stateComparison, + dateComparison: dateComparison, + ); + return model.data; } Future getStatistic() async { - try { - return await consultationService.getStatistics(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return consultationService.getStatistics(); } Future> getConsultantFinances(String? date, FiltersComparisonEnum? dateComparison) async { - try { - return await consultationService.getConsultantFinances(date, dateComparison); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return consultationService.getConsultantFinances(date, dateComparison); } Future> getUserPurchases(String? date, FiltersComparisonEnum? dateComparison) async { - try { - return await consultationService.getUserPurchases(date, dateComparison); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return consultationService.getUserPurchases(date, dateComparison); } } 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 4b7315ab..9788c7ce 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -13,9 +13,9 @@ import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:http/http.dart' as http; -import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:path_provider/path_provider.dart'; class CreateDietCubit extends Cubit { @@ -29,9 +29,10 @@ class CreateDietCubit extends Cubit { Future setInitialData(Diet? diet) async { if (diet == null) return; - emit(state.copyWith(isLoading: true, isDietBeingEdited: true)); - dietId = diet.id; - final dailyScheduleWithNutrition = await createDietRepository.getDietPortionsNutrition(diet.id); + try { + emit(state.copyWith(isLoading: true, isDietBeingEdited: true)); + dietId = diet.id; + final dailyScheduleWithNutrition = await createDietRepository.getDietPortionsNutrition(diet.id); final countOfDays = diet.duration; final List>> dailySchedule = List.generate(countOfDays, (index) => [[], [], [], [], []]); @@ -66,7 +67,11 @@ class CreateDietCubit extends Cubit { imageUrl: appConfig.buildType == AppBuildType.tests ? null : imageUrl, ), ); - emit(state.copyWith(isLoading: false, isDietBeingEdited: false, dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration))); + emit(state.copyWith(isLoading: false, isDietBeingEdited: false, dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration))); + } catch (e, st) { + await reportFrom(e, st, this); + emit(state.copyWith(isLoading: false, isDietBeingEdited: false)); + } } void setDetails(String text) { @@ -112,7 +117,12 @@ class CreateDietCubit extends Cubit { } Future> getGlossaryListByQuery(String query) async { - return await glossaryRepository.getGlossaryListByQuery(query); + try { + return await glossaryRepository.getGlossaryListByQuery(query); + } catch (e, st) { + await reportFrom(e, st, this); + return []; + } } void setImage(XFile? image, {String? imageUrl}) { @@ -204,7 +214,8 @@ class CreateDietCubit extends Cubit { Future getDietForPreview() async { if (!_isFormValidForDraft()) return null; - final authorProfile = await profileRepository.getMyProfile(); + try { + final authorProfile = await profileRepository.getMyProfile(); final imageBytes = (await state.dietData.image?.readAsBytes()) ?? []; String image = ''; if (imageBytes.isNotEmpty && imageBytes.length <= _maxImageBytesForBase64) { @@ -297,19 +308,26 @@ class CreateDietCubit extends Cubit { isFavorite: false, dailyNutrition: dailyNutrition, ); - return diet; + return diet; + } catch (e, st) { + await reportFrom(e, st, this); + return null; + } } - Future downloadImageAsFile(String imageUrl) async { - final response = await http.get(Uri.parse(imageUrl)); - - if (response.statusCode == 200) { + Future downloadImageAsFile(String imageUrl) async { + try { + final response = await http.get(Uri.parse(imageUrl)); + if (response.statusCode != 200) { + throw Exception('Не удалось загрузить изображение'); + } final tempDir = await getTemporaryDirectory(); final filePath = '${tempDir.path}/${imageUrl.split('/').last}'; final file = File(filePath); - return await file.writeAsBytes(response.bodyBytes); - } else { - throw Exception('Не удалось загрузить изображение'); + return file.writeAsBytes(response.bodyBytes); + } catch (e, st) { + await reportFrom(e, st, this); + return null; } } @@ -353,7 +371,7 @@ class CreateDietCubit extends Cubit { emit(state.copyWith(published: dietState != DietState.draft, drafted: dietState == DietState.draft)); return null; } catch (e, st) { - await Sentry.captureException(e, stackTrace: st); + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(state.copyWith(isDietPublishing: false)); } @@ -391,7 +409,7 @@ class CreateDietCubit extends Cubit { emit(state.copyWith(isDietBeingEdited: false)); return null; } catch (e, st) { - await Sentry.captureException(e, stackTrace: st); + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(state.copyWith(isDietBeingEdited: false)); return 'error'; diff --git a/lib/features/create_diet/repositories/create_diet_repository.dart b/lib/features/create_diet/repositories/create_diet_repository.dart index feb37496..6898399a 100644 --- a/lib/features/create_diet/repositories/create_diet_repository.dart +++ b/lib/features/create_diet/repositories/create_diet_repository.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class CreateDietRepository { final CreateDietService createDietService; @@ -62,48 +61,40 @@ class CreateDietRepository { dietMap.addAll({'name': name, 'duration': duration}); final formData = FormData.fromMap(dietMap); - try { - switch (state) { - case DietState.active: - return await createDietService.createPublishedDiet(formData: formData); - case DietState.draft: - return await createDietService.createDraftedDiet(formData: formData); - case DietState.personal: - return await createDietService.createPersonalDiet(formData: formData); - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + switch (state) { + case DietState.active: + return await createDietService.createPublishedDiet(formData: formData); + case DietState.draft: + return await createDietService.createDraftedDiet(formData: formData); + case DietState.personal: + return await createDietService.createPersonalDiet(formData: formData); } + } Future>>> getDietPortionsNutrition(int dietId) async { - try { - final response = await createDietService.getDietPortionsNutrition(diet: dietId); - final data = response.data; - final dailySchedule = (data as Map).map((key, value) { - return MapEntry( - int.parse(key), - (value as Map).map((key, value) { - return MapEntry( - int.parse(key), - (value as Map).map((key, value) { - final portionMap = Map.from(value as Map); - final pnvid = portionMap['product_nutritional_value_id']; - if (pnvid is String) { - portionMap['product_nutritional_value_id'] = int.tryParse(pnvid); - } - return MapEntry(int.parse(key), DailySchedule.fromJson(portionMap)); - }), - ); - }), - ); - }); - return dailySchedule; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + final response = await createDietService.getDietPortionsNutrition(diet: dietId); + final data = response.data; + final dailySchedule = (data as Map).map((key, value) { + return MapEntry( + int.parse(key), + (value as Map).map((key, value) { + return MapEntry( + int.parse(key), + (value as Map).map((key, value) { + final portionMap = Map.from(value as Map); + final pnvid = portionMap['product_nutritional_value_id']; + if (pnvid is String) { + portionMap['product_nutritional_value_id'] = int.tryParse(pnvid); + } + return MapEntry(int.parse(key), DailySchedule.fromJson(portionMap)); + }), + ); + }), + ); + }); + return dailySchedule; + } Future updateDiet({ @@ -161,23 +152,16 @@ class CreateDietRepository { } final formData = FormData.fromMap(dietMap); - try { - switch (state) { - case DietState.active: - final response = await createDietService.updatePublished(diet: dietId, formData: formData); - return response.response.statusCode ?? 0; - case DietState.draft: - final response = await createDietService.updateDrafted(diet: dietId, formData: formData); - return response.response.statusCode ?? 0; - case DietState.personal: - final response = await createDietService.updatePersonal(diet: dietId, formData: formData); - return response.response.statusCode ?? 0; - } - } on DioException catch (e) { - return e.response?.statusCode ?? 0; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + switch (state) { + case DietState.active: + final response = await createDietService.updatePublished(diet: dietId, formData: formData); + return response.response.statusCode ?? 0; + case DietState.draft: + final response = await createDietService.updateDrafted(diet: dietId, formData: formData); + return response.response.statusCode ?? 0; + case DietState.personal: + final response = await createDietService.updatePersonal(diet: dietId, formData: formData); + return response.response.statusCode ?? 0; } } } diff --git a/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart b/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart index 21b07b68..be09737e 100644 --- a/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart +++ b/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; class CreatePlanCubit extends Cubit { @@ -457,7 +458,8 @@ class CreatePlanCubit extends Cubit { await createPlanRepository.createPlan(model: planMap); emit(state.copyWith(isLoading: false, planCreated: true)); } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); emit(state.copyWith(isLoading: false, planCreated: true)); } diff --git a/lib/features/create_plan/repositories/create_plan_repository.dart b/lib/features/create_plan/repositories/create_plan_repository.dart index 1011005e..17ed3298 100644 --- a/lib/features/create_plan/repositories/create_plan_repository.dart +++ b/lib/features/create_plan/repositories/create_plan_repository.dart @@ -1,6 +1,5 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class CreatePlanRepository { final CreatePlanService createPlanService; @@ -10,13 +9,9 @@ class CreatePlanRepository { Future createPlan({ required Map model, }) async { - try { - final formData = FormData.fromMap(model); - return await createPlanService.createPlan(formData: formData); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + final formData = FormData.fromMap(model); + return await createPlanService.createPlan(formData: formData); + } } 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 e7537af7..e65fe5ef 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 @@ -11,7 +11,7 @@ import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; @@ -166,7 +166,7 @@ class CreateRecipeCubit extends Cubit { await recipeRepository.publishRecipe(model, update, slug); return true; } catch (e, st) { - await Sentry.captureException(e, stackTrace: st); + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); return false; } @@ -179,7 +179,7 @@ class CreateRecipeCubit extends Cubit { emit(state.copyWith(drafted: true)); return true; } catch (e, st) { - await Sentry.captureException(e, stackTrace: st); + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); return false; } @@ -204,16 +204,19 @@ class CreateRecipeCubit extends Cubit { )); } - Future downloadImageAsFile(String imageUrl) async { - final response = await http.get(Uri.parse(imageUrl)); - - if (response.statusCode == 200) { + Future downloadImageAsFile(String imageUrl) async { + try { + final response = await http.get(Uri.parse(imageUrl)); + if (response.statusCode != 200) { + throw Exception('Не удалось загрузить изображение'); + } final tempDir = await getTemporaryDirectory(); final filePath = '${tempDir.path}/${imageUrl.split('/').last}'; final file = File(filePath); - return await file.writeAsBytes(response.bodyBytes); - } else { - throw Exception('Не удалось загрузить изображение'); + return file.writeAsBytes(response.bodyBytes); + } catch (e, st) { + await reportFrom(e, st, this); + return null; } } @@ -222,44 +225,53 @@ class CreateRecipeCubit extends Cubit { } Future recipeToEntity(Recipe recipe) async { - final file = await downloadImageAsFile('${sl().baseUrl}/storage/${recipe.image}'); + try { + final file = await downloadImageAsFile('${sl().baseUrl}/storage/${recipe.image}'); + if (file == null) { + disableLoading(); + return; + } - final List steps = await Future.wait( - recipe.steps!.map( - (e) async => StepEntity( - description: e.description, - image: e.image.isEmpty ? null : await downloadImageAsFile('${sl().baseUrl}/storage/${e.image}'), + final List steps = await Future.wait( + recipe.steps!.map( + (e) async => StepEntity( + description: e.description, + image: e.image.isEmpty ? null : await downloadImageAsFile('${sl().baseUrl}/storage/${e.image}'), + ), ), - ), - ); - emit( - CreateRecipeState( - entity: RecipeCreateEntity( - title: recipe.title, - typeOfRecipe: TypeOfRecipe.fromApiType(recipe.type!), - countOfPortions: recipe.servings, - energyInOnePortion: recipe.calories, - totalCookHours: recipe.cookingTime ~/ 60, - totalCookMinutes: recipe.cookingTime % 60, - activeCookHours: recipe.activeCookingTime ~/ 60, - activeCookMinutes: recipe.activeCookingTime % 60, - description: recipe.description, - image: file, - videoUrl: recipe.videoUrl, - ingredients: recipe.ingredients! - .map((e) => i.Ingredient( - name: e.name, - unit: e.unit!, - portion: e.portion, - productId: e.productId, - )) - .toList(), - steps: steps, - tags: recipe.tags.map((e) => (e.name)).toList(), + ); + emit( + CreateRecipeState( + entity: RecipeCreateEntity( + title: recipe.title, + typeOfRecipe: TypeOfRecipe.fromApiType(recipe.type!), + countOfPortions: recipe.servings, + energyInOnePortion: recipe.calories, + totalCookHours: recipe.cookingTime ~/ 60, + totalCookMinutes: recipe.cookingTime % 60, + activeCookHours: recipe.activeCookingTime ~/ 60, + activeCookMinutes: recipe.activeCookingTime % 60, + description: recipe.description, + image: file, + videoUrl: recipe.videoUrl, + ingredients: recipe.ingredients! + .map((e) => i.Ingredient( + name: e.name, + unit: e.unit!, + portion: e.portion, + productId: e.productId, + )) + .toList(), + steps: steps, + tags: recipe.tags.map((e) => (e.name)).toList(), + ), + loading: false, ), - loading: false, - ), - ); + ); + } catch (e, st) { + await reportFrom(e, st, this); + disableLoading(); + } } bool validateAll() { @@ -376,13 +388,18 @@ class CreateRecipeCubit extends Cubit { await recipeRepository.deleteRecipe(slug); return true; } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); + await reportFrom(e, s, this); return false; } } Future> getGlossaryListByQuery(String query) async { - return await glossaryRepository.getGlossaryListByQuery(query); + try { + return await glossaryRepository.getGlossaryListByQuery(query); + } catch (e, st) { + await reportFrom(e, st, this); + return []; + } } bool validateSteps() { diff --git a/lib/features/create_recipe/reporitories/recipe_repository.dart b/lib/features/create_recipe/reporitories/recipe_repository.dart index 6acc905b..9659a520 100644 --- a/lib/features/create_recipe/reporitories/recipe_repository.dart +++ b/lib/features/create_recipe/reporitories/recipe_repository.dart @@ -7,7 +7,6 @@ import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart'; import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart'; import 'package:eatvio_app/features/create_recipe/models/step.dart'; import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:eatvio_app/features/create_recipe/models/product.dart'; class RecipeRepository { @@ -16,12 +15,8 @@ class RecipeRepository { RecipeRepository({required this.recipeService}); Future>> inputProduct(String name) async { - try { - return await recipeService.getInputProduct(name); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await recipeService.getInputProduct(name); + } Future _buildStepsMap(List? steps) async { @@ -110,67 +105,43 @@ class RecipeRepository { } Future publishRecipe(RecipeModel recipe, [bool update = false, String? slug]) async { - try { - final formData = await _buildPublishFormData(recipe); - if (update) { - await recipeService.updateRecipe(slug: slug!, formData: formData); - } else { - await recipeService.publishRecipe(formData: formData); - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + final formData = await _buildPublishFormData(recipe); + if (update) { + await recipeService.updateRecipe(slug: slug!, formData: formData); + } else { + await recipeService.publishRecipe(formData: formData); } + } Future saveAsDraft(RecipeModel recipe, [bool update = false, String? slug]) async { - try { - final formData = await _buildDraftFormData(recipe); - log(formData.fields.toString()); - if (update) { - await recipeService.updateRecipe(slug: slug!, formData: formData); - } else { - await recipeService.createDraftedRecipe(formData: formData); - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + final formData = await _buildDraftFormData(recipe); + log(formData.fields.toString()); + if (update) { + await recipeService.updateRecipe(slug: slug!, formData: formData); + } else { + await recipeService.createDraftedRecipe(formData: formData); } + } Future draftExistingRecipe(int id) async { - try { - await recipeService.draftRecipe(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await recipeService.draftRecipe(id: id); + } Future getRecipe(String recipeID) async { - try { - return await recipeService.getRecipe(recipeID); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await recipeService.getRecipe(recipeID); + } Future> getRecipesTags() async { - try { - return (await recipeService.getTags()).data; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await recipeService.getTags()).data; + } Future deleteRecipe(String slug) async { - try { - return await recipeService.deleteRecipe(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await recipeService.deleteRecipe(slug: slug); + } } diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart index 37c85b79..a21f9282 100644 --- a/lib/features/dev_tools/presentation/dev_tools_screen.dart +++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart @@ -240,7 +240,7 @@ class _DevToolsScreenContentState extends State { } else { platform = 'ios'; } - await dio.post('/api/profile/notification/token/register', data: {'token': await FirebaseApi().getToken(), 'platform': platform}); + await dio.post('/api/profile/notification/token/register', data: {'token': await sl().getToken(), 'platform': platform}); showInfoToast('Токен добавлен'); } catch (e) { showInfoToast('Не удалось добавть токен'); @@ -253,7 +253,7 @@ class _DevToolsScreenContentState extends State { buttonStyle: const ButtonStyle(), onPressed: () async { try { - final token = await FirebaseApi().getToken(); + final token = await sl().getToken(); if (token != null) { Clipboard.setData(ClipboardData(text: token)); showInfoToast('Токен скопирован'); @@ -278,7 +278,7 @@ class _DevToolsScreenContentState extends State { } else { platform = 'ios'; } - await dio.post('/api/profile/notification/token/unregister', data: {'token': await FirebaseApi().getToken(), 'platform': platform}); + await dio.post('/api/profile/notification/token/unregister', data: {'token': await sl().getToken(), 'platform': platform}); showInfoToast('Токен удалён'); } catch (e) { showInfoToast('Не удалось удалить токен'); diff --git a/lib/features/dev_tools/test_cases.dart b/lib/features/dev_tools/test_cases.dart index 3d43bd2a..be5cd9ff 100644 --- a/lib/features/dev_tools/test_cases.dart +++ b/lib/features/dev_tools/test_cases.dart @@ -36,7 +36,6 @@ 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:eatvio_app/core/env_config.dart'; - class TestResult { final bool success; final String? error; @@ -57,7 +56,9 @@ class CreateRecipeTask implements TestCases { Future run({required String baseUrl}) async { try { final dio = sl(); - final recipeRepository = RecipeRepository(recipeService: CreateRecipeService(dio, baseUrl: baseUrl)); + final recipeRepository = RecipeRepository( + recipeService: CreateRecipeService(dio, baseUrl: baseUrl), + ); await recipeRepository.publishRecipe( const RecipeModel( title: 'Title', diff --git a/lib/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart b/lib/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart index 5ef8dea1..e1f8efc5 100644 --- a/lib/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart +++ b/lib/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart'; import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class ConsultantCommentsCubit extends Cubit { final DiaryRepository diaryRepository; @@ -74,6 +75,7 @@ class ConsultantCommentsCubit extends Cubit { } await diaryRepository.commentUserDay(diaryId: diaryId, commentModel: state.commentsModel); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); } } diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 39a0a7e7..289e3ecf 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -19,8 +19,15 @@ import 'package:eatvio_app/features/glossary/repositories/glossary_repository.da import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class DiaryCubit extends Cubit { + static const NutritionModel _zeroNutritionModel = NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0); + static const NutritionDataModel _fallbackNutrition = NutritionDataModel( + dailyNutritionIntake: _zeroNutritionModel, + dailyNutritionRecommendation: _zeroNutritionModel, + ); + late final DateTime today; late DateTime selectedDay; int selectedMeal = 1; @@ -102,6 +109,7 @@ class DiaryCubit extends Cubit { emit(currentState); return false; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(DiaryState.error(errorMessage: e.toString())); return false; @@ -115,7 +123,8 @@ class DiaryCubit extends Cubit { if (diaryEntity != null) { setDiary(diaryDay: diaryEntity, diaryId: diaryId); } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); } } @@ -126,6 +135,7 @@ class DiaryCubit extends Cubit { final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset()); return diaryDay; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); if (e is DioException) { if (e.response?.statusCode == 422) { @@ -152,8 +162,9 @@ class DiaryCubit extends Cubit { return false; } emit(const DiaryState.loading()); - if (diaryDay.diet?.id != null && diaryDay.dayOfChosenDiet != null) { - final diet = await dietRepository.getDiet((diaryDay.diet?.id)!); + try { + if (diaryDay.diet?.id != null && diaryDay.dayOfChosenDiet != null) { + final diet = await dietRepository.getDiet((diaryDay.diet?.id)!); final meals = diet.dailySchedule[diaryDay.dayOfChosenDiet]; final firstMealPortions = meals?[1]?.values.map((elem) { return PortionModel( @@ -221,9 +232,14 @@ class DiaryCubit extends Cubit { selectedMeal: selectedMeal, ), ); - return true; + return true; + } + return false; + } catch (e, st) { + await reportFrom(e, st, this); + emit(DiaryState.error(errorMessage: e.toString())); + return false; } - return false; } Future setDiary({int? diaryId, required DiaryDayEntity diaryDay}) async { @@ -279,6 +295,7 @@ class DiaryCubit extends Cubit { ), ); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(DiaryState.error(errorMessage: e.toString())); } @@ -290,13 +307,19 @@ class DiaryCubit extends Cubit { await updateCurrentDiary(); return true; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); return false; } } Future getNutrition(int index) async { - return (await diaryRepository.getNutritionData(index)); + try { + return (await diaryRepository.getNutritionData(index)); + } catch (e, st) { + await reportFrom(e, st, this); + return _fallbackNutrition; + } } Future addProductToMeal({ @@ -308,15 +331,23 @@ class DiaryCubit extends Cubit { int? productNutritionalValueId, }) async { emit((state as DiaryStateLoaded).copyWith(isLoadingAddingProductInMeal: true)); - await diaryRepository.addPortionToMeal( - diaryId: diaryId, - mealId: mealId, - productName: productName, - portion: portion, - quantity: quantity, - productNutritionalValueId: productNutritionalValueId, - ); - await updateCurrentDiary(); + try { + await diaryRepository.addPortionToMeal( + diaryId: diaryId, + mealId: mealId, + productName: productName, + portion: portion, + quantity: quantity, + productNutritionalValueId: productNutritionalValueId, + ); + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); + final s = state; + if (s is DiaryStateLoaded) { + emit(s.copyWith(isLoadingAddingProductInMeal: false)); + } + } } Future updateProductInMeal({ @@ -329,62 +360,95 @@ class DiaryCubit extends Cubit { int? productNutritionalValueId, required PortionState portionState, }) async { - if (portionState == PortionState.filledDiet || portionState == PortionState.eatenFromDiet) { - await diaryRepository.addPortionToMeal( - diaryId: diaryId, - mealId: mealId, - productName: productName, - portion: portion, - quantity: quantity, - productNutritionalValueId: productNutritionalValueId, - ); - } else { - await diaryRepository.updateProductInMeal( - diaryId: diaryId, - mealId: mealId, - portionId: portionId, - productName: productName, - portion: portion, - quantity: quantity, - productNutritionalValueId: productNutritionalValueId, - portionState: portionState, - ); + try { + if (portionState == PortionState.filledDiet || portionState == PortionState.eatenFromDiet) { + await diaryRepository.addPortionToMeal( + diaryId: diaryId, + mealId: mealId, + productName: productName, + portion: portion, + quantity: quantity, + productNutritionalValueId: productNutritionalValueId, + ); + } else { + await diaryRepository.updateProductInMeal( + diaryId: diaryId, + mealId: mealId, + portionId: portionId, + productName: productName, + portion: portion, + quantity: quantity, + productNutritionalValueId: productNutritionalValueId, + portionState: portionState, + ); + } + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); } - await updateCurrentDiary(); } Future markEaten({required int diaryId, required int mealId, required int portionId}) async { - await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId); - await updateCurrentDiary(); + try { + await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId); + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future unmarkEaten({required int diaryId, required int mealId, required int portionId}) async { - await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId); - await updateCurrentDiary(); + try { + await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId); + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future restore({required int diaryId, required int mealId, required int portionId}) async { - await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId); - await updateCurrentDiary(); + try { + await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId); + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async { - await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId); - await updateCurrentDiary(); + try { + await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId); + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future> getGlossaryListByQuery(String query) async { - return await glossaryRepository.getGlossaryListByQuery(query); + try { + return await glossaryRepository.getGlossaryListByQuery(query); + } catch (e, st) { + await reportFrom(e, st, this); + return []; + } } Future uploadPhotos({required List files, required int diaryId}) async { - await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal); - await updateCurrentDiary(); + try { + await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal); + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future deletePhoto({required DiaryPhotoModel photo, required int diaryId}) async { - await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal); - await updateCurrentDiary(); + try { + await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal); + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future sendProductToModeration({ @@ -397,34 +461,55 @@ class DiaryCubit extends Cubit { try { return await diaryRepository.sendProductToModeration(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates); } catch (e, stackTrace) { + await reportFrom(e, stackTrace, this); log(e.toString(), stackTrace: stackTrace); } return null; } Future fillTime({required int diaryId, required String time}) async { - await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time); - await updateCurrentDiary(); + try { + await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time); + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future fillWakeup({required int diaryId, required String time}) async { - await diaryRepository.fillWakeup(diaryId: diaryId, time: time); - await updateCurrentDiary(); + try { + await diaryRepository.fillWakeup(diaryId: diaryId, time: time); + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future fillRetire({required int diaryId, required String time}) async { - await diaryRepository.fillRetire(diaryId: diaryId, time: time); - await updateCurrentDiary(); + try { + await diaryRepository.fillRetire(diaryId: diaryId, time: time); + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future fillSatiety({required int diaryId, required int satiety}) async { - await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety); - await updateCurrentDiary(); + try { + await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety); + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future fillMood({required int diaryId, required int mood}) async { - await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood); - await updateCurrentDiary(); + try { + await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood); + await updateCurrentDiary(); + } catch (e, st) { + await reportFrom(e, st, this); + } } void fillComment(String comment) { diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index 071d03df..3072da4e 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; -import 'package:dio/dio.dart'; + +import 'package:collection/collection.dart'; import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart'; import 'package:eatvio_app/features/diary/models/consultant_review_model.dart'; import 'package:eatvio_app/features/diary/models/create_day_model.dart'; @@ -10,7 +11,6 @@ import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; import 'package:eatvio_app/features/diary/models/product_moderation_model.dart'; import 'package:eatvio_app/features/diary/services/diary_service.dart'; import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class DiaryRepository { final DiaryService diaryService; @@ -20,134 +20,82 @@ class DiaryRepository { DiaryRepository({required this.diaryService}); - Future getDiaryByDate(String date) async { - try { - Set ids = {}; - final diaryDays = await getDiaryIndexByDate(date); - if (diaryDays.isEmpty) { - throw Exception('No diary entry found for $date'); - } - final DiaryDayModel diaryDay = diaryDays.last; - for (var meal in diaryDay.meals) { - for (var portion in meal.portions) { - if (portion.productNutritionalValueId != null) { - 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(); + PortionModel _portionWithGlossary(PortionModel portion, List glossaries) { + final id = portion.productNutritionalValueId; + if (id == null) return portion; + final glossaryItem = glossaries.firstWhereOrNull((g) => g.id == id); + if (glossaryItem == null) return portion; + 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, + ), + ); + } - updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions); - } - final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals); - return updatedDiaryDay; - } - return diaryDay; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + Future getDiaryByDate(String date) async { + final ids = {}; + final diaryDays = await getDiaryIndexByDate(date); + if (diaryDays.isEmpty) { + throw Exception('No diary entry found for $date'); } + final DiaryDayModel diaryDay = diaryDays.last; + for (var meal in diaryDay.meals) { + for (var portion in meal.portions) { + if (portion.productNutritionalValueId != null) { + ids.add(portion.productNutritionalValueId!); + } + } + } + if (ids.isEmpty) { + return diaryDay; + } + final glossary = await getGlossariesIndex(ids: ids.toList()); + final glossaries = glossary.glossariesList; + final updatedMeals = diaryDay.meals.map((meal) { + final updatedPortions = meal.portions.map((p) => _portionWithGlossary(p, glossaries)).toList(); + return meal.copyWith(portions: updatedPortions); + }).toList(); + return diaryDay.copyWith(meals: updatedMeals); } Future> getDiaryIndexByDate(String date) async { - try { - return (await diaryService.getDiaryIndexByDate(date: date)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.getDiaryIndexByDate(date: date); } Future getDiaryDay(int diaryId) async { - try { - // 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!); - } + final diaryWrapper = await diaryService.getDiaryDay(diary: diaryId); + final DiaryDayModel diaryDay = diaryWrapper.diary; + final 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; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; } + if (ids.isEmpty) { + return diaryDay; + } + final glossary = await getGlossariesIndex(ids: ids.toList()); + final glossaries = glossary.glossariesList; + final updatedMeals = diaryDay.meals.map((meal) { + final updatedPortions = meal.portions.map((p) => _portionWithGlossary(p, glossaries)).toList(); + return meal.copyWith(portions: updatedPortions); + }).toList(); + return diaryDay.copyWith(meals: updatedMeals); } Future getNutritionData(int index) async { - try { - return await diaryService.getNutrition(diary: index); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.getNutrition(diary: index); } Future addPortionToMeal({ @@ -158,19 +106,14 @@ class DiaryRepository { required int quantity, int? productNutritionalValueId, }) async { - try { - return await diaryService.addProductToMeal( - diary: diaryId, - typeNo: mealId, - productName: productName, - portion: portion, - quantity: quantity, - productNutritionalValueId: productNutritionalValueId, - ); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.addProductToMeal( + diary: diaryId, + typeNo: mealId, + productName: productName, + portion: portion, + quantity: quantity, + productNutritionalValueId: productNutritionalValueId, + ); } Future updateProductInMeal({ @@ -183,83 +126,43 @@ class DiaryRepository { int? productNutritionalValueId, required PortionState portionState, }) async { - try { - Map data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson(); - if (productNutritionalValueId != null) data['product_nutritional_value_id'] = productNutritionalValueId; - data['state'] = portionState.name; - return await diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + final data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson(); + if (productNutritionalValueId != null) data['product_nutritional_value_id'] = productNutritionalValueId; + data['state'] = portionState.name; + return diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data); } Future markEaten({required int diaryId, required int mealId, required int portionId}) async { - try { - return await diaryService.markEaten(diary: diaryId, typeNo: mealId, portionModel: portionId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.markEaten(diary: diaryId, typeNo: mealId, portionModel: portionId); } Future unmarkEaten({required int diaryId, required int mealId, required int portionId}) async { - try { - return await diaryService.unmarkEaten(diary: diaryId, typeNo: mealId, portionModel: portionId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.unmarkEaten(diary: diaryId, typeNo: mealId, portionModel: portionId); } Future restore({required int diaryId, required int mealId, required int portionId}) async { - try { - return await diaryService.restore(diary: diaryId, typeNo: mealId, portionModel: portionId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.restore(diary: diaryId, typeNo: mealId, portionModel: portionId); } Future deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async { - try { - return await diaryService.deleteProductFromMeal(diary: diaryId, typeNo: mealId, portionModel: portionId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.deleteProductFromMeal(diary: diaryId, typeNo: mealId, portionModel: portionId); } Future uploadPhotos({required int diaryId, required int mealId, required List files}) async { - try { - for (int i = 0; i < files.length; i++) { - await diaryService.uploadPhotoOfMeal(diary: diaryId, typeNo: mealId, photo: files[i]); - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + for (var i = 0; i < files.length; i++) { + await diaryService.uploadPhotoOfMeal(diary: diaryId, typeNo: mealId, photo: files[i]); } } Future deletePhoto({required int diaryId, required int mealId, required DiaryPhotoModel photo}) async { - try { - await diaryService.deletePhotoOfMeal(diaryId: diaryId, typeNo: mealId, userAttachment: photo.id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await diaryService.deletePhotoOfMeal(diaryId: diaryId, typeNo: mealId, userAttachment: photo.id); } Future finishDiaryDay({required int diaryId, required String wakeUp, required String retire, String? comment}) async { - try { - return await diaryService.finishDiaryDay( - diary: diaryId, - finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment), - ); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.finishDiaryDay( + diary: diaryId, + finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment), + ); } Future sendProductToModeration({ @@ -269,129 +172,66 @@ class DiaryRepository { required double fats, required double carbohydrates, }) async { - try { - return await diaryService.sendProductToModeration( - data: ProductModerationModel(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates).toJson(), - ); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.sendProductToModeration( + data: ProductModerationModel(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates).toJson(), + ); } Future fillSatiety({required int diaryId, required int mealId, required int satiety}) async { - try { - return await diaryService.fillSatiety(diary: diaryId, typeNo: mealId, satiety: satiety); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.fillSatiety(diary: diaryId, typeNo: mealId, satiety: satiety); } Future fillMood({required int diaryId, required int mealId, required int mood}) async { - try { - return await diaryService.fillMood(diary: diaryId, typeNo: mealId, mood: mood); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.fillMood(diary: diaryId, typeNo: mealId, mood: mood); } Future fillTime({required int diaryId, required int mealId, required String time}) async { - try { - return await diaryService.fillTime(diary: diaryId, typeNo: mealId, time: time); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.fillTime(diary: diaryId, typeNo: mealId, time: time); } Future fillWakeup({required int diaryId, required String time}) async { - try { - return await diaryService.fillWakeup(diary: diaryId, wakeUp: time); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.fillWakeup(diary: diaryId, wakeUp: time); } Future fillRetire({required int diaryId, required String time}) async { - try { - return await diaryService.fillRetire(diary: diaryId, retire: time); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.fillRetire(diary: diaryId, retire: time); } Future commentUserDay({required int diaryId, required ConsultantCommentModel commentModel}) async { - try { - await diaryService.commentUserDay(id: diaryId, body: commentModel); - _diaryForceFetchController.add('force'); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await diaryService.commentUserDay(id: diaryId, body: commentModel); + _diaryForceFetchController.add('force'); } Future getDiaryComments({required int planId, required int diaryId}) async { - try { - return await diaryService.getDiaryComments(planId: planId, diaryId: diaryId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.getDiaryComments(planId: planId, diaryId: diaryId); } Future viewCommentOfDiaryDayByConsultant({required int diaryId}) async { - try { - await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId); } Future getGlossariesIndex({required List ids}) async { - Map filters = {}; + final filters = {}; if (ids.isNotEmpty) { - for (int i = 0; i < ids.length; i++) { + for (var i = 0; i < ids.length; i++) { filters['filters[id][\$in][$i]'] = ids[i]; } } - try { - return await diaryService.getGlossariesIndex(filters: filters); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return diaryService.getGlossariesIndex(filters: filters); } Future getDiaryDayV2({required String day}) async { - try { - final diaryDay = await diaryService.getDiaryDayV2(day: day); - return setCPFCToDiary(diaryDay); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + final diaryDay = await diaryService.getDiaryDayV2(day: day); + return setCPFCToDiary(diaryDay); } Future createDiaryDay({required String day}) async { - try { - await diaryService.createDay(body: CreateDayModel(day: day)); - await getDiaryByDate(day); - } catch (e, s) { - if (e is DioException && e.response?.statusCode == 422) { - } else { - await Sentry.captureException(e, stackTrace: s); - } - rethrow; - } + await diaryService.createDay(body: CreateDayModel(day: day)); + await getDiaryByDate(day); } Future setCPFCToDiary(DiaryDayModel diaryDay) async { - Set ids = {}; + final ids = {}; for (var meal in diaryDay.meals) { for (var portion in meal.portions) { if (portion.productNutritionalValueId != null) { @@ -399,41 +239,15 @@ class DiaryRepository { } } } - 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; + if (ids.isEmpty) { + return diaryDay; } - return diaryDay; + final glossary = await getGlossariesIndex(ids: ids.toList()); + final glossaries = glossary.glossariesList; + final updatedMeals = diaryDay.meals.map((meal) { + final updatedPortions = meal.portions.map((p) => _portionWithGlossary(p, glossaries)).toList(); + return meal.copyWith(portions: updatedPortions); + }).toList(); + return diaryDay.copyWith(meals: updatedMeals); } } diff --git a/lib/features/diets/presentation/cubit/about_diet_cubit.dart b/lib/features/diets/presentation/cubit/about_diet_cubit.dart index 6ba7980f..5336c0e8 100644 --- a/lib/features/diets/presentation/cubit/about_diet_cubit.dart +++ b/lib/features/diets/presentation/cubit/about_diet_cubit.dart @@ -9,6 +9,7 @@ import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_state.da import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class AboutDietCubit extends Cubit { AboutDietCubit({required this.dietsRepository, required this.profileRepository}) : super(const AboutDietState.initial()); @@ -31,7 +32,8 @@ class AboutDietCubit extends Cubit { dietsRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: diet.totalComments)); } emit(AboutDietState.loaded(diet: diet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); emit(AboutDietState.error(message: e.toString())); } @@ -62,7 +64,8 @@ class AboutDietCubit extends Cubit { } _selectedIndexes = List.generate(diet.dailySchedule.length, (index) => 0); emit(AboutDietState.loaded(diet: diet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); emit(AboutDietState.error(message: e.toString())); } @@ -90,7 +93,8 @@ class AboutDietCubit extends Cubit { EasyDebounce.debounce('like_diet', const Duration(seconds: 1), () async { try { await dietsRepository.likeDiet(id); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); if (dietIndexInFeed != null) { dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); @@ -114,7 +118,8 @@ class AboutDietCubit extends Cubit { EasyDebounce.debounce('add_or_remove_diet_to_favorites', const Duration(seconds: 1), () async { try { await dietsRepository.addToFavorite(id); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); if (dietIndex != null) { @@ -139,7 +144,8 @@ class AboutDietCubit extends Cubit { EasyDebounce.debounce('add_or_remove_diet_to_favorites', const Duration(seconds: 1), () async { try { await dietsRepository.removeFromFavorite(id); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); if (dietIndex != null) { @@ -159,6 +165,7 @@ class AboutDietCubit extends Cubit { await dietsRepository.startDiet(id, stringDate); return true; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); return false; } @@ -170,10 +177,12 @@ class AboutDietCubit extends Cubit { final id = (state as DietStateLoaded).diet.id; await dietsRepository.cancelDiet(id); return true; - } on DioException catch (e) { + } on DioException catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); return false; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); return false; } @@ -194,7 +203,8 @@ class AboutDietCubit extends Cubit { Future getDietBySlug(String slug) async { try { return await dietsRepository.getDietBySlug(slug); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); } return null; diff --git a/lib/features/diets/repositories/diet_repository.dart b/lib/features/diets/repositories/diet_repository.dart index 7ae0b6bc..9dc83ecb 100644 --- a/lib/features/diets/repositories/diet_repository.dart +++ b/lib/features/diets/repositories/diet_repository.dart @@ -4,7 +4,6 @@ import 'package:eatvio_app/features/diets/models/diet_likes_model.dart'; import 'package:eatvio_app/features/diets/services/diet_service.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:equatable/equatable.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class DietRepository { DietRepository({required this.dietsService}); @@ -18,76 +17,44 @@ class DietRepository { Stream get dietCommentsState => _dietCommentsStateStreamController.stream; Future likeDiet(int id) async { - try { - return await dietsService.likeDiet(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await dietsService.likeDiet(id: id); + } Future getDiet(int id) async { - try { - final diet = await dietsService.getDiet(id: id); - return diet; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + final diet = await dietsService.getDiet(id: id); + return diet; + } Future getDietBySlug(String slug) async { - try { - final diet = await dietsService.getDietBySlug(slug: slug); - return diet; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + final diet = await dietsService.getDietBySlug(slug: slug); + return diet; + } Future addToFavorite(int id) async { - try { - return await dietsService.addToFavorite(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await dietsService.addToFavorite(id: id); + } Future removeFromFavorite(int id) async { - try { - return await dietsService.removeFromFavorite(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await dietsService.removeFromFavorite(id: id); + } Future startDiet(int id, String date) async { - try { - return await dietsService.startDiet(id: id, body: {'date': date}); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await dietsService.startDiet(id: id, body: {'date': date}); + } Future cancelDiet(int id) async{ - try { - return await dietsService.cancelDiet(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await dietsService.cancelDiet(id: id); + } Future deleteDiet(int id) async { - try { - return await dietsService.deleteDiet(id: id); - } catch (e) { - rethrow; - } + return dietsService.deleteDiet(id: id); } void emitLikesStateToFeed(LikesState state) => _dietLikeStateStreamController.add(state); diff --git a/lib/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart b/lib/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart index 2c3473bd..dde50bce 100644 --- a/lib/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart +++ b/lib/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_state.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class ConsultantsCubit extends Cubit { final FeedRepository feedRepository; @@ -21,6 +22,7 @@ class ConsultantsCubit extends Cubit { ), ); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(ConsultantsState.error(errorMessage: e.toString())); } diff --git a/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart b/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart index e2f9b95b..5d1d5357 100644 --- a/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart +++ b/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart @@ -8,6 +8,7 @@ import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class DietsCubit extends Cubit { final FeedRepository feedRepository; @@ -109,6 +110,7 @@ class DietsCubit extends Cubit { ), ); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit( state.copyWith(isLoading: false, errorMessage: e.toString()), @@ -144,7 +146,8 @@ class DietsCubit extends Cubit { ), ); } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit( state.copyWith(isLoading: false, errorMessage: e.toString()), ); @@ -191,7 +194,8 @@ class DietsCubit extends Cubit { () async { try { await dietsRepository.likeDiet(id); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); emit( state.copyWith( @@ -287,7 +291,8 @@ class DietsCubit extends Cubit { () async { try { await dietsRepository.addToFavorite(id); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); emit( @@ -327,7 +332,8 @@ class DietsCubit extends Cubit { () async { try { await dietsRepository.removeFromFavorite(id); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); emit( @@ -348,7 +354,8 @@ class DietsCubit extends Cubit { try { await dietsRepository.deleteDiet(dietId); return true; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); return false; } } 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 c749f6d4..9254bf07 100644 --- a/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart +++ b/lib/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart @@ -4,6 +4,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class FeedCubit extends Cubit { final FeedRepository feedRepository; @@ -45,7 +46,8 @@ class FeedCubit extends Cubit { if (!isClosed) { emit(FeedState.loaded(feedBlogPostList: feedBlogPosts.feedBlogPostList, isLoading: false)); } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); if (!isClosed) { emit(FeedState.error(errorMessage: e.toString())); } @@ -69,7 +71,8 @@ class FeedCubit extends Cubit { emit(FeedState.loaded(feedBlogPostList: [...(state as FeedStateLoaded).feedBlogPostList, ...feedBlogPosts.feedBlogPostList], isLoading: isLoading)); } } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); if (!isClosed) { emit(FeedState.error(errorMessage: e.toString())); } @@ -81,14 +84,48 @@ class FeedCubit extends Cubit { try { await feedRepository.likeBlogPost(id: id); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); } } - Future unlikedPost(int id) async => await feedRepository.unlikedBlogPost(id: id); + Future unlikedPost(int id) async { + try { + await feedRepository.unlikedBlogPost(id: id); + } catch (e, st) { + await reportFrom(e, st, this); + } + } - Future viewPost(int id) async => await feedRepository.viewPost(id); - 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 viewPost(int id) async { + try { + await feedRepository.viewPost(id); + } catch (e, st) { + await reportFrom(e, st, this); + } + } + + Future viewRecipe(String slug) async { + try { + await feedRepository.viewRecipe(slug); + } catch (e, st) { + await reportFrom(e, st, this); + } + } + + Future viewDiet(int id) async { + try { + await feedRepository.viewDiet(id); + } catch (e, st) { + await reportFrom(e, st, this); + } + } + + Future viewPlan(String slug) async { + try { + await feedRepository.viewPlan(slug); + } catch (e, st) { + await reportFrom(e, st, this); + } + } } diff --git a/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart b/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart index 99432c22..b99dbcac 100644 --- a/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart +++ b/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart @@ -10,6 +10,7 @@ import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:eatvio_app/features/plans/repositories/plan_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class PlansCubit extends Cubit { final FeedRepository feedRepository; @@ -41,6 +42,17 @@ class PlansCubit extends Cubit { }); } + @override + Future close() async { + EasyDebounce.cancel('loadPlans'); + EasyDebounce.cancel('add_or_remove_plan_from_favorite'); + EasyDebounce.cancel('like_plan'); + likesStateSubscription.cancel(); + favoriteStateSubscription.cancel(); + commentsStateSubscription.cancel(); + await super.close(); + } + void changeDurationFilter(int index) { emit(state.copyWith(filterState: state.filterState.copyWith(durationIndex: index), errorMessage: null)); EasyDebounce.debounce( @@ -168,6 +180,7 @@ class PlansCubit extends Cubit { ), ); } catch (e, stackTrace) { + await reportFrom(e, stackTrace, this); log(e.toString()); log(stackTrace.toString()); emit(state.copyWith(isLoading: isLoading, errorMessage: e.toString())); @@ -199,7 +212,8 @@ class PlansCubit extends Cubit { ), ); } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(state.copyWith(errorMessage: e.toString())); } } @@ -228,7 +242,8 @@ class PlansCubit extends Cubit { () async { try { await planRepository.addToFavorite(slug); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); emit( @@ -268,7 +283,8 @@ class PlansCubit extends Cubit { () async { try { await planRepository.removeFromFavorite(slug); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); emit( @@ -324,7 +340,8 @@ class PlansCubit extends Cubit { () async { try { await planRepository.likePlan(slug); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); emit( state.copyWith( diff --git a/lib/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart index 2136624b..81c5d7f7 100644 --- a/lib/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class FeedProfileCubit extends Cubit { final ProfileRepository profileRepository; @@ -13,6 +14,7 @@ class FeedProfileCubit extends Cubit { final profile = await profileRepository.getMyProfile(); emit(state.copyWith(profile: profile)); } catch (e, stackTrace) { + await reportFrom(e, stackTrace, this); log(e.toString(), stackTrace: stackTrace); } } diff --git a/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart index 001a7ec2..233cf7aa 100644 --- a/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart +++ b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart @@ -9,6 +9,7 @@ import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart'; import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; import 'dart:developer'; class RecipesCubit extends Cubit { @@ -62,6 +63,7 @@ class RecipesCubit extends Cubit { ); lastPage = currentPage; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(state.copyWith(errorMessage: e.toString(), isLoading: false)); } @@ -93,7 +95,8 @@ class RecipesCubit extends Cubit { ); } lastPage = currentPage; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(state.copyWith(errorMessage: e.toString(), isLoading: false)); } } @@ -132,7 +135,8 @@ class RecipesCubit extends Cubit { () async { try { await aboutRecipeRepository.addToFavorite(slug); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); emit( @@ -172,7 +176,8 @@ class RecipesCubit extends Cubit { () async { try { await aboutRecipeRepository.removeFromFavorite(slug); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); emit( @@ -228,7 +233,8 @@ class RecipesCubit extends Cubit { () async { try { await aboutRecipeRepository.likeRecipe(slug); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); emit( state.copyWith( @@ -305,7 +311,8 @@ class RecipesCubit extends Cubit { try { await aboutRecipeRepository.deleteRecipe(slug); return true; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); return false; } } diff --git a/lib/features/feed/repositories/feed_repository.dart b/lib/features/feed/repositories/feed_repository.dart index 6263d766..b5128be8 100644 --- a/lib/features/feed/repositories/feed_repository.dart +++ b/lib/features/feed/repositories/feed_repository.dart @@ -7,7 +7,6 @@ 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'; -import 'package:sentry_flutter/sentry_flutter.dart'; class FeedRepository { final FeedService feedService; @@ -15,191 +14,107 @@ class FeedRepository { FeedRepository({required this.feedService}); Future getFeedBlogPosts({String? query, int page = 1}) async { - try { - return (await feedService.getBlogPostList(page: page, query: query)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.getBlogPostList(page: page, query: query)); + } Future getBlogPost({required int id}) async { - try { - return await feedService.getBlogPost(postId: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await feedService.getBlogPost(postId: id); + } Future> getRecipes({String? query, List? tags, int page = 1}) async { - try { - return (await feedService.getRecipes(query: query, tags: tags, page: page)).recipesList; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.getRecipes(query: query, tags: tags, page: page)).recipesList; + } 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)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.getDiets(page: page, query: query, types: types, duration: duration, sorting: sorting)); + } 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)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice)); + } Future getBlogPostComments({required String id}) async { - try { - return (await feedService.getBlogPostComments(id: id)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.getBlogPostComments(id: id)); + } Future sendBlogPostComment({required String id, required String commentText, int? parentCommentId}) async { - try { - await feedService.sendBlogPostComment(id: id, body: commentText, parentCommentId: parentCommentId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await feedService.sendBlogPostComment(id: id, body: commentText, parentCommentId: parentCommentId); + } Future getDietsComments({required String id}) async { - try { - return (await feedService.getDietsComments(id: id)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.getDietsComments(id: id)); + } Future sendDietsComment({required String id, required String commentText, int? parentCommentId}) async { - try { - await feedService.sendDietsComment(id: id, body: commentText, parentCommentId: parentCommentId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await feedService.sendDietsComment(id: id, body: commentText, parentCommentId: parentCommentId); + } Future getRecipesComments({required String id}) async { - try { - return (await feedService.getRecipesComments(id: id)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.getRecipesComments(id: id)); + } Future sendRecipesComment({required String id, required String commentText, int? parentCommentId}) async { - try { - await feedService.sendRecipesComment(id: id, body: commentText, parentCommentId: parentCommentId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await feedService.sendRecipesComment(id: id, body: commentText, parentCommentId: parentCommentId); + } Future getPlansComments({required String slug}) async { - try { - return (await feedService.getPlansComments(slug: slug)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.getPlansComments(slug: slug)); + } Future sendPlansComment({required String slug, required String commentText, int? parentCommentId}) async { - try { - await feedService.sendPlansComment(slug: slug, body: commentText, parentCommentId: parentCommentId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await feedService.sendPlansComment(slug: slug, body: commentText, parentCommentId: parentCommentId); + } Future likeBlogPost({required int id}) async { - try { - await feedService.likeBlogPost(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await feedService.likeBlogPost(id: id); + } Future unlikedBlogPost({required int id}) async { - try { - await feedService.unlikedBlogPost(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await feedService.unlikedBlogPost(id: id); + } Future> getConsultants([String? name]) async { - try { - return (await feedService.getConsultants(name: name)).data; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.getConsultants(name: name)).data; + } Future viewPost(int id) async { - try { - return (await feedService.viewPost(id: id)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.viewPost(id: id)); + } Future viewRecipe(String slug) async { - try { - return (await feedService.viewRecipe(slug: slug)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.viewRecipe(slug: slug)); + } Future viewDiet(int id) async { - try { - return (await feedService.viewDiet(id: id)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.viewDiet(id: id)); + } Future viewPlan(String slug) async { - try { - return (await feedService.viewPlan(slug: slug)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await feedService.viewPlan(slug: slug)); + } Future viewContent({required ViewModel viewModel}) async { - try { - await feedService.viewContent(viewModel: viewModel); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await feedService.viewContent(viewModel: viewModel); + } } diff --git a/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart b/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart index a2f6daf0..8675bda1 100644 --- a/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart +++ b/lib/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart @@ -6,6 +6,7 @@ import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/arc import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class ConsultationsArchiveCubit extends Cubit { final ConsultationsRepository consultationsRepository; @@ -34,7 +35,8 @@ class ConsultationsArchiveCubit extends Cubit { : FiltersComparisonEnum.equal, ); emit(state.copyWith(consultations: consultations, isLoading: false)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(state.copyWith(isLoading: false)); log(e.toString()); } @@ -55,7 +57,8 @@ class ConsultationsArchiveCubit extends Cubit { emit(state.copyWith(isLoading: true)); final statistic = await consultationsRepository.getStatistic(); emit(state.copyWith(statisticModel: statistic, isLoading: false)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(state.copyWith(isLoading: false)); log(e.toString()); } diff --git a/lib/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart b/lib/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart index 69d25052..9d2d57c6 100644 --- a/lib/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart +++ b/lib/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/features/consultations/repositories/consultations_rep import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class ConsultantFinancesCubit extends Cubit { final ConsultationsRepository consultationsRepository; @@ -22,7 +23,8 @@ class ConsultantFinancesCubit extends Cubit { state.periodFilterStatus == PeriodFilterStatus.all ? null : FiltersComparisonEnum.greaterThanOrEqual, ); emit(state.copyWith(financesModels: consultantFinances, isLoading: false)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(state.copyWith(financesModels: [], isLoading: false)); log(e.toString()); } diff --git a/lib/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart b/lib/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart index 55fdbc68..981ec6d7 100644 --- a/lib/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart +++ b/lib/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/features/consultations/repositories/consultations_rep import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_state.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class UserPurchasesCubit extends Cubit { final ConsultationsRepository consultationsRepository; @@ -23,7 +24,8 @@ class UserPurchasesCubit extends Cubit { ); emit(state.copyWith(purchasesModels: purchasesModels, isLoading: false)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(state.copyWith(purchasesModels: [], isLoading: false)); log(e.toString()); } diff --git a/lib/features/glossary/repositories/glossary_repository.dart b/lib/features/glossary/repositories/glossary_repository.dart index 9702db8e..c23a7ba7 100644 --- a/lib/features/glossary/repositories/glossary_repository.dart +++ b/lib/features/glossary/repositories/glossary_repository.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class GlossaryRepository { final GlossaryService glossaryService; @@ -10,11 +9,7 @@ class GlossaryRepository { }); Future> getGlossaryListByQuery(String query) async { - try { - return (await glossaryService.getGlossaryByQuery(query: query)).glossariesList; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await glossaryService.getGlossaryByQuery(query: query)).glossariesList; + } } diff --git a/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart b/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart index bb60d139..b4ab6b41 100644 --- a/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart +++ b/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class MenuCubit extends Cubit { final ProfileRepository profileRepository; @@ -12,7 +13,8 @@ class MenuCubit extends Cubit { try { final passKey = await profileRepository.getPassKey(routeName: 'product_nutritional_value.index', routeParams: {}); return passKey; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); } return null; diff --git a/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart index 6c5c567c..60b0986d 100644 --- a/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart +++ b/lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart @@ -1,5 +1,6 @@ import 'dart:developer'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart'; import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; @@ -16,6 +17,7 @@ class NotificationSettingsCubit extends Cubit { final notificationSettings = await _notificationSettingsRepository.getNotificationSettings(); emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings)); } catch (e, stackTrace) { + await reportFrom(e, stackTrace, this); log('Error getting notification settings: $e', stackTrace: stackTrace); emit(NotificationSettingsStateError(errorMessage: e.toString())); } @@ -26,6 +28,7 @@ class NotificationSettingsCubit extends Cubit { final notificationSettings = await _notificationSettingsRepository.updateNotificationSettings(data); emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings)); } catch (e, stackTrace) { + await reportFrom(e, stackTrace, this); log('Error updating notification settings: $e', stackTrace: stackTrace); emit(NotificationSettingsStateError(errorMessage: e.toString())); } diff --git a/lib/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart b/lib/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart index 3954e248..3a8b07ef 100644 --- a/lib/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart +++ b/lib/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart'; import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class BlockedUsersCubit extends Cubit { final BlockedUsersRepository blockedUsersRepository; @@ -13,17 +14,26 @@ class BlockedUsersCubit extends Cubit { try { emit(BlockedUsersState.loaded(profilesList: await blockedUsersRepository.getBlockedUsers())); } catch (e, st) { + await reportFrom(e, st, this); emit(BlockedUsersState.error(errorMessage: '$e $st')); } } Future blockUser(String slug) async { - await blockedUsersRepository.blockUserBySlug(slug); - await getBlockedUsers(); + try { + await blockedUsersRepository.blockUserBySlug(slug); + await getBlockedUsers(); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future unblockUser(String slug) async { - await blockedUsersRepository.unblockUserBySlug(slug); - await getBlockedUsers(); + try { + await blockedUsersRepository.unblockUserBySlug(slug); + await getBlockedUsers(); + } catch (e, st) { + await reportFrom(e, st, this); + } } } diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index dc4e3d82..859b1b6f 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/app_config.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:eatvio_app/core/error_reporting/error_reporter.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'; @@ -144,7 +144,7 @@ class MenuScreen extends StatelessWidget { try { await sl().post('${sl().baseUrl}/api/profile/logout'); } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); + await sl().report(e, s, origin: 'MenuScreen'); } finally { await sl().clear(); await TokenStorage.removeTokenInStorage(); diff --git a/lib/features/menu/presentation/repositories/blocked_users_repository.dart b/lib/features/menu/presentation/repositories/blocked_users_repository.dart index 2620252f..fe76c0d8 100644 --- a/lib/features/menu/presentation/repositories/blocked_users_repository.dart +++ b/lib/features/menu/presentation/repositories/blocked_users_repository.dart @@ -1,7 +1,5 @@ -import 'dart:developer'; import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class BlockedUsersRepository { final BlockedUserService blockedUserService; @@ -11,40 +9,25 @@ class BlockedUsersRepository { }); Future> getBlockedUsers() async { - try { - return (await blockedUserService.getBlockedUsers()).profilesList; - } catch (e, st) { - log(e.toString(), stackTrace: st); - await Sentry.captureException(e, stackTrace: st); - rethrow; - } + return (await blockedUserService.getBlockedUsers()).profilesList; + } Future blockUserBySlug(String slug) async { - try { - return await blockedUserService.blockUser( - data: { - 'slug': slug, - }, - ); - } catch (e, st) { - log(e.toString(), stackTrace: st); - await Sentry.captureException(e, stackTrace: st); - rethrow; - } + return await blockedUserService.blockUser( + data: { + 'slug': slug, + }, + ); + } Future unblockUserBySlug(String slug) async { - try { - return await blockedUserService.unblockUser( - data: { - 'slug': slug, - }, - ); - } catch (e, st) { - log(e.toString(), stackTrace: st); - await Sentry.captureException(e, stackTrace: st); - rethrow; - } + return await blockedUserService.unblockUser( + data: { + 'slug': slug, + }, + ); + } } diff --git a/lib/features/menu/repositories/notification_settings_repository.dart b/lib/features/menu/repositories/notification_settings_repository.dart index 81d782ef..3b46554e 100644 --- a/lib/features/menu/repositories/notification_settings_repository.dart +++ b/lib/features/menu/repositories/notification_settings_repository.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/features/menu/models/notification_settings_model.dart'; import 'package:eatvio_app/features/menu/services/notification_settings_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class NotificationSettingsRepository { final NotificationSettingsService _notificationSettingsService; @@ -8,20 +7,12 @@ class NotificationSettingsRepository { NotificationSettingsRepository(this._notificationSettingsService); Future getNotificationSettings() async { - try { - return await _notificationSettingsService.getNotificationSettings(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await _notificationSettingsService.getNotificationSettings(); + } Future updateNotificationSettings(Map data) async { - try { - return await _notificationSettingsService.updateNotificationSettings(data: data); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await _notificationSettingsService.updateNotificationSettings(data: data); + } } diff --git a/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart b/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart index 398fdb63..be5fcca9 100644 --- a/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart +++ b/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class MyDietsCubit extends Cubit { final MyDietsRepository myDietsRepository; @@ -212,6 +213,7 @@ class MyDietsCubit extends Cubit { ), ); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit( state.copyWith(activeDietsError: e.toString()), @@ -242,7 +244,8 @@ class MyDietsCubit extends Cubit { error: null, ), ); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit( state.copyWith(draftDietsError: e.toString()), ); @@ -272,7 +275,8 @@ class MyDietsCubit extends Cubit { error: null, ), ); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit( state.copyWith(personalDietsError: e.toString()), ); @@ -283,7 +287,8 @@ class MyDietsCubit extends Cubit { try { await myDietsRepository.deleteDiet(dietId); return true; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit( state.copyWith(error: e.toString()), ); @@ -299,6 +304,7 @@ class MyDietsCubit extends Cubit { ); return statusCode; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); return 0; } @@ -312,6 +318,7 @@ class MyDietsCubit extends Cubit { ); return true; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); return false; } diff --git a/lib/features/my_diets/repositories/my_diets_repository.dart b/lib/features/my_diets/repositories/my_diets_repository.dart index df90c157..b579eebb 100644 --- a/lib/features/my_diets/repositories/my_diets_repository.dart +++ b/lib/features/my_diets/repositories/my_diets_repository.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class MyDietsRepository { final MyDietsService myDietsService; @@ -8,12 +7,8 @@ class MyDietsRepository { MyDietsRepository({required this.myDietsService}); Future deleteDiet(int dietId) async { - try { - await myDietsService.deleteDiet(dietId: dietId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await myDietsService.deleteDiet(dietId: dietId); + } Future getDiets({ @@ -24,18 +19,14 @@ class MyDietsRepository { String? sorting, String? state, }) async { - try { - return (await myDietsService.getDiets( - page: page, - query: query, - types: types, - duration: duration, - sorting: sorting, - state: state, - )); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await myDietsService.getDiets( + page: page, + query: query, + types: types, + duration: duration, + sorting: sorting, + state: state, + )); + } } diff --git a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart index 184cd44f..802391cb 100644 --- a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart +++ b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart @@ -6,6 +6,7 @@ import 'package:eatvio_app/features/my_finance/models/payment_url_model.dart'; import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart'; import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class UserCardsCubit extends Cubit { final CardsRepository cardsRepository; @@ -15,6 +16,7 @@ class UserCardsCubit extends Cubit { try { return await cardsRepository.addCard(); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); } return null; @@ -27,6 +29,7 @@ class UserCardsCubit extends Cubit { final cards = await cardsRepository.getCards(); emit(state.copyWith(cards: cards, state: BaseState.success)); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(state.copyWith(state: BaseState.error, error: e.toString())); } @@ -47,6 +50,7 @@ class UserCardsCubit extends Cubit { try { await cardsRepository.markAsDefault(id: id); } catch (e, st) { + await reportFrom(e, st, this); emit(state.copyWith(cards: oldCards)); log(e.toString(), stackTrace: st); } @@ -61,6 +65,7 @@ class UserCardsCubit extends Cubit { emit(state.copyWith(cards: cards)); await cardsRepository.deleteCard(id: id); } catch (e, st) { + await reportFrom(e, st, this); emit(state.copyWith(cards: oldCards, error: e.toString(), state: BaseState.error)); log(e.toString(), stackTrace: st); } @@ -71,6 +76,7 @@ class UserCardsCubit extends Cubit { await cardsRepository.collectCharge(chosenPlaId: chosenPlaId); return true; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); } return false; diff --git a/lib/features/my_finance/repositories/cards_repository.dart b/lib/features/my_finance/repositories/cards_repository.dart index 9c77a917..8f887d00 100644 --- a/lib/features/my_finance/repositories/cards_repository.dart +++ b/lib/features/my_finance/repositories/cards_repository.dart @@ -1,53 +1,32 @@ import 'package:eatvio_app/features/my_finance/models/card_model.dart'; import 'package:eatvio_app/features/my_finance/models/payment_url_model.dart'; import 'package:eatvio_app/features/my_finance/services/cards_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class CardsRepository { final CardsService cardsService; CardsRepository({required this.cardsService}); Future addCard() async { - try { - return await cardsService.addCard(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await cardsService.addCard(); + } Future> getCards() async { - try { - return await cardsService.getCards(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await cardsService.getCards(); + } Future markAsDefault({required int id}) async { - try { - await cardsService.markAsDefault(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await cardsService.markAsDefault(id: id); + } Future deleteCard({required int id}) async { - try { - await cardsService.deleteCard(id: id); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await cardsService.deleteCard(id: id); + } Future collectCharge({required int chosenPlaId}) async { - try { - await cardsService.collectCharge(chosenPlanId: chosenPlaId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await cardsService.collectCharge(chosenPlanId: chosenPlaId); + } } diff --git a/lib/features/my_plans/presentation/cubit/my_plans_cubit.dart b/lib/features/my_plans/presentation/cubit/my_plans_cubit.dart index 33462563..129bfc9d 100644 --- a/lib/features/my_plans/presentation/cubit/my_plans_cubit.dart +++ b/lib/features/my_plans/presentation/cubit/my_plans_cubit.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class MyPlansCubit extends Cubit { final MyPlansRepository myPlansRepository; @@ -145,7 +146,8 @@ class MyPlansCubit extends Cubit { maxActivePlanPrice: maxPlanPrice, ), ); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit( state.copyWith(error: e.toString()), ); @@ -185,7 +187,8 @@ class MyPlansCubit extends Cubit { maxDraftPlanPrice: maxPlanPrice, ), ); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit( state.copyWith(error: e.toString()), ); @@ -197,7 +200,8 @@ class MyPlansCubit extends Cubit { await myPlansRepository.deletePlan(slug); await loadAllPlans(); return true; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit( state.copyWith(error: e.toString()), ); @@ -209,7 +213,8 @@ class MyPlansCubit extends Cubit { try { await myPlansRepository.unpublish(slug); return true; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); return false; } } diff --git a/lib/features/my_plans/repositories/my_diets_repository.dart b/lib/features/my_plans/repositories/my_diets_repository.dart index a378bc4f..177023d5 100644 --- a/lib/features/my_plans/repositories/my_diets_repository.dart +++ b/lib/features/my_plans/repositories/my_diets_repository.dart @@ -1,8 +1,5 @@ -import 'dart:developer'; - import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class MyPlansRepository { final MyPlansService myPlansService; @@ -10,30 +7,17 @@ class MyPlansRepository { MyPlansRepository({required this.myPlansService}); Future deletePlan(String slug) async { - try { - await myPlansService.deletePlan(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await myPlansService.deletePlan(slug: slug); + } Future getPlans({int page = 1, String? query, String? types, int? duration, int? minPrice, int? maxPrice}) async { - try { - return (await myPlansService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice)); - } catch (e, st) { - log(e.toString(), error: e, stackTrace: st); - await Sentry.captureException(e, stackTrace: st); - rethrow; - } + return (await myPlansService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice)); + } Future unpublish(String slug) async { - try { - return await myPlansService.unpublish(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await myPlansService.unpublish(slug: slug); + } } diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart index 51771c24..a3c5827d 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/features/notifications/models/notification_model.dart import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart'; import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class NotificationsCubit extends Cubit { final List _notifications = []; @@ -16,10 +17,14 @@ class NotificationsCubit extends Cubit { NotificationsCubit({required this.wsManager, required this.notificationsRepository}) : super(const NotificationsState.empty()); Future connectToWebSocket() async { - await _newNotificationsSubscription?.cancel(); - _newNotificationsSubscription = wsManager.newNotificationsStream?.listen((event) { - _handleNotification(event.data); - }); + try { + await _newNotificationsSubscription?.cancel(); + _newNotificationsSubscription = wsManager.newNotificationsStream?.listen((event) { + _handleNotification(event.data); + }); + } catch (e, st) { + await reportFrom(e, st, this); + } } BaseNotificationModel _parseNotification(Map notificationJson, NotificationType type) { @@ -341,12 +346,21 @@ class NotificationsCubit extends Cubit { ), NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt ?? jsonEncode(notificationJson))), }; - } catch (e) { + } catch (e, st) { + unawaited(reportFrom(e, st, this)); return UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now()); } } void _handleNotification(String notificationData) { + try { + _handleNotificationParsed(notificationData); + } catch (e, st) { + unawaited(reportFrom(e, st, this)); + } + } + + void _handleNotificationParsed(String notificationData) { final json = jsonDecode(notificationData) as Map; // Улучшенная логика определения типа уведомления (порядок: database_type → type → notification_class_name) @@ -420,6 +434,7 @@ class NotificationsCubit extends Cubit { ); } } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); } } @@ -456,6 +471,7 @@ class NotificationsCubit extends Cubit { emit(currentState.copyWith(isLoadingMore: false)); } } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(currentState.copyWith(isLoadingMore: false)); } @@ -471,6 +487,7 @@ class NotificationsCubit extends Cubit { await getNotifications(); emit(currentState.copyWith(isLoading: false, hasUnreadNotifications: false)); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(currentState.copyWith(isLoading: false)); } diff --git a/lib/features/notifications/repository/notifications_repository.dart b/lib/features/notifications/repository/notifications_repository.dart index 6508ee2e..449154b0 100644 --- a/lib/features/notifications/repository/notifications_repository.dart +++ b/lib/features/notifications/repository/notifications_repository.dart @@ -1,27 +1,16 @@ -import 'dart:developer'; import 'package:eatvio_app/features/notifications/service/notifications_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class NotificationsRepository { final NotificationsService notificationsService; NotificationsRepository({required this.notificationsService}); Future?> getNotifications({required int page}) async { - try { - return await notificationsService.getNotifications(page: page); - } catch (e, s) { - log(e.toString()); - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await notificationsService.getNotifications(page: page); + } Future markNotificationsAsRead() async { - try { - await notificationsService.markNotificationsAsRead(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await notificationsService.markNotificationsAsRead(); + } } diff --git a/lib/features/order_info/cubit/order_info_cubit.dart b/lib/features/order_info/cubit/order_info_cubit.dart index af934fab..ff331c01 100644 --- a/lib/features/order_info/cubit/order_info_cubit.dart +++ b/lib/features/order_info/cubit/order_info_cubit.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/order_info/cubit/order_info_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class OrderInfoCubit extends Cubit { OrderInfoCubit({required this.chatRepository, required this.profileRepository}) : super(OrderInfoState()); @@ -15,22 +16,32 @@ class OrderInfoCubit extends Cubit { if (plan == null) return null; final paymentModel = await chatRepository.getPaymentLink(id: plan.id); return paymentModel.paymentUrl; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); return null; } } - Future getChosenPlan({required int id, String? customerSlug}) async { + Future getChosenPlan({required int id, String? customerSlug, bool isConsultantView = false}) async { try { emit(state.copyWith(isLoading: true)); final chosenPlan = await chatRepository.getChosenPlan(id: id); + if (customerSlug != null) { final profile = await profileRepository.getProfileBySlug(customerSlug); - emit(state.copyWith(customerProfile: profile)); + emit(state.copyWith(customerProfile: profile, isLoading: false, chosenPlan: chosenPlan)); + return; + } + if (isConsultantView) { + final chatRoom = await chatRepository.getChatRoom(chosenPlan.chatId); + final customerSlug = chatRoom.users.receiver.slug; + final profile = await profileRepository.getProfileBySlug(customerSlug); + emit(state.copyWith(customerProfile: profile, isLoading: false, chosenPlan: chosenPlan)); } emit(state.copyWith(isLoading: false, chosenPlan: chosenPlan)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); emit(state.copyWith(isLoading: false)); } diff --git a/lib/features/order_info/presentation/info_by_order_screen.dart b/lib/features/order_info/presentation/info_by_order_screen.dart index adce3286..15ad28dd 100644 --- a/lib/features/order_info/presentation/info_by_order_screen.dart +++ b/lib/features/order_info/presentation/info_by_order_screen.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/core/locator_service.dart'; +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/features/cancel_plan_bottom_sheet/cancel_plan_bottom_sheet.dart'; @@ -24,20 +25,21 @@ import 'package:go_router/go_router.dart'; class InfoByOrderScreen extends StatelessWidget { static const name = 'infoByOrderScreen'; static const path = '/info-by-order-screen'; - const InfoByOrderScreen({super.key, this.successfulPayment, required this.planId, this.customerSlug}); + const InfoByOrderScreen({super.key, this.successfulPayment, required this.planId, this.customerSlug, this.isConsultantView = false}); final bool? successfulPayment; final int planId; final String? customerSlug; + final bool isConsultantView; @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => sl()..getChosenPlan(id: planId, customerSlug: customerSlug), + create: (context) => sl()..getChosenPlan(id: planId, customerSlug: customerSlug, isConsultantView: isConsultantView), child: InfoByOrderScreenContent(successfulPayment: successfulPayment, planId: planId, customerSlug: customerSlug), ); } } -class InfoByOrderScreenContent extends StatefulWidget { +class InfoByOrderScreenContent extends StatefulWidget { const InfoByOrderScreenContent({super.key, required this.planId, required this.successfulPayment, this.customerSlug}); final int planId; final bool? successfulPayment; @@ -61,7 +63,11 @@ class _InfoByOrderScreenContentState extends State { leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { - GoRouter.of(context).pop(); + if (GoRouter.of(context).canPop()) { + GoRouter.of(context).pop(); + } else { + GoRouter.of(context).go(RouteConstants.feedScreen); + } }, ), ), @@ -75,7 +81,11 @@ class _InfoByOrderScreenContentState extends State { leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { - GoRouter.of(context).pop(); + if (GoRouter.of(context).canPop()) { + GoRouter.of(context).pop(); + } else { + GoRouter.of(context).go(RouteConstants.feedScreen); + } }, ), ), @@ -110,7 +120,11 @@ class _InfoByOrderScreenContentState extends State { leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { - GoRouter.of(context).pop(); + if (GoRouter.of(context).canPop()) { + GoRouter.of(context).pop(); + } else { + GoRouter.of(context).go(RouteConstants.feedScreen); + } }, ), ), @@ -134,7 +148,7 @@ class _InfoByOrderScreenContentState extends State { const SizedBox(height: 8), OptionWidget(option: chosenOption, disableDuration: true, duration: plan.plan.duration), const SizedBox(height: 8), - PlanState(state: plan.chosenPlanState, hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,), + PlanState(state: plan.chosenPlanState, hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false), const SizedBox(height: 20), if (widget.successfulPayment == null) CustomOutlinedButton( diff --git a/lib/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart b/lib/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart index cde49e7e..0afb7d3e 100644 --- a/lib/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart +++ b/lib/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart @@ -4,6 +4,7 @@ import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_state.dart'; import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class PersonalDietSuggestionCubit extends Cubit { final DietRepository dietRepository; @@ -17,6 +18,7 @@ class PersonalDietSuggestionCubit extends Cubit { final diet = await dietRepository.getDiet(dietId); emit(state.copyWith(isLoading: false, dietName: diet.name)); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(state.copyWith(isLoading: false, error: e.toString())); } @@ -36,6 +38,7 @@ class PersonalDietSuggestionCubit extends Cubit { consultationsWithoutDiet: consultationWithoutDiet, )); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); emit(state.copyWith(isLoading: false, error: e.toString())); } @@ -48,6 +51,7 @@ class PersonalDietSuggestionCubit extends Cubit { await personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId); return true; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); return false; } @@ -60,6 +64,7 @@ class PersonalDietSuggestionCubit extends Cubit { await personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId); return true; } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); return false; } diff --git a/lib/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart b/lib/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart index 784d9857..b966dafd 100644 --- a/lib/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart +++ b/lib/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class PersonalDietSuggestionRepository { final PersonalDietSuggestionService personalDietSuggestionService; @@ -8,29 +7,17 @@ class PersonalDietSuggestionRepository { PersonalDietSuggestionRepository({required this.personalDietSuggestionService}); Future> getConsultationWithPersonalDiets() async{ - try { - return (await personalDietSuggestionService.getConsultationWithPersonalDiets()).data; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await personalDietSuggestionService.getConsultationWithPersonalDiets()).data; + } Future attachPersonalDiet({required int consultationId, required int dietId}) async { - try { - return personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId); + } Future reattachPersonalDiet({required int consultationId, required int dietId}) async { - try { - return await personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId); + } } diff --git a/lib/features/plans/presentation/cubit/about_plan_cubit.dart b/lib/features/plans/presentation/cubit/about_plan_cubit.dart index 85d26b0f..6d43871d 100644 --- a/lib/features/plans/presentation/cubit/about_plan_cubit.dart +++ b/lib/features/plans/presentation/cubit/about_plan_cubit.dart @@ -8,6 +8,7 @@ import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_state.dart'; import 'package:eatvio_app/features/plans/repositories/plan_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class AboutPlanCubit extends Cubit { final PlanRepository planRepository; @@ -21,6 +22,7 @@ class AboutPlanCubit extends Cubit { } emit(state.copyWith(planModel: plan, error: null)); } catch (e, stackTrace) { + await reportFrom(e, stackTrace, this); log(e.toString(), stackTrace: stackTrace); emit(state.copyWith(error: e.toString(), isLoading: false)); } @@ -32,6 +34,7 @@ class AboutPlanCubit extends Cubit { final plan = await planRepository.getPlan(slug); emit(state.copyWith(planModel: plan, error: null, isLoading: false)); } catch (e, stackTrace) { + await reportFrom(e, stackTrace, this); log(e.toString(), stackTrace: stackTrace); emit(state.copyWith(error: e.toString(), isLoading: false)); } @@ -59,7 +62,8 @@ class AboutPlanCubit extends Cubit { EasyDebounce.debounce('like_plan_$slug', const Duration(seconds: 1), () async { try { await planRepository.likePlan(slug); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); if (index != null) { planRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentPlan.wasLikedByMe, totalLikes: currentPlan.totalLikes, index: index)); @@ -80,7 +84,8 @@ class AboutPlanCubit extends Cubit { EasyDebounce.debounce('add_or_remove_plan_to_favorites', const Duration(seconds: 1), () async { try { await planRepository.addToFavorite(slug); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); if (index != null) { @@ -102,7 +107,8 @@ class AboutPlanCubit extends Cubit { EasyDebounce.debounce('add_or_remove_plan_to_favorites', const Duration(seconds: 1), () async { try { await planRepository.removeFromFavorite(slug); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); if (index != null) { @@ -123,11 +129,13 @@ class AboutPlanCubit extends Cubit { return (await planRepository.chosePlan(slug, body), null); } } on DioException catch (e, st) { + await reportFrom(e, st, this); if (e.response?.statusCode == 400) { return (null, 'hasPlan'); } log(e.toString(), stackTrace: st); } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); } return (null, ''); diff --git a/lib/features/plans/repositories/plan_repository.dart b/lib/features/plans/repositories/plan_repository.dart index fcc942f9..67ea6003 100644 --- a/lib/features/plans/repositories/plan_repository.dart +++ b/lib/features/plans/repositories/plan_repository.dart @@ -3,7 +3,6 @@ import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:eatvio_app/features/plans/services/plan_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class PlanRepository { final PlanService planService; @@ -18,48 +17,28 @@ class PlanRepository { Stream get planCommentsState => _planCommentsStateStreamController.stream; Future getPlan(String slug) async { - try { - return planService.getPlan(slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return planService.getPlan(slug); + } Future likePlan(String slug) async { - try { - await planService.likePlan(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await planService.likePlan(slug: slug); + } Future addToFavorite(String slug) async { - try { - await planService.addToFavorite(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await planService.addToFavorite(slug: slug); + } Future removeFromFavorite(String slug) async { - try { - await planService.removeFromFavorite(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await planService.removeFromFavorite(slug: slug); + } Future chosePlan(String slug, Map option ) async { - try { - return await planService.chosePlan(slug: slug, option: option); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await planService.chosePlan(slug: slug, option: option); + } void emitLikesStateToFeed(LikesState state) => _planLikeStateStreamController.add(state); diff --git a/lib/features/plans/repositories/plans_repository.dart b/lib/features/plans/repositories/plans_repository.dart index 74aec6b1..10386890 100644 --- a/lib/features/plans/repositories/plans_repository.dart +++ b/lib/features/plans/repositories/plans_repository.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/plans/services/plans_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class PlansRepository { final PlansService plansService; @@ -8,12 +7,8 @@ class PlansRepository { PlansRepository({required this.plansService}); Future> getPlansBySlug(String slug) async { - try { - final plans = (await plansService.getPlansBySlug(author: slug)).data; - return plans.where((plan) => plan.state == 'active').toList(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + final plans = (await plansService.getPlansBySlug(author: slug)).data; + return plans.where((plan) => plan.state == 'active').toList(); + } } diff --git a/lib/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart b/lib/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart index 4b5d86b6..a6370e48 100644 --- a/lib/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart +++ b/lib/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:eatvio_app/features/plans/repositories/plans_repository.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class PlansCubit extends Cubit { final PlansRepository plansRepository; @@ -15,7 +16,8 @@ class PlansCubit extends Cubit { emit(const PlansState.loading()); try { emit(PlansState.loaded(plansList: await plansRepository.getPlansBySlug(slug))); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); emit(PlansState.error(errorMessage: e.toString())); } 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 9538de53..db3bf384 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -10,6 +10,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; import 'package:go_router/go_router.dart'; class ProfileCubit extends Cubit { @@ -41,9 +42,9 @@ class ProfileCubit extends Cubit { certPhotos: certPhotos, ), ); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(ProfileState.error(errorMessage: e.toString())); - rethrow; } } @@ -67,6 +68,7 @@ class ProfileCubit extends Cubit { ), ); } catch (e, stackTrace) { + await reportFrom(e, stackTrace, this); log(e.toString(), stackTrace: stackTrace); emit(ProfileState.error(errorMessage: e.toString())); } @@ -101,7 +103,8 @@ class ProfileCubit extends Cubit { hipCircumference: hipCircumference, ); await getMyProfile(period: _currentPeriod); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(ProfileState.error(errorMessage: e.toString())); } } @@ -110,7 +113,8 @@ class ProfileCubit extends Cubit { try { await profileRepository.addWeight(weight: weight); await getMyProfile(period: _currentPeriod); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(ProfileState.error(errorMessage: e.toString())); } } @@ -121,27 +125,37 @@ class ProfileCubit extends Cubit { final userProfile = await profileRepository.getProfileBySlug(slug); if (chatIdOrError == '404') { AppRouter.navigatorKey.currentContext?.pushNamed('emptyChatScreen', extra: userProfile); - } else if (chatIdOrError == 'error') { - showErrorToast('Ошибка создания чата'); } else { AppRouter.navigatorKey.currentContext?.pushNamed('chatScreen', extra: {'uuid': chatIdOrError}); return chatIdOrError; } } catch (e, st) { + await reportFrom(e, st, this); log(e.toString(), stackTrace: st); + showErrorToast('Ошибка создания чата'); return ''; } return null; } Future uploadPhoto(File file) async { - await profileRepository.uploadPhoto(await ImageCompressUtility.compressFile(file)); - return await getMyProfile(); + try { + await profileRepository.uploadPhoto(await ImageCompressUtility.compressFile(file)); + await getMyProfile(); + } catch (e, st) { + await reportFrom(e, st, this); + emit(ProfileState.error(errorMessage: e.toString())); + } } Future deletePhoto() async { - await profileRepository.deletePhoto(); - return await getMyProfile(); + try { + await profileRepository.deletePhoto(); + await getMyProfile(); + } catch (e, st) { + await reportFrom(e, st, this); + emit(ProfileState.error(errorMessage: e.toString())); + } } Future hideCertPhoto(int id) async { @@ -153,7 +167,8 @@ class ProfileCubit extends Cubit { if (currentState is ProfileStateLoaded) { emit(currentState.copyWith(certPhotos: certPhotos)); } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); } } @@ -167,7 +182,8 @@ class ProfileCubit extends Cubit { if (currentState is ProfileStateLoaded) { emit(currentState.copyWith(certPhotos: certPhotos)); } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); } } diff --git a/lib/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart b/lib/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart index 693fe8d5..c54d73bf 100644 --- a/lib/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart +++ b/lib/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class SubCubit extends Cubit { final ProfileRepository profileRepository; @@ -16,7 +17,8 @@ class SubCubit extends Cubit { return; } emit(SubState.loaded(subList: await profileRepository.getSubscriptionsBySlug(slug, query))); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(SubState.error(errorMessage: e.toString())); } } @@ -30,16 +32,25 @@ class SubCubit extends Cubit { return; } emit(SubState.loaded(subList: await profileRepository.getSubscribersBySlug(slug, query))); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(SubState.error(errorMessage: e.toString())); } } Future subscribe(String slug) async { - return await profileRepository.subscribe(slug); + try { + await profileRepository.subscribe(slug); + } catch (e, st) { + await reportFrom(e, st, this); + } } Future unsubscribe(String slug) async { - return await profileRepository.unsubscribe(slug); + try { + await profileRepository.unsubscribe(slug); + } catch (e, st) { + await reportFrom(e, st, this); + } } } diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 766e03c1..44ad5060 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -9,7 +9,6 @@ import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:eatvio_app/features/profile/models/sub_model.dart'; import 'package:eatvio_app/features/profile/models/weight_model.dart'; import 'package:eatvio_app/features/profile/services/profile_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class ProfileRepository { final ProfileService profileService; @@ -17,210 +16,101 @@ class ProfileRepository { ProfileRepository({required this.profileService}); Future getProfileBySlug(String slug) async { - try { - return await profileService.getProfileBySlug(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return profileService.getProfileBySlug(slug: slug); } Future getProfileDetailsBySlug(String slug) async { - try { - return await profileService.getProfileDetailsBySlug(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return profileService.getProfileDetailsBySlug(slug: slug); } Future getMyProfile() async { - try { - return await profileService.getMyProfile(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return profileService.getMyProfile(); } Future getMyProfileDetails() async { - try { - return await profileService.getMyProfileDetails(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return profileService.getMyProfileDetails(); } Future addCircumference({required int chestCircumference, required int waistCircumference, required int hipCircumference}) async { - try { - return (await profileService.addCircumference( - chestCircumference: chestCircumference, - waistCircumference: waistCircumference, - hipCircumference: hipCircumference, - )); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return profileService.addCircumference( + chestCircumference: chestCircumference, + waistCircumference: waistCircumference, + hipCircumference: hipCircumference, + ); } Future addWeight({required double weight}) async { - try { - return (await profileService.addWeight(weight: weight)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return profileService.addWeight(weight: weight); } Future> getBlogPostsByAuthorSlug({required String slug}) async { - try { - return (await profileService.getBlogPostByAuthorSlug(slug: slug)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return profileService.getBlogPostByAuthorSlug(slug: slug); } Future> getWeight({required String period, String? slug}) async { - try { - if (slug != null) { - return (await profileService.getWeightBySlug(period: period, slug: slug)); - } else { - return (await profileService.getWeight(period: period)); - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + if (slug != null) { + return profileService.getWeightBySlug(period: period, slug: slug); } + return profileService.getWeight(period: period); } Future> getCircumferences({required String period, String? slug}) async { - try { - if (slug != null) { - return (await profileService.getCircumferencesBySlug(period: period, slug: slug)); - } else { - return (await profileService.getCircumferences(period: period)); - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + if (slug != null) { + return profileService.getCircumferencesBySlug(period: period, slug: slug); } + return profileService.getCircumferences(period: period); } Future> getSubscribersBySlug(String slug, [String? query]) async { - try { - return (await profileService.getSubscribers(slug: slug, query: query)).data; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await profileService.getSubscribers(slug: slug, query: query)).data; } Future> getSubscriptionsBySlug(String slug, [String? query]) async { - try { - return (await profileService.getSubscriptions(slug: slug, query: query)).data; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await profileService.getSubscriptions(slug: slug, query: query)).data; } Future subscribe(String slug) async { - try { - return await profileService.subscribe(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return profileService.subscribe(slug: slug); } Future unsubscribe(String slug) async { - try { - return await profileService.unsubscribe(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return profileService.unsubscribe(slug: slug); } Future uploadPhoto(File file) async { - try { - return await profileService.uploadPhoto(photo: file); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return profileService.uploadPhoto(photo: file); } Future deletePhoto() async { - try { - return await profileService.deletePhoto(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return profileService.deletePhoto(); } Future> getChosenDiets() async { - try { - return (await profileService.getChosenDiets()).diets; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await profileService.getChosenDiets()).diets; } Future getPassKey({required String routeName, Map? routeParams}) async { - try { - return (await profileService.getPassKey( - json: PassKeyParams(routeName: routeName, routeParams: routeParams), - )).data['key_is']!; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await profileService.getPassKey( + json: PassKeyParams(routeName: routeName, routeParams: routeParams), + )).data['key_is']!; } Future getMyPhone() async { - try { - return (jsonDecode(await profileService.getMyPhone())['phone']); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (jsonDecode(await profileService.getMyPhone())['phone']); } Future sendEmailCode({String? newEmail}) async { - try { - if (newEmail != null) { - return await profileService.sendEmailCode(data: {'new_email': newEmail}); - } else { - return await profileService.sendEmailCode(); - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + if (newEmail != null) { + return profileService.sendEmailCode(data: {'new_email': newEmail}); } + return profileService.sendEmailCode(); } - Future tryVerifyEmailByCode(String code) async { - try { - await profileService.tryVerifyEmailByCode(code: "{\"code\": \"$code\"}"); - return true; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - return false; - } + Future tryVerifyEmailByCode(String code) async { + await profileService.tryVerifyEmailByCode(code: "{\"code\": \"$code\"}"); } Future deleteAccount() async { - try { - await profileService.deleteAccount(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await profileService.deleteAccount(); } } diff --git a/lib/features/recipe/presentation/cubit/about_recipe_cubit.dart b/lib/features/recipe/presentation/cubit/about_recipe_cubit.dart index 9e00eda9..9f8339d6 100644 --- a/lib/features/recipe/presentation/cubit/about_recipe_cubit.dart +++ b/lib/features/recipe/presentation/cubit/about_recipe_cubit.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository. import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class AboutRecipeCubit extends Cubit { AboutRecipeCubit({required this.recipeRepository, required this.profileRepository}) : super(const AboutRecipeState.initial()); @@ -23,7 +24,8 @@ class AboutRecipeCubit extends Cubit { recipeRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: recipe.commentsCount)); } emit(AboutRecipeState.loaded(recipe: recipe)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); emit(AboutRecipeState.error(message: e.toString())); } @@ -37,7 +39,8 @@ class AboutRecipeCubit extends Cubit { recipeRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: recipe.commentsCount)); } emit(AboutRecipeState.loaded(recipe: recipe)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); emit(AboutRecipeState.error(message: e.toString())); } @@ -76,7 +79,8 @@ class AboutRecipeCubit extends Cubit { () async { try { await recipeRepository.likeRecipe(id); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); if (recipeIndexInFeed != null) { recipeRepository @@ -107,7 +111,8 @@ class AboutRecipeCubit extends Cubit { () async { try { await recipeRepository.addToFavorite(id); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); if (recipeIndex != null) { @@ -138,7 +143,8 @@ class AboutRecipeCubit extends Cubit { () async { try { await recipeRepository.removeFromFavorite(id); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); if (recipeIndex != null) { diff --git a/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_cubit.dart b/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_cubit.dart index db46b7b4..715eefaa 100644 --- a/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_cubit.dart +++ b/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_cubit.dart @@ -8,7 +8,7 @@ import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/m import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class MyRecipesCubit extends Cubit { final MyRecipesRepository myRecipesRepository; @@ -31,7 +31,8 @@ class MyRecipesCubit extends Cubit { final myRecipes = await myRecipesRepository.getMyRecipes(); final List activeRecipes = myRecipes.where((recipe) => recipe.state == active).toList(); emit(MyRecipesState.loaded(recipes: activeRecipes, selectedState: active)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(const MyRecipesState.error()); } } @@ -42,7 +43,8 @@ class MyRecipesCubit extends Cubit { final myRecipes = await myRecipesRepository.getMyRecipes(); final List activeRecipes = myRecipes.where((recipe) => recipe.state == active).toList(); emit(MyRecipesState.loaded(recipes: activeRecipes, selectedState: active)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(const MyRecipesState.error()); } } @@ -53,7 +55,8 @@ class MyRecipesCubit extends Cubit { final myRecipes = await myRecipesRepository.getMyRecipes(); final List archivedRecipes = myRecipes.where((recipe) => recipe.state == archived).toList(); emit(MyRecipesState.loaded(recipes: archivedRecipes, selectedState: archived)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(const MyRecipesState.error()); } } @@ -64,13 +67,19 @@ class MyRecipesCubit extends Cubit { final myRecipes = await myRecipesRepository.getMyRecipes(); final List draftedRecipes = myRecipes.where((recipe) => recipe.state == drafted).toList(); emit(MyRecipesState.loaded(recipes: draftedRecipes, selectedState: drafted)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(const MyRecipesState.error()); } } - Future getRecipeById(String slug) async { - return await aboutRecipeRepository.getRecipe(slug); + Future getRecipeById(String slug) async { + try { + return await aboutRecipeRepository.getRecipe(slug); + } catch (e, st) { + await reportFrom(e, st, this); + return null; + } } /// Attempts to publish a drafted recipe @@ -81,10 +90,9 @@ class MyRecipesCubit extends Cubit { await recipeRepository.publishRecipe(recipe, true, slug); return true; } catch (e, st) { + await reportFrom(e, st, this); if (e is DioException && e.response?.statusCode == 422) { - log('Validation failed for publishing draft: \${e.response?.data}'); - } else { - await Sentry.captureException(e, stackTrace: st); + log('Validation failed for publishing draft: ${e.response?.data}'); } return false; } @@ -96,8 +104,8 @@ class MyRecipesCubit extends Cubit { await recipeRepository.draftExistingRecipe(id); return true; } catch (e, st) { - log('Failed to draft recipe: \$e'); - await Sentry.captureException(e, stackTrace: st); + await reportFrom(e, st, this); + log('Failed to draft recipe: $e'); return false; } } diff --git a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart index 9d0269de..ee6aa4ab 100644 --- a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart +++ b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart @@ -155,6 +155,10 @@ class _MyRecipeWidgetState extends State { final fullRecipe = await context.read().getRecipeById(widget.recipe.slug); if (context.mounted) { Navigator.pop(context); // close dialog + if (fullRecipe == null) { + showErrorToast('Не удалось загрузить рецепт'); + return; + } final result = await context.push(RecipeCreateScreen.path, extra: fullRecipe); if (!context.mounted) return; if (result == MyRecipesCubit.drafted) { @@ -226,11 +230,16 @@ class _MyRecipeWidgetState extends State { builder: (context) => const Center(child: CircularProgressIndicator()), ); final fullRecipe = await context.read().getRecipeById(widget.recipe.slug); + if (!context.mounted) return; + Navigator.pop(context); + if (fullRecipe == null) { + showErrorToast('Не удалось загрузить рецепт'); + return; + } final createCubit = sl(); await createCubit.recipeToEntity(fullRecipe); if (context.mounted) { - Navigator.pop(context); if (createCubit.validateAll()) { final success = await createCubit.publishRecipe(true, widget.recipe.slug); if (success && context.mounted) { diff --git a/lib/features/recipe/repositories/about_recipe_repository.dart b/lib/features/recipe/repositories/about_recipe_repository.dart index 4b6de461..8708a805 100644 --- a/lib/features/recipe/repositories/about_recipe_repository.dart +++ b/lib/features/recipe/repositories/about_recipe_repository.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class AboutRecipeRepository { final AboutRecipeService recipeService; @@ -17,48 +16,28 @@ class AboutRecipeRepository { AboutRecipeRepository({required this.recipeService}); Future getRecipe(String recipeID) async { - try { - return await recipeService.getRecipe(recipeID); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await recipeService.getRecipe(recipeID); + } Future likeRecipe(String slug) async { - try { - await recipeService.likeRecipe(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await recipeService.likeRecipe(slug: slug); + } Future addToFavorite(String slug) async { - try { - await recipeService.addToFavorite(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await recipeService.addToFavorite(slug: slug); + } Future removeFromFavorite(String slug) async { - try { - await recipeService.removeFromFavorite(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await recipeService.removeFromFavorite(slug: slug); + } Future deleteRecipe(String slug) async { - try { - await recipeService.deleteRecipe(slug: slug); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + await recipeService.deleteRecipe(slug: slug); + } void emitLikesStateToFeed(LikesState state) => _recipeLikeStateStreamController.add(state); diff --git a/lib/features/recipe/repositories/my_recipes_repository.dart b/lib/features/recipe/repositories/my_recipes_repository.dart index fb67f395..4b2b91e3 100644 --- a/lib/features/recipe/repositories/my_recipes_repository.dart +++ b/lib/features/recipe/repositories/my_recipes_repository.dart @@ -1,6 +1,5 @@ import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class MyRecipesRepository { final MyRecipesService myRecipesService; @@ -8,11 +7,7 @@ class MyRecipesRepository { MyRecipesRepository({required this.myRecipesService}); Future> getMyRecipes() async { - try { - return (await myRecipesService.getMyRecipes()).data; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return (await myRecipesService.getMyRecipes()).data; + } } \ No newline at end of file diff --git a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart index 3bd0a6a1..e9260cfd 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart @@ -9,9 +9,11 @@ import 'package:eatvio_app/core/prefs.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart'; +import 'package:eatvio_app/features/registration/registration_error_message.dart'; import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; import 'package:go_router/go_router.dart'; class RegistrationCubit extends Cubit with RegistrationDateCacheMixin { @@ -65,9 +67,11 @@ class RegistrationCubit extends Cubit with RegistrationDateCa cacheRegistrationDate(DateTime.now()); AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen'); } catch (e, st) { - log(e.toString(), stackTrace: st); - RegistrationAnalyticsService.reportRegistrationError(e.toString()); - emit(RegistrationState(sheet: state.sheet, error: e.toString())); + await reportFrom(e, st, this); + final message = registrationErrorMessage(e); + log(message, stackTrace: st); + RegistrationAnalyticsService.reportRegistrationError(message); + emit(RegistrationState(sheet: state.sheet, error: message)); } } @@ -76,8 +80,9 @@ class RegistrationCubit extends Cubit with RegistrationDateCa final location = (await registrationRepository.getLocationsListByQuery(query)); emit(RegistrationState(sheet: state.sheet.copyWith(responseBody: location.responseBody))); return location.locations; - } catch (e) { - rethrow; + } catch (e, st) { + await reportFrom(e, st, this); + return []; } } @@ -86,8 +91,9 @@ class RegistrationCubit extends Cubit with RegistrationDateCa final location = (await registrationRepository.getLocationsListByCoordinates(latitude, longitude)); emit(RegistrationState(sheet: state.sheet.copyWith(responseBody: location.responseBody))); return location.locations; - } catch (e) { - rethrow; + } catch (e, st) { + await reportFrom(e, st, this); + return []; } } } diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart index 7c9468d8..304af8e3 100644 --- a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart @@ -4,6 +4,7 @@ import 'package:eatvio_app/features/profile/repositories/profile_repository.dart import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class VerifyEmailCubit extends Cubit { final ProfileRepository profileRepository; @@ -16,7 +17,8 @@ class VerifyEmailCubit extends Cubit { final profile = await profileRepository.getMyProfile(); await profileRepository.sendEmailCode(); emit(VerifyEmailState.loaded(email: profile.email)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(const VerifyEmailState.error(errorMessage: 'Ошибка отправки кода.\nПовторите попытку позже')); } } @@ -24,18 +26,15 @@ class VerifyEmailCubit extends Cubit { Future tryVerify(String code) async { try { emit(const VerifyEmailState.verifyLoading()); - final result = await profileRepository.tryVerifyEmailByCode(code); - if (result == true) { - emit(const VerifyEmailState.verifySuccess()); - try { - RegistrationAnalyticsService.reportEmailVerification(); - } catch (e) { - // Игнорируем ошибки аналитики, верификация уже успешна - } - } else { - emit(const VerifyEmailState.verifyError()); + await profileRepository.tryVerifyEmailByCode(code); + emit(const VerifyEmailState.verifySuccess()); + try { + RegistrationAnalyticsService.reportEmailVerification(); + } catch (e, st) { + await reportFrom(e, st, this); } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(const VerifyEmailState.verifyError()); } } @@ -47,7 +46,8 @@ class VerifyEmailCubit extends Cubit { emit(const VerifyEmailState.changeEmailSuccess()); // Обновляем email в состоянии после успешной смены emit(VerifyEmailState.loaded(email: newEmail)); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(const VerifyEmailState.changeEmailError()); } } diff --git a/lib/features/registration/registration_error_message.dart b/lib/features/registration/registration_error_message.dart new file mode 100644 index 00000000..dfe1159b --- /dev/null +++ b/lib/features/registration/registration_error_message.dart @@ -0,0 +1,35 @@ +import 'package:dio/dio.dart'; + +/// Сообщение для UI / аналитики при ошибке регистрации (вызывать из кубита). +String registrationErrorMessage(Object error) { + if (error is String) { + return error; + } + if (error is DioException && error.response?.statusCode == 422) { + final data = error.response?.data; + if (data is Map) { + var errorMsg = ''; + if (data.containsKey('message')) { + errorMsg += '${data['message']}\n'; + } + if (data.containsKey('errors')) { + final errors = data['errors']; + if (errors is Map) { + errors.forEach((key, value) { + if (value is List && value.isNotEmpty) { + errorMsg += '${value.join('\n')}\n'; + } + }); + } + } + if (errorMsg.isNotEmpty) { + return errorMsg.trim(); + } + return 'Ошибка регистрации: $data'; + } + } + if (error is DioException) { + return error.message ?? error.toString(); + } + return error.toString(); +} diff --git a/lib/features/registration/repositories/registration_repository.dart b/lib/features/registration/repositories/registration_repository.dart index 7ab8c43a..fe02eea5 100644 --- a/lib/features/registration/repositories/registration_repository.dart +++ b/lib/features/registration/repositories/registration_repository.dart @@ -1,10 +1,9 @@ import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/locator_service.dart'; -import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; -import 'package:eatvio_app/features/registration/models/location_model.dart'; -import 'package:eatvio_app/features/registration/services/registration_service.dart'; import 'package:eatvio_app/core/env_config.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/features/registration/models/location_model.dart'; +import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; +import 'package:eatvio_app/features/registration/services/registration_service.dart'; class RegistrationRepository { final RegistrationService registrationService; @@ -12,94 +11,36 @@ class RegistrationRepository { RegistrationRepository({required this.registrationService}); Future tryRegister(UserRegistrationSheet sheet) async { - try { - final response = await registrationService.register(sheet: sheet.toJson()); - if (response.containsKey('token')) { - return response['token']!; - } else { - // Если нет токена, но есть message или errors - String errorMsg = ''; - if (response.containsKey('message')) { - errorMsg += '${response['message']!}\n'; - } - if (response.containsKey('errors')) { - final errors = response['errors']; - if (errors != null && errors is Map) { - (errors as Map).forEach((key, value) { - if (value is List && value.isNotEmpty) { - errorMsg += '${value.join('\n')}\n'; - } - }); - } - } - if (errorMsg.isNotEmpty) { - throw errorMsg.trim(); - } else { - throw 'Ошибка регистрации: $response'; - } - } - } on DioException catch (e, s) { - if (e.response != null && e.response?.statusCode == 422) { - final data = e.response?.data; - String errorMsg = ''; - if (data is Map) { - if (data.containsKey('message')) { - errorMsg += data['message'] + '\n'; - } - if (data.containsKey('errors')) { - final errors = data['errors']; - if (errors != null && errors is Map) { - (errors).forEach((key, value) { - if (value is List && value.isNotEmpty) { - errorMsg += '${value.join('\n')}\n'; - } - }); - } - } - } - if (errorMsg.isNotEmpty) { - throw errorMsg.trim(); - } else { - throw 'Ошибка регистрации: $data'; - } - } - await Sentry.captureException(e, stackTrace: s); - rethrow; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; + final response = await registrationService.register(sheet: sheet.toJson()); + if (response.containsKey('token')) { + return response['token']!; } + var errorMsg = ''; + if (response.containsKey('message')) { + errorMsg += '${response['message']}\n'; + } + if (response.containsKey('errors')) { + errorMsg += '${response['errors']}\n'; + } + if (errorMsg.isNotEmpty) { + throw errorMsg.trim(); + } + throw 'Ошибка регистрации: $response'; } Future getLocationsListByQuery(String query) async { - try { - return (await registrationService.getLocationsByAddress(query: query)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return registrationService.getLocationsByAddress(query: query); } Future getLocationsListByCoordinates(String latitude, String longitude) async { - try { - return (await registrationService.getLocationsByCoordinate(latitude: latitude, longitude: longitude)); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return registrationService.getLocationsByCoordinate(latitude: latitude, longitude: longitude); } Future emailIsReachable(String email) async { - try { - final response = await sl().getUri(Uri.parse('${EnvConfig.emailValidationApiUrl}?email=$email')); - if (response.data['result']['reachable'] != 'invalid') { - return true; - } else { - return false; - } - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - return false; + final response = await sl().getUri(Uri.parse('${EnvConfig.emailValidationApiUrl}?email=$email')); + if (response.data['result']['reachable'] != 'invalid') { + return true; } + return false; } } diff --git a/lib/features/registration_v2/cubit/registration_cubit.dart b/lib/features/registration_v2/cubit/registration_cubit.dart index cfc72b4b..585d1c41 100644 --- a/lib/features/registration_v2/cubit/registration_cubit.dart +++ b/lib/features/registration_v2/cubit/registration_cubit.dart @@ -1,267 +1,270 @@ -import 'dart:developer'; - -import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/token_storage.dart'; -import 'package:eatvio_app/core/utils/date_time_utils.dart'; -import 'package:eatvio_app/core/utils/validation_utils.dart'; -import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart'; -import 'package:eatvio_app/features/registration_v2/models/registration_model.dart'; -import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:gpassword/gpassword.dart'; - -class RegistrationCubitV2 extends Cubit { - final RegistrationRepositoryV2 registrationRepository; - RegistrationCubitV2({required this.registrationRepository}) : super(const RegistrationStateV2()); - - void setFirstName(String firstName) { - emit(state.copyWith(firstName: firstName)); - validateFirstName(); - } - - void setLastName(String lastName) { - emit(state.copyWith(lastName: lastName)); - validateLastName(); - } - - void setEmail(String email) { - emit(state.copyWith(email: email)); - validateEmail(); - } - - void setPhone(String phone) { - emit(state.copyWith(phone: phone)); - validatePhone(); - } - - void setBirthday(DateTime birthday) { - emit(state.copyWith(birthday: birthday)); - validateBirthday(); - } - - void setGender(String gender) { - emit(state.copyWith(gender: gender)); - validateGender(); - } - - void setWeight(int weight) { - emit(state.copyWith(weight: weight)); - validateWeight(); - } - - void setHeight(int height) { - emit(state.copyWith(height: height)); - validateHeight(); - } - - void setActivityCoefficient(double activityCoefficient) { - emit(state.copyWith(activityCoefficient: activityCoefficient)); - validateActivityCoefficient(); - } - - void setPassword(String password) { - emit(state.copyWith(password: password)); - } - - bool validateFirstName() { - if (state.firstName.isEmpty) { - emit(state.copyWith(isFirstNameValid: false)); - return false; - } - emit(state.copyWith(isFirstNameValid: true)); - return true; - } - - bool validateLastName() { - if (state.lastName.isEmpty) { - emit(state.copyWith(isLastNameValid: false)); - return false; - } - emit(state.copyWith(isLastNameValid: true)); - return true; - } - - bool validateEmail() { - if (state.email.isEmpty) { - emit(state.copyWith(isEmailValid: false)); - return false; - } - emit(state.copyWith(isEmailValid: true)); - return true; - } - - bool validatePhone() { - if (state.phone.isEmpty) { - emit(state.copyWith(isPhoneValid: false)); - return false; - } - emit(state.copyWith(isPhoneValid: true)); - return true; - } - - bool validateGender() { - if (state.gender.isEmpty) { - emit(state.copyWith(isGenderValid: false)); - return false; - } - emit(state.copyWith(isGenderValid: true)); - return true; - } - - bool validateWeight() { - if (state.weight == 0) { - emit(state.copyWith(isWeightValid: false)); - return false; - } - emit(state.copyWith(isWeightValid: true)); - return true; - } - - bool validateHeight() { - if (state.height == 0) { - emit(state.copyWith(isHeightValid: false)); - return false; - } - emit(state.copyWith(isHeightValid: true)); - return true; - } - - bool validateActivityCoefficient() { - if (state.activityCoefficient == 0) { - emit(state.copyWith(isActivityCoefficientValid: false)); - return false; - } - emit(state.copyWith(isActivityCoefficientValid: true)); - return true; - } - - bool validateBirthday() { - if (state.birthday == null) { - emit(state.copyWith(isBirthdayValid: false)); - return false; - } - emit(state.copyWith(isBirthdayValid: true)); - return true; - } - - void nextStep() { - emit(state.copyWith(step: 1)); - } - - void prevStep() { - emit(state.copyWith(step: 0)); - } - - bool validateAllFields() { - if (!validateFirstName() || - !validateLastName() || - !validateEmail() || - !validatePhone() || - !validateGender() || - !validateWeight() || - !validateHeight() || - !validateActivityCoefficient() || - !validateBirthday()) { - return false; - } - return true; - } - - Future<(bool, bool)> register() async { - if (!validateAllFields()) { - return (false, true); - } - final birthday = state.birthday?.format('yyyy-MM-dd'); - if (birthday == null) { - validateBirthday(); - return (false, true); - } - final password = await _generatePassword(); - final registrationModel = RegistrationModel( - firstName: state.firstName, - lastName: state.lastName, - email: state.email, - phone: ValidationUtils.normalizePhone(state.phone), - gender: state.gender, - password: password, - confirmPassword: password, - weight: state.weight, - height: state.height, - activityCoefficient: state.activityCoefficient, - birthday: birthday, - ); - bool didApiComplainOnValidation = false; - String errorMessage = ''; - try { - final token = await registrationRepository.register(registrationModel: registrationModel); - if (token != null) { - TokenStorage.saveTokenInStorage(token); - emit(state.copyWith(registrationError: null)); - return (true, false); - } - } on DioException catch (e) { - final errors = e.response?.data['errors'] as Map?; - final emailErrors = errors?['email'] as List?; - final phoneErrors = errors?['phone'] as List?; - final dateOfBirth = errors?['date_of_birth'] as List?; - - // Собираем все ошибки для отображения в диалоге - List allErrors = []; - - if (emailErrors != null && emailErrors.isNotEmpty) { - for (var error in emailErrors) { - allErrors.add(error.toString()); - if (error.toString() == 'Некорректный формат email') { - emit(state.copyWith(isEmailValid: false, emailDisplayError: 'Некорректный формат email')); - didApiComplainOnValidation = true; - } - if (error.toString() == 'Такой Эл. почта уже существует.') { - emit(state.copyWith(isEmailValid: false, emailDisplayError: 'Такая Эл. почта уже существует.')); - didApiComplainOnValidation = true; - } - } - } - if (phoneErrors != null && phoneErrors.isNotEmpty) { - for (var error in phoneErrors) { - allErrors.add(error.toString()); - if (error.toString() == 'Такой phone уже существует.') { - emit(state.copyWith(isPhoneValid: false, phoneDisplayError: 'Такой номер телефона уже существует.')); - didApiComplainOnValidation = true; - } - } - } - if (dateOfBirth != null && dateOfBirth.isNotEmpty) { - for (var error in dateOfBirth) { - allErrors.add(error.toString()); - if (error.toString() == 'Некорректный формат даты рождения') { - emit(state.copyWith(isBirthdayValid: false, dateOfBirthDisplayError: 'Некорректный формат даты рождения')); - didApiComplainOnValidation = true; - } - } - } - - // Сохраняем все ошибки для отображения в диалоге - if (allErrors.isNotEmpty) { - errorMessage = allErrors.join('\n'); - emit(state.copyWith(registrationError: errorMessage)); - } else { - // Если нет конкретных ошибок, берем общее сообщение - final message = e.response?.data['message'] as String?; - errorMessage = message ?? 'Ошибка при регистрации'; - emit(state.copyWith(registrationError: errorMessage)); - } - } catch (e) { - log(e.toString()); - errorMessage = e.toString(); - emit(state.copyWith(registrationError: errorMessage)); - } - return (false, didApiComplainOnValidation); - } - - Future _generatePassword() async { - final generator = GPassword(); - final password = generator.generate(passwordLength: 10); - emit(state.copyWith(password: password)); - return password; - } -} +import 'dart:developer'; + +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/core/utils/date_time_utils.dart'; +import 'package:eatvio_app/core/utils/validation_utils.dart'; +import 'package:eatvio_app/features/registration_v2/cubit/registration_state.dart'; +import 'package:eatvio_app/features/registration_v2/models/registration_model.dart'; +import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; +import 'package:gpassword/gpassword.dart'; + +class RegistrationCubitV2 extends Cubit { + final RegistrationRepositoryV2 registrationRepository; + RegistrationCubitV2({required this.registrationRepository}) : super(const RegistrationStateV2()); + + void setFirstName(String firstName) { + emit(state.copyWith(firstName: firstName)); + validateFirstName(); + } + + void setLastName(String lastName) { + emit(state.copyWith(lastName: lastName)); + validateLastName(); + } + + void setEmail(String email) { + emit(state.copyWith(email: email)); + validateEmail(); + } + + void setPhone(String phone) { + emit(state.copyWith(phone: phone)); + validatePhone(); + } + + void setBirthday(DateTime birthday) { + emit(state.copyWith(birthday: birthday)); + validateBirthday(); + } + + void setGender(String gender) { + emit(state.copyWith(gender: gender)); + validateGender(); + } + + void setWeight(int weight) { + emit(state.copyWith(weight: weight)); + validateWeight(); + } + + void setHeight(int height) { + emit(state.copyWith(height: height)); + validateHeight(); + } + + void setActivityCoefficient(double activityCoefficient) { + emit(state.copyWith(activityCoefficient: activityCoefficient)); + validateActivityCoefficient(); + } + + void setPassword(String password) { + emit(state.copyWith(password: password)); + } + + bool validateFirstName() { + if (state.firstName.isEmpty) { + emit(state.copyWith(isFirstNameValid: false)); + return false; + } + emit(state.copyWith(isFirstNameValid: true)); + return true; + } + + bool validateLastName() { + if (state.lastName.isEmpty) { + emit(state.copyWith(isLastNameValid: false)); + return false; + } + emit(state.copyWith(isLastNameValid: true)); + return true; + } + + bool validateEmail() { + if (state.email.isEmpty) { + emit(state.copyWith(isEmailValid: false)); + return false; + } + emit(state.copyWith(isEmailValid: true)); + return true; + } + + bool validatePhone() { + if (state.phone.isEmpty) { + emit(state.copyWith(isPhoneValid: false)); + return false; + } + emit(state.copyWith(isPhoneValid: true)); + return true; + } + + bool validateGender() { + if (state.gender.isEmpty) { + emit(state.copyWith(isGenderValid: false)); + return false; + } + emit(state.copyWith(isGenderValid: true)); + return true; + } + + bool validateWeight() { + if (state.weight == 0) { + emit(state.copyWith(isWeightValid: false)); + return false; + } + emit(state.copyWith(isWeightValid: true)); + return true; + } + + bool validateHeight() { + if (state.height == 0) { + emit(state.copyWith(isHeightValid: false)); + return false; + } + emit(state.copyWith(isHeightValid: true)); + return true; + } + + bool validateActivityCoefficient() { + if (state.activityCoefficient == 0) { + emit(state.copyWith(isActivityCoefficientValid: false)); + return false; + } + emit(state.copyWith(isActivityCoefficientValid: true)); + return true; + } + + bool validateBirthday() { + if (state.birthday == null) { + emit(state.copyWith(isBirthdayValid: false)); + return false; + } + emit(state.copyWith(isBirthdayValid: true)); + return true; + } + + void nextStep() { + emit(state.copyWith(step: 1)); + } + + void prevStep() { + emit(state.copyWith(step: 0)); + } + + bool validateAllFields() { + if (!validateFirstName() || + !validateLastName() || + !validateEmail() || + !validatePhone() || + !validateGender() || + !validateWeight() || + !validateHeight() || + !validateActivityCoefficient() || + !validateBirthday()) { + return false; + } + return true; + } + + Future<(bool, bool)> register() async { + if (!validateAllFields()) { + return (false, true); + } + final birthday = state.birthday?.format('yyyy-MM-dd'); + if (birthday == null) { + validateBirthday(); + return (false, true); + } + final password = await _generatePassword(); + final registrationModel = RegistrationModel( + firstName: state.firstName, + lastName: state.lastName, + email: state.email, + phone: ValidationUtils.normalizePhone(state.phone), + gender: state.gender, + password: password, + confirmPassword: password, + weight: state.weight, + height: state.height, + activityCoefficient: state.activityCoefficient, + birthday: birthday, + ); + bool didApiComplainOnValidation = false; + String errorMessage = ''; + try { + final token = await registrationRepository.register(registrationModel: registrationModel); + if (token != null) { + TokenStorage.saveTokenInStorage(token); + emit(state.copyWith(registrationError: null)); + return (true, false); + } + } on DioException catch (e, st) { + await reportFrom(e, st, this); + final errors = e.response?.data['errors'] as Map?; + final emailErrors = errors?['email'] as List?; + final phoneErrors = errors?['phone'] as List?; + final dateOfBirth = errors?['date_of_birth'] as List?; + + // Собираем все ошибки для отображения в диалоге + List allErrors = []; + + if (emailErrors != null && emailErrors.isNotEmpty) { + for (var error in emailErrors) { + allErrors.add(error.toString()); + if (error.toString() == 'Некорректный формат email') { + emit(state.copyWith(isEmailValid: false, emailDisplayError: 'Некорректный формат email')); + didApiComplainOnValidation = true; + } + if (error.toString() == 'Такой Эл. почта уже существует.') { + emit(state.copyWith(isEmailValid: false, emailDisplayError: 'Такая Эл. почта уже существует.')); + didApiComplainOnValidation = true; + } + } + } + if (phoneErrors != null && phoneErrors.isNotEmpty) { + for (var error in phoneErrors) { + allErrors.add(error.toString()); + if (error.toString() == 'Такой phone уже существует.') { + emit(state.copyWith(isPhoneValid: false, phoneDisplayError: 'Такой номер телефона уже существует.')); + didApiComplainOnValidation = true; + } + } + } + if (dateOfBirth != null && dateOfBirth.isNotEmpty) { + for (var error in dateOfBirth) { + allErrors.add(error.toString()); + if (error.toString() == 'Некорректный формат даты рождения') { + emit(state.copyWith(isBirthdayValid: false, dateOfBirthDisplayError: 'Некорректный формат даты рождения')); + didApiComplainOnValidation = true; + } + } + } + + // Сохраняем все ошибки для отображения в диалоге + if (allErrors.isNotEmpty) { + errorMessage = allErrors.join('\n'); + emit(state.copyWith(registrationError: errorMessage)); + } else { + // Если нет конкретных ошибок, берем общее сообщение + final message = e.response?.data['message'] as String?; + errorMessage = message ?? 'Ошибка при регистрации'; + emit(state.copyWith(registrationError: errorMessage)); + } + } catch (e, st) { + await reportFrom(e, st, this); + log(e.toString()); + errorMessage = e.toString(); + emit(state.copyWith(registrationError: errorMessage)); + } + return (false, didApiComplainOnValidation); + } + + Future _generatePassword() async { + final generator = GPassword(); + final password = generator.generate(passwordLength: 10); + emit(state.copyWith(password: password)); + return password; + } +} diff --git a/lib/features/registration_v2/repositories/registration_repository_v2.dart b/lib/features/registration_v2/repositories/registration_repository_v2.dart index 7640ec30..38c173e7 100644 --- a/lib/features/registration_v2/repositories/registration_repository_v2.dart +++ b/lib/features/registration_v2/repositories/registration_repository_v2.dart @@ -1,18 +1,13 @@ import 'package:eatvio_app/features/registration_v2/models/registration_model.dart'; import 'package:eatvio_app/features/registration_v2/services/registration_service_v2.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class RegistrationRepositoryV2 { final RegistrationServiceV2 registrationService; RegistrationRepositoryV2({required this.registrationService}); Future register({required RegistrationModel registrationModel}) async { - try { - final response = await registrationService.register(model: registrationModel.toJson()); - return response['token']; - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + final response = await registrationService.register(model: registrationModel.toJson()); + return response['token']; + } } diff --git a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart index ed123203..f468a1ce 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart @@ -10,6 +10,7 @@ import 'package:eatvio_app/features/settings/models/profile_settings_model.dart' import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart'; import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class SettingsCubit extends Cubit { final ProfileRepository profileRepository; @@ -36,20 +37,23 @@ class SettingsCubit extends Cubit { } else { consultantInfo = null; } - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); consultantInfo = null; } try { hasActiveConsultation = await _checkActiveConsultation(); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); hasActiveConsultation = false; } try { final oauthResponse = await settingsRepository.getOAuthAccounts(); oauthAccounts = oauthResponse.data; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); oauthAccounts = null; } @@ -64,7 +68,8 @@ class SettingsCubit extends Cubit { oauthAccounts: oauthAccounts, ), ); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); emit(SettingsState.error(errorMessage: e.toString())); } } @@ -72,8 +77,8 @@ class SettingsCubit extends Cubit { Future updateProfileDetails({required ProfileDetailsModel profileDetailsModel}) async { try { await settingsRepository.updateProfileDetails(profileDetailsModel: profileDetailsModel); - } catch (e) { - rethrow; + } catch (e, st) { + await reportFrom(e, st, this); } finally { await init(); } @@ -82,8 +87,8 @@ class SettingsCubit extends Cubit { Future updateProfileSettings({required ProfileSettingsModel profileSettingsModel}) async { try { await settingsRepository.updateProfileSettings(profileSettingsModel: profileSettingsModel); - } catch (e) { - rethrow; + } catch (e, st) { + await reportFrom(e, st, this); } finally { await init(); } @@ -93,7 +98,8 @@ class SettingsCubit extends Cubit { try { await profileRepository.deleteAccount(); return true; - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); return false; } } @@ -113,8 +119,8 @@ class SettingsCubit extends Cubit { ); await settingsRepository.linkOAuthAccount(request: request); await init(); - } catch (e) { - rethrow; + } catch (e, st) { + await reportFrom(e, st, this); } } @@ -122,8 +128,8 @@ class SettingsCubit extends Cubit { try { await settingsRepository.unlinkOAuthAccount(provider: provider); await init(); - } catch (e) { - rethrow; + } catch (e, st) { + await reportFrom(e, st, this); } } @@ -135,7 +141,8 @@ class SettingsCubit extends Cubit { return userPurchases.any((purchase) => activeStates.contains(purchase.chosenPlanState) ); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); return false; } } diff --git a/lib/features/settings/repositories/service_repository.dart b/lib/features/settings/repositories/service_repository.dart index aa86ec57..efbd117a 100644 --- a/lib/features/settings/repositories/service_repository.dart +++ b/lib/features/settings/repositories/service_repository.dart @@ -3,7 +3,6 @@ import 'package:eatvio_app/features/settings/models/oauth_account_model.dart'; import 'package:eatvio_app/features/settings/models/oauth_attach_request.dart'; import 'package:eatvio_app/features/settings/models/profile_settings_model.dart'; import 'package:eatvio_app/features/settings/services/settings_service.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; class SettingsRepository { final SettingsService settingsService; @@ -11,56 +10,32 @@ class SettingsRepository { const SettingsRepository({required this.settingsService}); Future getProfileSettings() async { - try { - return await settingsService.getProfileSettings(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await settingsService.getProfileSettings(); + } Future updateProfileSettings({required ProfileSettingsModel profileSettingsModel}) async { - try { - return await settingsService.updateProfileSettings(data: profileSettingsModel.toJson()); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await settingsService.updateProfileSettings(data: profileSettingsModel.toJson()); + } Future updateProfileDetails({required ProfileDetailsModel profileDetailsModel}) async { - try { - return await settingsService.updateProfileDetails(data: profileDetailsModel.toJson()); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await settingsService.updateProfileDetails(data: profileDetailsModel.toJson()); + } Future getOAuthAccounts() async { - try { - return await settingsService.getOAuthAccounts(); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await settingsService.getOAuthAccounts(); + } Future linkOAuthAccount({required OAuthAttachRequest request}) async { - try { - return await settingsService.linkOAuthAccount(request); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await settingsService.linkOAuthAccount(request); + } Future unlinkOAuthAccount({required String provider}) async { - try { - return await settingsService.unlinkOAuthAccount(provider); - } catch (e, s) { - await Sentry.captureException(e, stackTrace: s); - rethrow; - } + return await settingsService.unlinkOAuthAccount(provider); + } } diff --git a/lib/features/user_details/presentation/cubit/user_details_cubit.dart b/lib/features/user_details/presentation/cubit/user_details_cubit.dart index 0ce0bd93..6735043b 100644 --- a/lib/features/user_details/presentation/cubit/user_details_cubit.dart +++ b/lib/features/user_details/presentation/cubit/user_details_cubit.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:eatvio_app/core/error_reporting/report_from.dart'; class UserDetailsCubit extends Cubit { final ProfileRepository profileRepository; @@ -14,7 +15,8 @@ class UserDetailsCubit extends Cubit { final profileDetails = await profileRepository.getProfileDetailsBySlug(slug); emit(state.copyWith(profileDetails: profileDetails, isLoading: false)); getChartData(slug: slug); - } catch (e) { + } catch (e, st) { + await reportFrom(e, st, this); log(e.toString()); emit(state.copyWith(error: e.toString(), isLoading: false)); } @@ -26,6 +28,7 @@ class UserDetailsCubit extends Cubit { final circumferencePoints = await profileRepository.getCircumferences(period: period, slug: slug); emit(state.copyWith(weightPoints: weightPoints, circumferencePoints: circumferencePoints)); } catch (e, stackTrace) { + await reportFrom(e, stackTrace, this); log(e.toString(), stackTrace: stackTrace); emit(state.copyWith(error: e.toString(), isLoading: false)); } diff --git a/test/core/cubit/marketing/marketing_cubit_test.dart b/test/core/cubit/marketing/marketing_cubit_test.dart index 9fb33df0..77344fa9 100644 --- a/test/core/cubit/marketing/marketing_cubit_test.dart +++ b/test/core/cubit/marketing/marketing_cubit_test.dart @@ -103,11 +103,12 @@ void main() { .called(1); }); - test('rethrows exception when error occurs', () async { + test('returns false when prefs throws (error is reported)', () async { when(mockPrefs.getInt(PrefsKeys.registrationDate)) .thenThrow(Exception('Test error')); - expect(() => cubit.checkRegistrationDate(), throwsException); + final result = await cubit.checkRegistrationDate(); + expect(result, false); }); }); } diff --git a/test/core/error_reporting/error_report_policy_test.dart b/test/core/error_reporting/error_report_policy_test.dart new file mode 100644 index 00000000..52bbc6e5 --- /dev/null +++ b/test/core/error_reporting/error_report_policy_test.dart @@ -0,0 +1,114 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/error_reporting/error_report_policy.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +void main() { + group('ErrorReportPolicy.shouldReport', () { + test('returns false for null', () { + expect(ErrorReportPolicy.shouldReport(null), false); + }); + + test('returns false for String', () { + expect(ErrorReportPolicy.shouldReport('user message'), false); + }); + + test('returns false for SocketException', () { + expect(ErrorReportPolicy.shouldReport(const SocketException('offline')), false); + }); + + test('returns false for cancelled Dio request', () { + final e = DioException( + requestOptions: RequestOptions(path: '/x'), + type: DioExceptionType.cancel, + ); + expect(ErrorReportPolicy.shouldReport(e), false); + }); + + test('returns false for Dio timeouts', () { + for (final type in [ + DioExceptionType.connectionTimeout, + DioExceptionType.sendTimeout, + DioExceptionType.receiveTimeout, + ]) { + final e = DioException( + requestOptions: RequestOptions(path: '/x'), + type: type, + ); + expect(ErrorReportPolicy.shouldReport(e), false, reason: '$type'); + } + }); + + test('returns false for HTTP 4xx with response', () { + final e = DioException( + requestOptions: RequestOptions(path: '/x'), + response: Response(statusCode: 422, requestOptions: RequestOptions(path: '/x')), + type: DioExceptionType.badResponse, + ); + expect(ErrorReportPolicy.shouldReport(e), false); + }); + + test('returns false for HTTP 5xx (handled by sentry_dio failed-request)', () { + final e = DioException( + requestOptions: RequestOptions(path: '/x'), + response: Response(statusCode: 503, requestOptions: RequestOptions(path: '/x')), + type: DioExceptionType.badResponse, + ); + expect(ErrorReportPolicy.shouldReport(e), false); + }); + + test('returns false for Dio connectionError / offline-style', () { + final e = DioException( + requestOptions: RequestOptions(path: '/x'), + type: DioExceptionType.connectionError, + error: const SocketException('offline'), + ); + expect(ErrorReportPolicy.shouldReport(e), false); + }); + + test('returns true for arbitrary exceptions', () { + expect(ErrorReportPolicy.shouldReport(StateError('x')), true); + }); + }); + + group('ErrorReportPolicy.allowSentryEvent', () { + final hint = Hint(); + + test('allows 5xx Dio so sentry_dio events are not dropped in beforeSend', () { + final e = DioException( + requestOptions: RequestOptions(path: '/x'), + response: Response(statusCode: 503, requestOptions: RequestOptions(path: '/x')), + type: DioExceptionType.badResponse, + ); + final event = SentryEvent(throwable: e); + expect(ErrorReportPolicy.allowSentryEvent(event, hint), true); + }); + + test('drops 4xx Dio from SDK', () { + final e = DioException( + requestOptions: RequestOptions(path: '/x'), + response: Response(statusCode: 422, requestOptions: RequestOptions(path: '/x')), + type: DioExceptionType.badResponse, + ); + final event = SentryEvent(throwable: e); + expect(ErrorReportPolicy.allowSentryEvent(event, hint), false); + }); + + test('drops transient Dio from SDK (connectionError)', () { + final e = DioException( + requestOptions: RequestOptions(path: '/x'), + type: DioExceptionType.connectionError, + error: const SocketException('reset'), + ); + final event = SentryEvent(throwable: e); + expect(ErrorReportPolicy.allowSentryEvent(event, hint), false); + }); + + test('drops SocketException from SDK when not wrapped in Dio', () { + final event = SentryEvent(throwable: const SocketException('x')); + expect(ErrorReportPolicy.allowSentryEvent(event, hint), false); + }); + }); +} diff --git a/test/core/firebase_api_test.dart b/test/core/firebase_api_test.dart index 00423672..e4948f6e 100644 --- a/test/core/firebase_api_test.dart +++ b/test/core/firebase_api_test.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/error_reporting/no_op_error_reporter.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -7,7 +8,7 @@ void main() { // В тестах Firebase не может быть инициализирован // Проверяем, что попытка создания приводит к ожидаемой ошибке expect( - () => FirebaseApi(), + () => FirebaseApi(errorReporter: const NoOpErrorReporter()), throwsA(isA()), ); }); @@ -16,7 +17,7 @@ void main() { // Метод getToken требует инициализированный Firebase // В тестах мы проверяем, что он выбрасывает ожидаемую ошибку try { - final api = FirebaseApi(); + final api = FirebaseApi(errorReporter: const NoOpErrorReporter()); expect(() => api.getToken(), throwsA(isA())); } catch (e) { expect(e.toString(), contains('No Firebase App')); @@ -26,7 +27,7 @@ void main() { test('registerAnonymousPushToken возвращает Future', () { // Метод registerAnonymousPushToken требует инициализированный Firebase try { - final api = FirebaseApi(); + final api = FirebaseApi(errorReporter: const NoOpErrorReporter()); expect(() => api.registerAnonymousPushToken(), throwsA(isA())); } catch (e) { expect(e.toString(), contains('No Firebase App')); @@ -36,7 +37,7 @@ void main() { test('registerPushToken возвращает Future', () { // Метод registerPushToken требует инициализированный Firebase try { - final api = FirebaseApi(); + final api = FirebaseApi(errorReporter: const NoOpErrorReporter()); expect(() => api.registerPushToken(), throwsA(isA())); } catch (e) { expect(e.toString(), contains('No Firebase App')); diff --git a/test/core/websockets/ws_manager_test.dart b/test/core/websockets/ws_manager_test.dart index 5c17c758..d0a584bb 100644 --- a/test/core/websockets/ws_manager_test.dart +++ b/test/core/websockets/ws_manager_test.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/error_reporting/no_op_error_reporter.dart'; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/locator_service.dart'; @@ -29,7 +30,7 @@ void main() { test('initialize returns early when client is not null', () async { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); await manager.initialize(); @@ -38,35 +39,35 @@ void main() { test('newMessagesStream returns null when channel is null', () { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); expect(manager.newMessagesStream, isNull); }); test('newNotificationsStream returns null when channel is null', () { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); expect(manager.newNotificationsStream, isNull); }); test('directChatStream returns null when channel is null', () { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); expect(manager.directChatStream, isNull); }); test('clientTypingStream returns null when channel is null', () { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); expect(manager.clientTypingStream, isNull); }); test('subscribeDirectChatChannel does nothing when channel is null', () { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); // Не должно быть исключений manager.subscribeDirectChatChannel(); @@ -74,7 +75,7 @@ void main() { test('unsubscribeDirectChatChannel does nothing when channel is null', () { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); // Не должно быть исключений manager.unsubscribeDirectChatChannel(); @@ -82,7 +83,7 @@ void main() { test('clear sets client and userId to null', () async { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); manager.userId = 1; @@ -94,7 +95,7 @@ void main() { test('initPrivateDirectChatChannel - создает канал для прямого чата', () async { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); const chatId = 'chat-123'; const token = 'test-token'; @@ -109,7 +110,7 @@ void main() { test('initialize - обрабатывает ошибку при отсутствии токена', () async { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); // Убеждаемся, что токена нет await TokenStorage.removeTokenInStorage(); @@ -122,7 +123,7 @@ void main() { test('initialize - обрабатывает ошибку при ошибке getWebsocket', () async { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); const token = 'test-token'; await TokenStorage.saveTokenInStorage(token); @@ -136,7 +137,7 @@ void main() { test('initialize - устанавливает userId из WebsocketInfoModel', () async { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); const token = 'test-token'; const userId = 42; @@ -158,7 +159,7 @@ void main() { test('clear - отписывается от всех каналов', () async { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); manager.userId = 1; @@ -171,7 +172,7 @@ void main() { test('initialize - не инициализирует повторно если client уже существует', () async { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); const token = 'test-token'; await TokenStorage.saveTokenInStorage(token); @@ -193,7 +194,7 @@ void main() { test('subscribeDirectChatChannel - подписывается на канал когда он существует', () { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); // Когда канал null, метод не должен выбрасывать исключение manager.subscribeDirectChatChannel(); @@ -203,7 +204,7 @@ void main() { test('unsubscribeDirectChatChannel - отписывается от канала когда он существует', () { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); // Когда канал null, метод не должен выбрасывать исключение manager.unsubscribeDirectChatChannel(); @@ -213,35 +214,35 @@ void main() { test('newMessagesStream - возвращает null когда канал не инициализирован', () { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); expect(manager.newMessagesStream, isNull); }); test('newNotificationsStream - возвращает null когда канал не инициализирован', () { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); expect(manager.newNotificationsStream, isNull); }); test('directChatStream - возвращает null когда канал не инициализирован', () { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); expect(manager.directChatStream, isNull); }); test('clientTypingStream - возвращает null когда канал не инициализирован', () { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); expect(manager.clientTypingStream, isNull); }); test('initialize - обрабатывает случай когда TokenStorage.haveTokenInStorage возвращает false', () async { final wsService = MockWsService(); - final manager = WsManager(wsService: wsService); + final manager = WsManager(wsService: wsService, errorReporter: const NoOpErrorReporter()); // Убеждаемся, что токена нет await TokenStorage.removeTokenInStorage(); diff --git a/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart b/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart index 8bf682e2..94401422 100644 --- a/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart +++ b/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart @@ -379,25 +379,22 @@ void main() { ), ).thenThrow(Exception('Ошибка подтверждения')); - expect( - () => oauthCubit!.confirmOAuthVerification( - provider: 'google', - accessToken: 'access_token', - refreshToken: 'refresh_token', - clientId: 'client_id', - id: 'id', - email: 'test@example.com', - phone: null, - code: '123456', - verificationType: 'email', - firstName: 'Test', - lastName: 'User', - dateOfBirth: '2000-01-01', - gender: 'male', - weight: 70, - height: 175, - ), - throwsException, + await oauthCubit!.confirmOAuthVerification( + provider: 'google', + accessToken: 'access_token', + refreshToken: 'refresh_token', + clientId: 'client_id', + id: 'id', + email: 'test@example.com', + phone: null, + code: '123456', + verificationType: 'email', + firstName: 'Test', + lastName: 'User', + dateOfBirth: '2000-01-01', + gender: 'male', + weight: 70, + height: 175, ); await Future.delayed(const Duration(milliseconds: 100)); diff --git a/test/features/auth/repositories/reset_password_repository_test.dart b/test/features/auth/repositories/reset_password_repository_test.dart index f4c702da..05593cb7 100644 --- a/test/features/auth/repositories/reset_password_repository_test.dart +++ b/test/features/auth/repositories/reset_password_repository_test.dart @@ -21,7 +21,7 @@ void main() { mockResetPasswordService = MockResetPasswordService(); resetPasswordRepository = ResetPasswordRepository( resetPasswordService: mockResetPasswordService, - ); + ); }); test('sendEmailCodeForPasswordChange - успешная отправка кода (200)', () async { diff --git a/test/features/barcode/repositories/barcode_repository_test.dart b/test/features/barcode/repositories/barcode_repository_test.dart index 23509eab..feaabb43 100644 --- a/test/features/barcode/repositories/barcode_repository_test.dart +++ b/test/features/barcode/repositories/barcode_repository_test.dart @@ -24,74 +24,54 @@ void main() { test('getProductByBarcode - возвращает успешный результат (200)', () async { const barcode = '1234567890123'; final productData = {'GTIN': barcode, 'Описание (ru)': 'Test Product'}; - final response = Response( + final response = Response>( data: productData, statusCode: 200, requestOptions: RequestOptions(path: '/api/gsone/barcode/$barcode'), ); - when(mockDio.get('/api/gsone/barcode/$barcode')).thenAnswer((_) async => response); + when(mockDio.get>(any, options: anyNamed('options'))).thenAnswer((_) async => response); final result = await repository.getProductByBarcode(barcode); expect(result.type, BarcodeResultType.success); expect(result.product, isNotNull); expect(result.product!.description, 'Test Product'); - verify(mockDio.get('/api/gsone/barcode/$barcode')).called(1); + verify(mockDio.get>(any, options: anyNamed('options'))).called(1); }); test('getProductByBarcode - возвращает статус обработки (202)', () async { const barcode = '1234567890123'; final taskData = {'task_id': 'task-123', 'status': 'processing'}; - final response = Response( + final response = Response>( data: taskData, statusCode: 202, requestOptions: RequestOptions(path: '/api/gsone/barcode/$barcode'), ); - when(mockDio.get('/api/gsone/barcode/$barcode')).thenAnswer((_) async => response); + when(mockDio.get>(any, options: anyNamed('options'))).thenAnswer((_) async => response); final result = await repository.getProductByBarcode(barcode); expect(result.type, BarcodeResultType.processing); expect(result.taskId, equals('task-123')); - verify(mockDio.get('/api/gsone/barcode/$barcode')).called(1); + verify(mockDio.get>(any, options: anyNamed('options'))).called(1); }); - test('getProductByBarcode - обрабатывает DioException с 202 статусом', () async { + test('getProductByBarcode - пробрасывает DioException при нестандартном ответе', () async { const barcode = '1234567890123'; - final taskData = {'task_id': 'task-456', 'status': 'processing'}; - final dioException = DioException( + final response = Response>( + data: {'error': 'Not found'}, + statusCode: 404, requestOptions: RequestOptions(path: '/api/gsone/barcode/$barcode'), - response: Response( - data: taskData, - statusCode: 202, - requestOptions: RequestOptions(path: '/api/gsone/barcode/$barcode'), - ), ); - when(mockDio.get('/api/gsone/barcode/$barcode')).thenThrow(dioException); + when(mockDio.get>(any, options: anyNamed('options'))).thenAnswer((_) async => response); - final result = await repository.getProductByBarcode(barcode); - - expect(result.type, BarcodeResultType.processing); - expect(result.taskId, equals('task-456')); - }); - - test('getProductByBarcode - пробрасывает исключение при другой ошибке', () async { - const barcode = '1234567890123'; - final dioException = DioException( - requestOptions: RequestOptions(path: '/api/gsone/barcode/$barcode'), - response: Response( - data: {'error': 'Not found'}, - statusCode: 404, - requestOptions: RequestOptions(path: '/api/gsone/barcode/$barcode'), - ), + await expectLater( + repository.getProductByBarcode(barcode), + throwsA(isA()), ); - - when(mockDio.get('/api/gsone/barcode/$barcode')).thenThrow(dioException); - - expect(() => repository.getProductByBarcode(barcode), throwsException); }); test('getTaskStatus - возвращает completed статус', () async { diff --git a/test/features/chat/repositories/chat_repository_test.dart b/test/features/chat/repositories/chat_repository_test.dart index ea0bda09..53f8a9bd 100644 --- a/test/features/chat/repositories/chat_repository_test.dart +++ b/test/features/chat/repositories/chat_repository_test.dart @@ -357,15 +357,16 @@ void main() { verify(mockChatService.getChatByUserSlug(slug: slug)).called(1); }); - test('getChatByUserSlug - возвращает error при ошибке', () async { + test('getChatByUserSlug - пробрасывает исключение при ошибке сети', () async { const slug = 'user-slug'; when(mockChatService.getChatByUserSlug(slug: slug)) .thenThrow(Exception('Network error')); - final result = await chatRepository.getChatByUserSlug(slug); - - expect(result, 'error'); + await expectLater( + chatRepository.getChatByUserSlug(slug), + throwsA(isA()), + ); }); }); } diff --git a/test/features/glossary/repositories/glossary_repository_test.dart b/test/features/glossary/repositories/glossary_repository_test.dart index a852e69a..588d2c14 100644 --- a/test/features/glossary/repositories/glossary_repository_test.dart +++ b/test/features/glossary/repositories/glossary_repository_test.dart @@ -1,75 +1,75 @@ -import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; -import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; -import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockGlossaryService extends Mock implements GlossaryService {} - -void main() { - group('GlossaryRepository', () { - late GlossaryRepository repository; - late MockGlossaryService mockService; - - setUp(() { - mockService = MockGlossaryService(); - repository = GlossaryRepository(glossaryService: mockService); - }); - - test('getGlossaryListByQuery - успешный запрос', () async { - const query = 'test'; - final mockGlossaries = []; - final mockPagination = GlossaryPaginationModel(glossariesList: mockGlossaries); - - when(() => mockService.getGlossaryByQuery(query: query)) - .thenAnswer((_) async => mockPagination); - - final result = await repository.getGlossaryListByQuery(query); - - expect(result, mockGlossaries); - verify(() => mockService.getGlossaryByQuery(query: query)).called(1); - }); - - test('getGlossaryListByQuery - пробрасывает ошибку', () async { - const query = 'test'; - - when(() => mockService.getGlossaryByQuery(query: query)) - .thenThrow(Exception('Error')); - - expect(() => repository.getGlossaryListByQuery(query), throwsException); - }); - }); -} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; +import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; +import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockGlossaryService extends Mock implements GlossaryService {} + +void main() { + group('GlossaryRepository', () { + late GlossaryRepository repository; + late MockGlossaryService mockService; + + setUp(() { + mockService = MockGlossaryService(); + repository = GlossaryRepository(glossaryService: mockService); + }); + + test('getGlossaryListByQuery - успешный запрос', () async { + const query = 'test'; + final mockGlossaries = []; + final mockPagination = GlossaryPaginationModel(glossariesList: mockGlossaries); + + when(() => mockService.getGlossaryByQuery(query: query)) + .thenAnswer((_) async => mockPagination); + + final result = await repository.getGlossaryListByQuery(query); + + expect(result, mockGlossaries); + verify(() => mockService.getGlossaryByQuery(query: query)).called(1); + }); + + test('getGlossaryListByQuery - пробрасывает ошибку', () async { + const query = 'test'; + + when(() => mockService.getGlossaryByQuery(query: query)) + .thenThrow(Exception('Error')); + + expect(() => repository.getGlossaryListByQuery(query), throwsException); + }); + }); +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/features/menu/repositories/blocked_users_repository_test.dart b/test/features/menu/repositories/blocked_users_repository_test.dart index 6fba2382..b844ca43 100644 --- a/test/features/menu/repositories/blocked_users_repository_test.dart +++ b/test/features/menu/repositories/blocked_users_repository_test.dart @@ -17,7 +17,7 @@ void main() { mockBlockedUserService = MockBlockedUserService(); blockedUsersRepository = BlockedUsersRepository( blockedUserService: mockBlockedUserService, - ); + ); }); test('getBlockedUsers - возвращает список заблокированных пользователей', () async { diff --git a/test/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository_test.dart b/test/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository_test.dart index 914732d4..76b292aa 100644 --- a/test/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository_test.dart +++ b/test/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository_test.dart @@ -18,7 +18,7 @@ void main() { mockService = MockPersonalDietSuggestionService(); repository = PersonalDietSuggestionRepository( personalDietSuggestionService: mockService, - ); + ); }); test('getConsultationWithPersonalDiets - успешная загрузка консультаций', () async { diff --git a/test/features/plans/presentation/cubit/plans_cubit_test.dart b/test/features/plans/presentation/cubit/plans_cubit_test.dart index c1c46b39..ad63a960 100644 --- a/test/features/plans/presentation/cubit/plans_cubit_test.dart +++ b/test/features/plans/presentation/cubit/plans_cubit_test.dart @@ -1,3 +1,4 @@ +import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/models/pagination_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; @@ -59,9 +60,12 @@ void main() { }); tearDown(() async { + EasyDebounce.cancel('loadPlans'); + EasyDebounce.cancel('like_plan'); + EasyDebounce.cancel('add_or_remove_plan_from_favorite'); if (plansCubit != null && !plansCubit!.isClosed) { await Future.delayed(const Duration(milliseconds: 100)); - plansCubit!.close(); + await plansCubit!.close(); plansCubit = null; } }); diff --git a/test/features/profile/presentation/bloc/profile_cubit/profile_cubit_test.dart b/test/features/profile/presentation/bloc/profile_cubit/profile_cubit_test.dart index c76ace2e..9b109014 100644 --- a/test/features/profile/presentation/bloc/profile_cubit/profile_cubit_test.dart +++ b/test/features/profile/presentation/bloc/profile_cubit/profile_cubit_test.dart @@ -143,7 +143,7 @@ void main() { when(mockProfileRepository.getProfileBySlug('test-user')).thenThrow(Exception('Ошибка загрузки')); - expect(() => profileCubit!.getProfileBySlug('test-user'), throwsException); + await profileCubit!.getProfileBySlug('test-user'); final state = profileCubit!.state; expect(state, isA()); diff --git a/test/features/profile/repositories/profile_repository_test.dart b/test/features/profile/repositories/profile_repository_test.dart index 8c29c465..bd693991 100644 --- a/test/features/profile/repositories/profile_repository_test.dart +++ b/test/features/profile/repositories/profile_repository_test.dart @@ -280,9 +280,8 @@ void main() { when(mockService.tryVerifyEmailByCode(code: '{"code": "$code"}')).thenAnswer((_) async => Future.value()); - final result = await repository.tryVerifyEmailByCode(code); + await repository.tryVerifyEmailByCode(code); - expect(result, isTrue); verify(mockService.tryVerifyEmailByCode(code: '{"code": "$code"}')).called(1); }); @@ -291,9 +290,10 @@ void main() { when(mockService.tryVerifyEmailByCode(code: '{"code": "$code"}')).thenThrow(Exception('Invalid code')); - final result = await repository.tryVerifyEmailByCode(code); - - expect(result, isFalse); + await expectLater( + repository.tryVerifyEmailByCode(code), + throwsA(isA()), + ); }); test('deleteAccount - удаляет аккаунт', () async { diff --git a/test/features/recipes/presentation/cubit/my_recipes_cubit_test.dart b/test/features/recipes/presentation/cubit/my_recipes_cubit_test.dart index 4afde5ec..63216626 100644 --- a/test/features/recipes/presentation/cubit/my_recipes_cubit_test.dart +++ b/test/features/recipes/presentation/cubit/my_recipes_cubit_test.dart @@ -228,7 +228,8 @@ void main() { final result = await myRecipesCubit!.getRecipeById('test-recipe'); expect(result, isA()); - expect(result.id, 1); + expect(result, isNotNull); + expect(result!.id, 1); expect(result.slug, 'test-recipe'); verify(mockAboutRecipeRepository.getRecipe('test-recipe')).called(1); }); diff --git a/test/features/registration/presentation/bloc/registration_cubit/registration_cubit_test.dart b/test/features/registration/presentation/bloc/registration_cubit/registration_cubit_test.dart index 58aa0ffc..11bfe5d6 100644 --- a/test/features/registration/presentation/bloc/registration_cubit/registration_cubit_test.dart +++ b/test/features/registration/presentation/bloc/registration_cubit/registration_cubit_test.dart @@ -139,13 +139,14 @@ void main() { expect(cubit.state.sheet.responseBody, '{"locations": ["London, UK"]}'); }); - test('getLocationsByQuery rethrows exception on error', () async { + test('getLocationsByQuery returns empty list on error', () async { const query = 'Invalid'; final exception = Exception('Network error'); when(() => mockRepository.getLocationsListByQuery(query)).thenThrow(exception); - expect(() => cubit.getLocationsByQuery(query), throwsException); + final result = await cubit.getLocationsByQuery(query); + expect(result, isEmpty); }); test('getLocationsByCoordinates returns list of locations', () async { @@ -176,14 +177,15 @@ void main() { expect(cubit.state.sheet.responseBody, '{"locations": ["London, UK"]}'); }); - test('getLocationsByCoordinates rethrows exception on error', () async { + test('getLocationsByCoordinates returns empty list on error', () async { const latitude = 'invalid'; const longitude = 'invalid'; final exception = Exception('Invalid coordinates'); when(() => mockRepository.getLocationsListByCoordinates(latitude, longitude)).thenThrow(exception); - expect(() => cubit.getLocationsByCoordinates(latitude, longitude), throwsException); + final result = await cubit.getLocationsByCoordinates(latitude, longitude); + expect(result, isEmpty); }); test('getLocationsByQuery handles empty query', () async { diff --git a/test/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit_test.dart b/test/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit_test.dart index 2a78ce73..cd39b8ed 100644 --- a/test/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit_test.dart +++ b/test/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit_test.dart @@ -140,7 +140,7 @@ void main() { when(mockRepository.sendEmailCode()) .thenAnswer((_) async => {}); when(mockRepository.tryVerifyEmailByCode('wrong')) - .thenAnswer((_) async => false); + .thenThrow(Exception('Неверный код')); await cubit!.sendEmailCode(); await cubit!.tryVerify('wrong'); diff --git a/test/features/registration/repositories/registration_repository_test.dart b/test/features/registration/repositories/registration_repository_test.dart index 07c06bec..aeee031a 100644 --- a/test/features/registration/repositories/registration_repository_test.dart +++ b/test/features/registration/repositories/registration_repository_test.dart @@ -145,7 +145,10 @@ void main() { when(() => mockService.register(sheet: any(named: 'sheet'))) .thenThrow(dioException); - expect(() => repository.tryRegister(sheet), throwsA(isA())); + await expectLater( + repository.tryRegister(sheet), + throwsA(isA()), + ); }); test('getLocationsListByQuery - возвращает LocationModel', () async { diff --git a/test/fixtures/doubles/auth_doubles.dart b/test/fixtures/doubles/auth_doubles.dart index 8dbfaedf..3e1eecdd 100644 --- a/test/fixtures/doubles/auth_doubles.dart +++ b/test/fixtures/doubles/auth_doubles.dart @@ -14,7 +14,8 @@ import 'package:eatvio_app/features/auth/services/auth_service.dart'; /// - Returns data loaded from fixture files so QA/backenders can see /// exactly what is exchanged. class FixtureAuthRepository extends AuthRepository { - FixtureAuthRepository({required this.tokenFixture}) : super(authService: _NoopAuthService()); + FixtureAuthRepository({required this.tokenFixture}) + : super(authService: _NoopAuthService()); final TokenModel tokenFixture; diff --git a/test/fixtures/doubles/blog_post_doubles.dart b/test/fixtures/doubles/blog_post_doubles.dart index bb1df7c6..d79ca54c 100644 --- a/test/fixtures/doubles/blog_post_doubles.dart +++ b/test/fixtures/doubles/blog_post_doubles.dart @@ -4,7 +4,8 @@ import 'package:eatvio_app/features/blog_post_create/services/blog_post_service. import 'package:flutter_image_compress/flutter_image_compress.dart'; class FixtureBlogPostRepository extends BlogPostRepository { - FixtureBlogPostRepository() : super(blogPostService: _NoopBlogPostService()); + FixtureBlogPostRepository() + : super(blogPostService: _NoopBlogPostService()); final List createPayloads = []; final List updatePayloads = []; diff --git a/test/fixtures/doubles/create_diet_doubles.dart b/test/fixtures/doubles/create_diet_doubles.dart index 52645ae5..c5addea6 100644 --- a/test/fixtures/doubles/create_diet_doubles.dart +++ b/test/fixtures/doubles/create_diet_doubles.dart @@ -12,7 +12,8 @@ import 'package:retrofit/dio.dart' show HttpResponse; import '../../fixtures/fixture_reader.dart'; class FixtureCreateDietRepository extends CreateDietRepository { - FixtureCreateDietRepository() : super(createDietService: _NoopCreateDietService()); + FixtureCreateDietRepository() + : super(createDietService: _NoopCreateDietService()); final List> createRequests = >[]; final List> updateRequests = >[]; @@ -136,7 +137,8 @@ class FixtureCreateDietRepository extends CreateDietRepository { } class FixtureGlossaryRepository extends GlossaryRepository { - FixtureGlossaryRepository() : super(glossaryService: _NoopGlossaryService()); + FixtureGlossaryRepository() + : super(glossaryService: _NoopGlossaryService()); @override Future> getGlossaryListByQuery(String query) async { diff --git a/test/fixtures/doubles/create_plan_doubles.dart b/test/fixtures/doubles/create_plan_doubles.dart index 6c32d0bb..75fd6668 100644 --- a/test/fixtures/doubles/create_plan_doubles.dart +++ b/test/fixtures/doubles/create_plan_doubles.dart @@ -3,7 +3,8 @@ import 'package:eatvio_app/features/create_plan/repositories/create_plan_reposit import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; class FixtureCreatePlanRepository extends CreatePlanRepository { - FixtureCreatePlanRepository() : super(createPlanService: _NoopCreatePlanService()); + FixtureCreatePlanRepository() + : super(createPlanService: _NoopCreatePlanService()); final List> createPlanRequests = >[]; diff --git a/test/fixtures/doubles/profile_doubles.dart b/test/fixtures/doubles/profile_doubles.dart index d3b98791..dfc8e935 100644 --- a/test/fixtures/doubles/profile_doubles.dart +++ b/test/fixtures/doubles/profile_doubles.dart @@ -13,7 +13,8 @@ import 'package:eatvio_app/features/profile/services/profile_service.dart'; import '../fixture_reader.dart'; class FixtureProfileRepository extends ProfileRepository { - FixtureProfileRepository() : super(profileService: _NoopProfileService()); + FixtureProfileRepository() + : super(profileService: _NoopProfileService()); final List> addWeightRequests = >[]; final List> addCircumferenceRequests = >[]; @@ -64,7 +65,8 @@ class _NoopProfileService implements ProfileService { } class FixtureChatRepository extends ChatRepository { - FixtureChatRepository() : super(chatService: _NoopChatService()); + FixtureChatRepository() + : super(chatService: _NoopChatService()); @override Future getChatByUserSlug(String slug) async { @@ -78,7 +80,8 @@ class _NoopChatService implements ChatService { } class FixtureConsultantRepository extends ConsultantRepository { - FixtureConsultantRepository() : super(consultantService: _NoopConsultantService()); + FixtureConsultantRepository() + : super(consultantService: _NoopConsultantService()); @override Future getConsultantRequestStatus(String slug) async { From a71bcc752dc583d28f16b58bf0bba02cd32b82ff Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 7 Apr 2026 10:06:04 +0300 Subject: [PATCH 10/18] feat(fixes): fixed analyzer issue --- lib/core/app_router.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 2b32db51..62a840e0 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -231,7 +231,6 @@ class AppRouter { } final target = _resolveDeepLinkUri(uri); - print('target: $target'); FlutterNativeSplash.remove(); if (target != null) { appRouter.go(target); From f17f1ecb57ebfd82169821fc12c5a22de14c376a Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 7 Apr 2026 10:32:10 +0300 Subject: [PATCH 11/18] version changed --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0af67d5b..079cf190 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.3.20+100 +version: 1.3.21+101 publish_to: 'none' environment: From 4ca90eb2df5d94eabd70194118f3828f6ba11362 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 8 Apr 2026 12:14:43 +0300 Subject: [PATCH 12/18] feat(push_navigation): fixed navigation to chat screen --- .../bloc/chat_room_cubit/chat_room_cubit.dart | 21 ++++++++----------- .../chat/presentation/pages/chat_screen.dart | 8 +++++++ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index d61f8ddc..dc56a0c1 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -93,6 +93,7 @@ class ChatRoomCubit extends Cubit { Future initChat(String uuid, {bool sendInitialMessage = false}) async { try { + emit(const ChatRoomState.loading()); final chatRoom = await chatRepository.getChatRoom(uuid); ChosenPlanModel? chosenPlan; if (chatRoom.chosenPlanId != null) { @@ -175,12 +176,12 @@ class ChatRoomCubit extends Cubit { AudioPlayer().play((AssetSource('audio/recive_message.wav'))); final message = ChatMessageModel.fromJson(jsonDecode(event.data)); final currentState = state as ChatRoomStateLoaded; - + // Сообщения от текущего пользователя не приходят через WebSocket, // они обрабатываются через ответ API в sendMessage // Здесь обрабатываем только входящие сообщения от других пользователей List messagesList = [message, ...currentState.chatMessages]; - + chatRepository.emitNewMessageToChats(message); emit(currentState.copyWith(chatMessages: messagesList, isLoading: isLoading)); EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl().getChatRooms()); @@ -201,7 +202,7 @@ class ChatRoomCubit extends Cubit { final messageUuid = uuid.v4(); final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List.from(files)) : null; AudioPlayer().play((AssetSource('audio/send_message.wav'))); - + // Создаем временное сообщение с состоянием loading emit( (state as ChatRoomStateLoaded).copyWith( @@ -226,7 +227,7 @@ class ChatRoomCubit extends Cubit { try { // Отправляем сообщение и получаем ответ от сервера с правильными данными final serverMessage = await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug); - + // Заменяем локальное сообщение данными с сервера (с правильными URL файлов) emit( (state as ChatRoomStateLoaded).copyWith( @@ -357,7 +358,7 @@ class ChatRoomCubit extends Cubit { if (message != null) { try { AudioPlayer().play((AssetSource('audio/send_message.wav'))); - + // Определяем, есть ли файлы для повторной отправки List? filesToResend; if (message.mediaData != null && message.mediaData!.isNotEmpty) { @@ -368,7 +369,7 @@ class ChatRoomCubit extends Cubit { filesToResend = message.mediaData!.cast(); } } - + emit( (state as ChatRoomStateLoaded).copyWith( chatMessages: [ @@ -391,12 +392,8 @@ class ChatRoomCubit extends Cubit { try { // Отправляем сообщение и получаем ответ от сервера - final serverMessage = await chatRepository.sendMessage( - data: message.data, - files: filesToResend, - slug: userSlug, - ); - + final serverMessage = await chatRepository.sendMessage(data: message.data, files: filesToResend, slug: userSlug); + // Заменяем локальное сообщение данными с сервера emit( (state as ChatRoomStateLoaded).copyWith( diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index f5e4105b..423b6980 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -63,6 +63,14 @@ class _ChatScreenState extends State { chatRoomCubit.setTimer(); } + @override + void didUpdateWidget(ChatScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.uuid != widget.uuid) { + chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false); + } + } + @override void initState() { super.initState(); From 71a405c63ff20573d75356d3dd3dddcc798c5b6b Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 8 Apr 2026 12:43:15 +0300 Subject: [PATCH 13/18] feat(notifications): changed navigation flow from Cancelled diet and started diet notifications --- lib/core/routing/main_navigation_routes.dart | 8 ++++++-- .../presentation/widgets/notification_widget.dart | 12 +++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/core/routing/main_navigation_routes.dart b/lib/core/routing/main_navigation_routes.dart index 7d06147f..d6dd7420 100644 --- a/lib/core/routing/main_navigation_routes.dart +++ b/lib/core/routing/main_navigation_routes.dart @@ -47,8 +47,12 @@ class MainNavigationRoutes { path: RouteConstants.feedScreen, name: 'feedScreen', pageBuilder: (context, state) { - final initialTabIndex = int.tryParse(state.uri.queryParameters['initialTabIndex'] ?? ''); - return NoTransitionPage(child: FeedScreen(initialTabIndex: initialTabIndex)); + final initialTabIndexFromQuery = int.tryParse(state.uri.queryParameters['initialTabIndex'] ?? ''); + final extra = state.extra; + if (extra is int) { + return NoTransitionPage(child: FeedScreen(initialTabIndex: extra)); + } + return NoTransitionPage(child: FeedScreen(initialTabIndex: initialTabIndexFromQuery)); }, redirect: (context, state) async { try { diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index 7f22f5e7..a8b2ab4e 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart'; @@ -594,7 +595,6 @@ class _EntityCommentedWidget extends StatelessWidget { } } - // Новые виджеты для дополнительных типов уведомлений class _PlanSubscriptionExpiredWidget extends StatelessWidget { const _PlanSubscriptionExpiredWidget({required this.notification}); @@ -737,7 +737,11 @@ class _DietCancelledWidget extends StatelessWidget { createdAt: notification.createdAt, onTap: () { if (notification.dietId > 0) { - context.push(RouteConstants.aboutDietScreen, extra: {'id': notification.dietId}); + const dietTabIndexWithHiddenPlansTab = 3; + const dietTabIndexWithoutHiddenPlansTab = 4; + + final initialTabIndex = hidePlans ? dietTabIndexWithHiddenPlansTab : dietTabIndexWithoutHiddenPlansTab; + _navigateToBottomNavRoute(context, RouteConstants.feedScreen, initialTabIndex); } }, ); @@ -754,9 +758,7 @@ class _DietStartedWidget extends StatelessWidget { notificationsText: 'Старт диеты "${notification.dietName}"! Успехов! 🥗', createdAt: notification.createdAt, onTap: () { - if (notification.dietId > 0) { - context.push(RouteConstants.aboutDietScreen, extra: {'id': notification.dietId}); - } + _navigateToBottomNavRoute(context, RouteConstants.diaryScreen); }, ); } From 631ceb2b7a4019ad4009c04d4d1495d1ae5a37d7 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 8 Apr 2026 12:47:07 +0300 Subject: [PATCH 14/18] version changed --- pubspec.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 079cf190..b7f19603 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.3.21+101 +version: 1.3.22+102 publish_to: 'none' environment: @@ -84,7 +84,7 @@ dependencies: webview_flutter_platform_interface: ^2.14.0 intl_utils: ^2.8.13 sentry_dio: ^9.10.0 - + dev_dependencies: build_runner: ^2.10.4 freezed: ^3.2.0 @@ -111,7 +111,7 @@ flutter: - assets/icons/ - assets/splash/ - assets/lottie_animations/ - + fonts: - family: Mulish-Local fonts: From 684415ef5b563528aef3b21ca42c8aba100ee6e0 Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 14 Apr 2026 09:28:02 +0300 Subject: [PATCH 15/18] feat(setting): added profile data apdating for sttings screen, added validation for general info form --- lib/core/routing/main_navigation_routes.dart | 11 +- .../bloc/menu_cubit/menu_cubit.dart | 14 +- .../bloc/menu_cubit/menu_state.dart | 18 +- .../menu/presentation/pages/menu_screen.dart | 173 +++++++++------- .../pages/general_info_screen.dart | 190 ++++++++++-------- .../bloc/menu_cubit/menu_state_test.dart | 68 +++++-- .../pages/menu_screen_smoke_test.dart | 6 +- 7 files changed, 291 insertions(+), 189 deletions(-) diff --git a/lib/core/routing/main_navigation_routes.dart b/lib/core/routing/main_navigation_routes.dart index d6dd7420..d0746c88 100644 --- a/lib/core/routing/main_navigation_routes.dart +++ b/lib/core/routing/main_navigation_routes.dart @@ -89,16 +89,7 @@ class MainNavigationRoutes { 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(); - }, - ), - ), + pageBuilder: (context, state) => const NoTransitionPage(child: MenuScreen()), ), ], ), diff --git a/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart b/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart index b4ab6b41..3d5b94ed 100644 --- a/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart +++ b/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart @@ -7,7 +7,19 @@ import 'package:eatvio_app/core/error_reporting/report_from.dart'; class MenuCubit extends Cubit { final ProfileRepository profileRepository; - MenuCubit(this.profileRepository) : super(MenuState()); + MenuCubit(this.profileRepository) : super(const MenuState.initial()); + + Future getMyProfile() async { + emit(const MenuState.loading()); + try { + final profile = await profileRepository.getMyProfile(); + emit(MenuState.loaded(profileModel: profile)); + } catch (e, st) { + await reportFrom(e, st, this); + log(e.toString()); + emit(const MenuState.error()); + } + } Future makePassKey() async { try { diff --git a/lib/features/menu/presentation/bloc/menu_cubit/menu_state.dart b/lib/features/menu/presentation/bloc/menu_cubit/menu_state.dart index 70f37bb8..51dfcc1d 100644 --- a/lib/features/menu/presentation/bloc/menu_cubit/menu_state.dart +++ b/lib/features/menu/presentation/bloc/menu_cubit/menu_state.dart @@ -1 +1,17 @@ -class MenuState {} \ No newline at end of file +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'menu_state.freezed.dart'; + +@freezed +abstract class MenuState with _$MenuState { + const factory MenuState.initial() = MenuStateInitial; + + const factory MenuState.loading() = MenuStateLoading; + + const factory MenuState.loaded({ + required ProfileModel profileModel, + }) = MenuStateLoaded; + + const factory MenuState.error() = MenuStateError; +} \ No newline at end of file diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 859b1b6f..05cabce2 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -6,6 +6,8 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/image_constant.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'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; @@ -13,9 +15,9 @@ import 'package:eatvio_app/features/consultant_rating/presentation/consultant_ra 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/bloc/menu_cubit/menu_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_state.dart'; import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; -import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -25,90 +27,107 @@ import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; class MenuScreen extends StatelessWidget { - final ProfileModel profileModel; - - const MenuScreen({super.key, required this.profileModel}); + const MenuScreen({super.key}); @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => sl(), + create: (context) => sl()..getMyProfile(), child: Builder( builder: (context) { - return Scaffold( - appBar: AppBar(centerTitle: false, title: const Text('Меню')), - body: SingleChildScrollView( - child: Column( - children: [ - _tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')), - if (profileModel.roles.contains('consultant')) _divider(context), - if (profileModel.roles.contains('consultant')) - _tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push(MyRecipesScreen.path)), - if (profileModel.roles.contains('consultant')) _divider(context), - if (profileModel.roles.contains('consultant')) - _tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)), - if (profileModel.roles.contains('consultant')) _divider(context), - if (profileModel.roles.contains('consultant')) - _tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)), - if (!hidePlans) ...[ - _divider(context), - _tile( - leading: 'assets/icons/finance.svg', - title: 'Мои финансы', - onTap: () { - if (profileModel.roles.contains('consultant')) { - GoRouter.of(context).push(ConsultantFinancesScreen.path); - return; - } - GoRouter.of(context).push(UserPurchasesScreen.path); - }, + return BlocBuilder( + builder: (BuildContext context, MenuState state) { + if (state is MenuStateLoaded) { + final profileModel = state.profileModel; + return Scaffold( + appBar: AppBar(centerTitle: false, title: const Text('Меню')), + body: SingleChildScrollView( + child: Column( + children: [ + _tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')), + if (profileModel.roles.contains('consultant')) _divider(context), + if (profileModel.roles.contains('consultant')) + _tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push(MyRecipesScreen.path)), + if (profileModel.roles.contains('consultant')) _divider(context), + if (profileModel.roles.contains('consultant')) + _tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)), + if (profileModel.roles.contains('consultant')) _divider(context), + if (profileModel.roles.contains('consultant')) + _tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)), + if (!hidePlans) ...[ + _divider(context), + _tile( + leading: 'assets/icons/finance.svg', + title: 'Мои финансы', + onTap: () { + if (profileModel.roles.contains('consultant')) { + GoRouter.of(context).push(ConsultantFinancesScreen.path); + return; + } + GoRouter.of(context).push(UserPurchasesScreen.path); + }, + ), + ], + if (profileModel.roles.contains('consultant')) _divider(context), + if (profileModel.roles.contains('consultant')) + _tile( + leading: 'assets/icons/consultant_rating_icon.svg', + title: 'Мой рейтинг', + onTap: () { + GoRouter.of(context).push(ConsultantRatingScreen.path); + }, + ), + _divider(context), + _tile( + leading: ImageConstant.icnGlossary, + title: 'Глоссарий', + onTap: () async { + final passKey = await context.read().makePassKey(); + if (passKey != null && context.mounted) { + GoRouter.of(context).push('/glossary-screen', extra: passKey); + } + }, + ), + _divider(context), + _tile( + leading: 'assets/icons/notifications.svg', + title: 'Уведомления', + onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen'), + ), + _divider(context), + _tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')), + _divider(context), + _tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')), + // _divider(context), + // _tile( + // leading: ImageConstant.icnDevTools, + // title: 'Для разработчиков', + // onTap: () { + // GoRouter.of(context).pushNamed(DevToolsScreen.name); + // }, + // ), + _divider(context), + _tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)), + const SizedBox(height: 10), + const AppInfo(), + ], ), - ], - if (profileModel.roles.contains('consultant')) _divider(context), - if (profileModel.roles.contains('consultant')) - _tile( - leading: 'assets/icons/consultant_rating_icon.svg', - title: 'Мой рейтинг', - onTap: () { - GoRouter.of(context).push(ConsultantRatingScreen.path); - }, - ), - _divider(context), - _tile( - leading: ImageConstant.icnGlossary, - title: 'Глоссарий', - onTap: () async { - final passKey = await context.read().makePassKey(); - if (passKey != null && context.mounted) { - GoRouter.of(context).push('/glossary-screen', extra: passKey); - } - }, ), - _divider(context), - _tile( - leading: 'assets/icons/notifications.svg', - title: 'Уведомления', - onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen'), - ), - _divider(context), - _tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')), - _divider(context), - _tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')), - // _divider(context), - // _tile( - // leading: ImageConstant.icnDevTools, - // title: 'Для разработчиков', - // onTap: () { - // GoRouter.of(context).pushNamed(DevToolsScreen.name); - // }, - // ), - _divider(context), - _tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)), - const SizedBox(height: 10), - const AppInfo(), - ], - ), - ), + ); + } + + if (state is MenuStateError) { + return Scaffold( + appBar: AppBar(centerTitle: false, title: const Text('Меню')), + body: DataErrorWidget(onReload: () async => context.read().getMyProfile()), + ); + } + + return Scaffold( + appBar: AppBar(centerTitle: false, title: const Text('Меню')), + body: const Loader(), + ); + }, ); }, ), diff --git a/lib/features/settings/presentation/pages/general_info_screen.dart b/lib/features/settings/presentation/pages/general_info_screen.dart index 7ea472ab..2f799bf7 100644 --- a/lib/features/settings/presentation/pages/general_info_screen.dart +++ b/lib/features/settings/presentation/pages/general_info_screen.dart @@ -1,6 +1,8 @@ import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -8,15 +10,11 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; class GeneralInfoScreen extends StatefulWidget { final SettingsEntity settingsEntity; - const GeneralInfoScreen({ - super.key, - required this.settingsEntity, - }); + const GeneralInfoScreen({super.key, required this.settingsEntity}); @override State createState() => _GeneralInfoScreenState(); @@ -31,6 +29,10 @@ class _GeneralInfoScreenState extends State { final cityController = TextEditingController(); final languageController = TextEditingController(); late SelectionPopupModel isPublicProfile; + bool isLoading = false; + bool isNameMissing = false; + bool isLastNameMissing = false; + bool isLanguageMissing = false; final List genderSelectionPopupList = [ const SelectionPopupModel(id: 0, title: 'Женский', value: 'female'), @@ -53,8 +55,9 @@ class _GeneralInfoScreenState extends State { countryController.text = widget.settingsEntity.profileDetailsModel.country; cityController.text = widget.settingsEntity.profileDetailsModel.city; languageController.text = widget.settingsEntity.profileDetailsModel.lang; - isPublicProfile = - widget.settingsEntity.profileSettingsModel.isPublicProfile == true ? isPublicProfileSelectionPopupList[1] : isPublicProfileSelectionPopupList[0]; + isPublicProfile = widget.settingsEntity.profileSettingsModel.isPublicProfile == true + ? isPublicProfileSelectionPopupList[1] + : isPublicProfileSelectionPopupList[0]; } @override @@ -62,9 +65,7 @@ class _GeneralInfoScreenState extends State { final s = S.of(context); return Scaffold( - appBar: AppBar( - title: const Text('Общая информация'), - ), + appBar: AppBar(title: const Text('Общая информация')), bottomSheet: SafeArea( top: false, child: Padding( @@ -80,34 +81,31 @@ class _GeneralInfoScreenState extends State { children: [ Text(s.first_name), const SizedBox(height: 4), - CustomTextFormField( - controller: firstNameController, - hintText: 'Введите имя', - onChanged: (newValue) {}, + Column( + children: [ + CustomTextFormField(hasError: isNameMissing, controller: firstNameController, hintText: 'Введите имя', onChanged: (newValue) {}), + if (isNameMissing) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Имя не может быть пустым')], + ], ), const SizedBox(height: 16), Text(s.last_name), const SizedBox(height: 4), - CustomTextFormField( - controller: lastNameController, - hintText: 'Введите фамилию', - onChanged: (newValue) {}, + Column( + children: [ + CustomTextFormField(hasError: isLastNameMissing, controller: lastNameController, hintText: 'Введите фамилию', onChanged: (newValue) {}), + if (isLastNameMissing) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Фамилия не может быть пустой')], + ], ), const SizedBox(height: 16), Text(s.date_of_birth), const SizedBox(height: 4), ListTile( onTap: onTapCalendar(context), - trailing: Icon( - Icons.keyboard_arrow_down_rounded, - color: Colors.grey.shade700, - ), + trailing: Icon(Icons.keyboard_arrow_down_rounded, color: Colors.grey.shade700), contentPadding: const EdgeInsets.symmetric(horizontal: 12), title: Text(selectedDate.format('dd MMMM yyyy', 'ru')), tileColor: const Color(0xFFF6F6F6), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), ), const SizedBox(height: 16), const Text('Пол'), @@ -124,20 +122,19 @@ class _GeneralInfoScreenState extends State { const SizedBox(height: 16), const Text('Страна'), const SizedBox(height: 4), - CustomTextFormField( - controller: countryController, - ), + CustomTextFormField(controller: countryController), const SizedBox(height: 16), const Text('Город'), const SizedBox(height: 4), - CustomTextFormField( - controller: cityController, - ), + CustomTextFormField(controller: cityController), const SizedBox(height: 16), const Text('Предпочтительный язык общения'), const SizedBox(height: 4), - CustomTextFormField( - controller: languageController, + Column( + children: [ + CustomTextFormField(hasError: isLanguageMissing, controller: languageController), + if (isLanguageMissing) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Язык не может быть пустым')], + ], ), const SizedBox(height: 16), const Text('Показывать анкету'), @@ -181,59 +178,92 @@ class _GeneralInfoScreenState extends State { builder: (BuildContext context) => Container( height: 216, padding: const EdgeInsets.only(top: 6), - margin: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), + margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), color: CupertinoColors.systemBackground.resolveFrom(context), - child: SafeArea( - top: false, - child: child, - ), + child: SafeArea(top: false, child: child), ), ); } - InkWell _saveButton() { - return InkWell( - onTap: () { - widget.settingsEntity.settingsCubit.updateProfileDetails( - profileDetailsModel: (widget.settingsEntity.settingsCubit.state as SettingsStateLoaded).profileDetails.copyWith( - firstName: firstNameController.text, - lastName: lastNameController.text, - birthdate: selectedDate.toString(), - gender: gender.value, - country: countryController.text, - city: cityController.text, - lang: languageController.text, - ), - ); - widget.settingsEntity.settingsCubit.updateProfileSettings( - profileSettingsModel: (widget.settingsEntity.settingsCubit.state as SettingsStateLoaded).profileSettings.copyWith( - isPublicProfile: isPublicProfile.value, + bool _checkFields() { + bool hasError = false; + if (firstNameController.text.isEmpty) { + setState(() { + isNameMissing = true; + }); + hasError = true; + } else { + setState(() { + isNameMissing = false; + }); + } + if (lastNameController.text.isEmpty) { + setState(() { + isLastNameMissing = true; + }); + hasError = true; + } else { + setState(() { + isLastNameMissing = false; + }); + } + if (languageController.text.isEmpty) { + setState(() { + isLanguageMissing = true; + }); + hasError = true; + } else { + setState(() { + isLanguageMissing = false; + }); + } + return hasError; + } + + Widget _saveButton() { + return isLoading + ? const Center(child: Loader()) + : InkWell( + onTap: () async { + final hasError = _checkFields(); + if (hasError) { + return; + } + setState(() { + isLoading = true; + }); + await widget.settingsEntity.settingsCubit.updateProfileDetails( + profileDetailsModel: (widget.settingsEntity.settingsCubit.state as SettingsStateLoaded).profileDetails.copyWith( + firstName: firstNameController.text, + lastName: lastNameController.text, + birthdate: selectedDate.toString(), + gender: gender.value, + country: countryController.text, + city: cityController.text, + lang: languageController.text, + ), + ); + await widget.settingsEntity.settingsCubit.updateProfileSettings( + profileSettingsModel: (widget.settingsEntity.settingsCubit.state as SettingsStateLoaded).profileSettings.copyWith( + isPublicProfile: isPublicProfile.value, + ), + ); + setState(() { + isLoading = false; + }); + }, + borderRadius: BorderRadius.circular(20), + child: Ink( + height: 52, + width: MediaQuery.of(context).size.width - 32, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), + child: const Center( + child: Text( + 'Сохранить', + style: TextStyle(color: Colors.white, height: 1, fontWeight: FontWeight.w600, fontSize: 18), + ), ), - ); - GoRouter.of(context).pop(); - }, - borderRadius: BorderRadius.circular(20), - child: Ink( - height: 52, - width: MediaQuery.of(context).size.width - 32, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), - child: const Center( - child: Text( - 'Сохранить', - style: TextStyle( - color: Colors.white, - height: 1, - fontWeight: FontWeight.w600, - fontSize: 18, ), - ), - ), - ), - ); + ); } } diff --git a/test/features/menu/presentation/bloc/menu_cubit/menu_state_test.dart b/test/features/menu/presentation/bloc/menu_cubit/menu_state_test.dart index 98a09db5..eae4f6ca 100644 --- a/test/features/menu/presentation/bloc/menu_cubit/menu_state_test.dart +++ b/test/features/menu/presentation/bloc/menu_cubit/menu_state_test.dart @@ -1,32 +1,64 @@ import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_state.dart'; +import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('MenuState', () { - test('creates MenuState instance', () { - final state = MenuState(); + test('creates MenuState initial state', () { + const state = MenuState.initial(); expect(state, isNotNull); - expect(state, isA()); + expect(state, isA()); }); - test('creates multiple MenuState instances', () { - final state1 = MenuState(); - final state2 = MenuState(); - - expect(state1, isNotNull); - expect(state2, isNotNull); - expect(state1, isA()); - expect(state2, isA()); + test('creates MenuState loading state', () { + const state = MenuState.loading(); + expect(state, isNotNull); + expect(state, isA()); }); - test('MenuState instances are not equal by default', () { - final state1 = MenuState(); - final state2 = MenuState(); + test('creates MenuState error state', () { + const state = MenuState.error(); + expect(state, isNotNull); + expect(state, isA()); + }); - // MenuState is a regular class without overridden == operator, - // so instances are compared by reference - expect(state1 == state2, isFalse); - expect(state1, isNot(same(state2))); + test('creates MenuState loaded state', () { + const profile = ProfileModel( + id: 1, + name: 'Test', + email: 'test@example.com', + phoneConfirmationState: 'confirmed', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + profilePhotoUrl: 'https://example.com/icon.jpg', + roles: [], + badges: [], + subscriptions: [], + subscribers: [], + slug: 'test-user', + ); + const state = MenuState.loaded(profileModel: profile); + expect(state, isNotNull); + expect(state, isA()); + expect((state as MenuStateLoaded).profileModel, isNotNull); + expect((state).profileModel.id, 1); + expect((state).profileModel.name, 'Test'); + expect((state).profileModel.email, 'test@example.com'); + expect((state).profileModel.phoneConfirmationState, 'confirmed'); + expect((state).profileModel.isPublicProfile, true); + expect((state).profileModel.isBlockedByMe, false); + expect((state).profileModel.isBlockedByAuthor, false); + expect((state).profileModel.isBlockedByUser, false); + expect((state).profileModel.isEntitySubscribedByMe, false); + expect((state).profileModel.profilePhotoUrl, 'https://example.com/icon.jpg'); + expect((state).profileModel.roles, []); + expect((state).profileModel.badges, []); + expect((state).profileModel.subscriptions, []); + expect((state).profileModel.subscribers, []); + expect((state).profileModel.slug, 'test-user'); }); }); } diff --git a/test/features/menu/presentation/pages/menu_screen_smoke_test.dart b/test/features/menu/presentation/pages/menu_screen_smoke_test.dart index cc1cc135..1facf0bb 100644 --- a/test/features/menu/presentation/pages/menu_screen_smoke_test.dart +++ b/test/features/menu/presentation/pages/menu_screen_smoke_test.dart @@ -141,11 +141,12 @@ void main() { final cubit = MenuCubit(repository); when(repository.getPassKey(routeName: anyNamed('routeName'), routeParams: anyNamed('routeParams'))).thenAnswer((_) async => 'test-passkey'); + when(repository.getMyProfile()).thenAnswer((_) async => mockUserProfile); sl.registerLazySingleton(() => repository); sl.registerLazySingleton(() => cubit); - await tester.pumpWidget(wrap(const MenuScreen(profileModel: mockUserProfile))); + await tester.pumpWidget(wrap(const MenuScreen())); await tester.pumpAndSettle(); expect(find.text('Меню'), findsOneWidget); @@ -157,11 +158,12 @@ void main() { final cubit = MenuCubit(repository); when(repository.getPassKey(routeName: anyNamed('routeName'), routeParams: anyNamed('routeParams'))).thenAnswer((_) async => 'test-passkey'); + when(repository.getMyProfile()).thenAnswer((_) async => mockConsultantProfile); sl.registerLazySingleton(() => repository); sl.registerLazySingleton(() => cubit); - await tester.pumpWidget(wrap(const MenuScreen(profileModel: mockConsultantProfile))); + await tester.pumpWidget(wrap(const MenuScreen())); await tester.pumpAndSettle(); expect(find.text('Меню'), findsOneWidget); From e814683984f53bab2af00e776019a9ae4d94b527 Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 14 Apr 2026 09:44:12 +0300 Subject: [PATCH 16/18] version changed --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index b7f19603..03784fb4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.3.22+102 +version: 1.3.23+103 publish_to: 'none' environment: From 03ec91a595bb8a005edba5c106e73338720fc28c Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 15 Apr 2026 15:50:09 +0300 Subject: [PATCH 17/18] feat(image_compress_utility): improved compressing logic --- .../image_compress_utility.dart | 42 ++++--------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/lib/core/image_compressing/image_compress_utility.dart b/lib/core/image_compressing/image_compress_utility.dart index b9cbeb18..239824b4 100644 --- a/lib/core/image_compressing/image_compress_utility.dart +++ b/lib/core/image_compressing/image_compress_utility.dart @@ -3,7 +3,6 @@ import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; -import 'package:image/image.dart' as img; import 'package:path/path.dart' as p; abstract class ImageCompressUtility { @@ -30,44 +29,21 @@ abstract class ImageCompressUtility { } static Future compressFile(File file) async { - // Skip compression in test environment to avoid hangs try { final appConfig = sl(); if (appConfig.buildType == AppBuildType.tests) { - // In tests, return the original file without compression return file; } - } catch (_) { - // If AppConfig is not available, proceed with compression + } catch (_) {} + + final compressedPath = '${p.withoutExtension(file.path)}_compressed.jpg'; + + final result = await FlutterImageCompress.compressAndGetFile(file.absolute.path, compressedPath, quality: 90, minHeight: 1080, minWidth: 1080); + + if (result == null) { + return file; } - final image = img.decodeImage(file.readAsBytesSync())!; - int width = image.width; - int height = image.height; - - if (width > height) { - if (width > 1080) { - height = (height * 1080) ~/ width; - width = 1080; - } - } else { - if (height > 1080) { - width = (width * 1080) ~/ height; - height = 1080; - } - } - - String filePathWithoutExtension = p.withoutExtension(file.path); - String compressedFilePath = '${filePathWithoutExtension}_compressed.jpg'; - - final result = await FlutterImageCompress.compressAndGetFile( - file.absolute.path, - compressedFilePath, - quality: 90, - minHeight: height, - minWidth: width, - ); - - return File(result!.path); + return File(result.path); } } From c8284338aa5831aca3a7822787afcb9a316a7ebb Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 15 Apr 2026 16:27:27 +0300 Subject: [PATCH 18/18] feat(image_compress_utility): adde test for corupted images --- .../image_compress_utility_test.dart | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/core/image_compressing/image_compress_utility_test.dart b/test/core/image_compressing/image_compress_utility_test.dart index 9f619520..456543f2 100644 --- a/test/core/image_compressing/image_compress_utility_test.dart +++ b/test/core/image_compressing/image_compress_utility_test.dart @@ -426,5 +426,27 @@ void main() { } } }); + + test('compressFile - does not crash on corrupted image', () async { + final file = File('corrupted.jpg'); + + try { + await file.writeAsBytes([ + 0xFF, 0xD8, 0xFF, 0xE0, + 0x00, 0x10, + 0x4A, 0x46, 0x49, 0x46, + 0xFF, 0xDA, + ]); + + final result = await ImageCompressUtility.compressFile(file); + + expect(result, isA()); + expect(result.path, file.path); + } finally { + if (await file.exists()) { + await file.delete(); + } + } + }); }); }