From 335e36dd180f296c463f956d210d3c79e52036b4 Mon Sep 17 00:00:00 2001 From: misha Date: Thu, 26 Mar 2026 18:06:20 +0300 Subject: [PATCH] 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())); }