forked from eatvio/eatvio_app
feat(pushNotifications): fixed push notifications navigation
This commit is contained in:
@@ -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<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
// Домены для deep links (в т.ч. ссылки из пушей dev2.eatvio.com)
|
||||
static const List<String> deepLinkHosts = ['eatvio.ru', 'eatvio.com'];
|
||||
static const List<String> deepLinkHosts = ['eatvio.ru', 'eatvio.com', 'dev2.eatvio.com'];
|
||||
static const List<String> metaSegments = ['ru', 'en', 'api', 'v2'];
|
||||
|
||||
static GoRouter appRouter = GoRouter(
|
||||
initialLocation: RouteConstants.updateScreen,
|
||||
@@ -30,11 +31,111 @@ class AppRouter {
|
||||
],
|
||||
);
|
||||
|
||||
static String? _resolveRecipeDeepLink(List<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> effectiveSegments) {
|
||||
return RouteConstants.myProfileScreen;
|
||||
}
|
||||
|
||||
static String? _resolveDiaryDeepLink(List<String> effectiveSegments) {
|
||||
developer.log('Deep link: diary', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
}
|
||||
|
||||
static String? _resolveConsultantDiaryCommentDeepLink(List<String> 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<String> 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 {
|
||||
|
||||
@@ -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<void> 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<String, dynamic> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<WsManager>().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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,10 +156,7 @@ class OtherRoutes {
|
||||
builder: (context, state) {
|
||||
if (state.extra is Map<String, dynamic>) {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
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<String, dynamic>;
|
||||
final uuid = data['uuid'] as String;
|
||||
final sendInitialMessage = data['sendInitialMessage'] as bool?;
|
||||
@@ -233,7 +234,7 @@ class OtherRoutes {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
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()),
|
||||
|
||||
@@ -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<ChatScreen> {
|
||||
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<ChatScreen> {
|
||||
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<ChatScreen> {
|
||||
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<ChatScreen> {
|
||||
messageOption = chosenPlan.plan.packet3Options;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_messageDate(message.updatedAt),
|
||||
@@ -284,7 +318,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
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<ChatScreen> {
|
||||
messageOption = chosenPlan.plan.packet3Options;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (isPlanOrderMessage && chosenPlan != null && messageOption != null)
|
||||
|
||||
@@ -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<PlansCubit>(create: (context) => sl<PlansCubit>()..loadPlans()),
|
||||
BlocProvider<ConsultantsCubit>(create: (context) => sl<ConsultantsCubit>()..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<FeedScreenContent> createState() => _FeedScreenContentState();
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
final consultantsController = TextEditingController();
|
||||
|
||||
late final List<String> chipsLabelList;
|
||||
int _selectedChipIndex = 0;
|
||||
late int _selectedChipIndex;
|
||||
final blogPostsScrollController = ScrollController();
|
||||
final recipesScrollController = ScrollController();
|
||||
final dietsScrollController = ScrollController();
|
||||
@@ -72,6 +72,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
final consultantsScrollController = ScrollController();
|
||||
@override
|
||||
void initState() {
|
||||
_selectedChipIndex = widget.initialTabIndex;
|
||||
chipsLabelList = hidePlans ? ['Все', 'Консультанты', 'Рецепты', 'Диеты'] : ['Все', 'Планы питания', 'Консультанты', 'Рецепты', 'Диеты'];
|
||||
consultantsScrollController.addListener(_onScrollConsultants);
|
||||
_checkRegistrationDate(context);
|
||||
@@ -88,6 +89,14 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
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) {}
|
||||
}
|
||||
|
||||
@@ -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<ProfileScreen> {
|
||||
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<ProfileScreen> {
|
||||
),
|
||||
);
|
||||
} 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(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await AppInitializer.initialize(buildType: AppBuildType.prod);
|
||||
await AppInitializer.runAppWithSentry(() => runApp(const MyApp()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user