feat: enhance Firebase messaging error logging and simplify BottomNavigationBarWidget by removing profileModel dependency

This commit is contained in:
mso7nueh
2026-01-14 16:09:07 +03:00
parent 41e96c63c6
commit c0032f7a95
6 changed files with 152 additions and 129 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io' as io;
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/env_config.dart';
@@ -51,6 +52,7 @@ class AppInitializer {
);
} catch (e) {
// Разрешение не критично, продолжаем работу
log('Failed to request Firebase messaging permission: $e');
}
}
}

View File

@@ -32,6 +32,7 @@ class FirebaseApi {
);
} catch (e) {
// Разрешение не критично, продолжаем
log('Failed to request Firebase messaging permission: $e');
}
}
token = await _firebaseMessaging.getToken().timeout(

View File

@@ -32,14 +32,7 @@ class MainNavigationRoutes {
});
}
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
builder: (BuildContext context, ProfileAvatarState state) {
if (state is ProfileAvatarStateLoaded) {
return BottomNavigationBarWidget(profileModel: state.profile, child: child);
}
return const SizedBox.shrink();
},
);
return BottomNavigationBarWidget(child: child);
},
routes: [
GoRoute(

View File

@@ -5,7 +5,8 @@ import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
@@ -16,13 +17,11 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
class BottomNavigationBarWidget extends StatefulWidget {
final ProfileModel profileModel;
final Widget child;
const BottomNavigationBarWidget({
super.key,
required this.child,
required this.profileModel,
});
@override
@@ -31,15 +30,12 @@ class BottomNavigationBarWidget extends StatefulWidget {
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
int currentBottomNavBarIndex = 1;
late final List<String> pages;
List<String>? pages;
int tapsCount = 0;
@override
void initState() {
super.initState();
if (widget.profileModel.roles.contains('consultant')) {
pages = [
List<String> _getPages(bool isConsultant) {
if (isConsultant) {
return [
'/my-profile-screen',
'/feed-screen',
'/diary-screen',
@@ -49,7 +45,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
'/menu-screen',
];
} else {
pages = [
return [
'/my-profile-screen',
'/feed-screen',
'/diary-screen',
@@ -61,7 +57,8 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
}
void _onItemTapped(int index) async {
if (pages.length == 7) {
if (pages == null) return;
if (pages!.length == 7) {
if (index == 6) {
tapsCount++;
if (tapsCount == 7) {
@@ -82,108 +79,117 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
setState(() {
currentBottomNavBarIndex = index;
});
GoRouter.of(context).replace(pages[currentBottomNavBarIndex]);
GoRouter.of(context).replace(pages![currentBottomNavBarIndex]);
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Scaffold(
body: widget.child,
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: _onItemTapped,
selectedFontSize: 10,
unselectedFontSize: 10,
selectedItemColor: appTheme.blueA400,
currentIndex: currentBottomNavBarIndex,
items: [
BottomNavigationBarItem(
label: '',
icon: CircleAvatar(
foregroundImage: NetworkImage(
widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + widget.profileModel.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 16,
),
),
BottomNavigationBarItem(
label: 'Лента',
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
width: 20,
height: 20,
ImageConstant.icnFeed,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnFeed,
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
BottomNavigationBarItem(
label: s.diary,
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
ImageConstant.icnDiary,
width: 20,
height: 20,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnDiary,
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
if (widget.profileModel.roles.contains('consultant'))
BottomNavigationBarItem(
label: 'Консультации',
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
'assets/icons/consultations.svg',
width: 20,
height: 20,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
'assets/icons/consultations.svg',
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
builder: (BuildContext context, ProfileAvatarState state) {
if (state is ProfileAvatarStateLoaded) {
final profileModel = state.profile;
final isConsultant = profileModel.roles.contains('consultant');
// Инициализируем pages на основе profileModel
pages ??= _getPages(isConsultant);
return Scaffold(
body: widget.child,
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: _onItemTapped,
selectedFontSize: 10,
unselectedFontSize: 10,
selectedItemColor: appTheme.blueA400,
currentIndex: currentBottomNavBarIndex,
items: [
BottomNavigationBarItem(
label: '',
icon: CircleAvatar(
foregroundImage: NetworkImage(
profileModel.profilePhotoUrl.contains('https') ? profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + profileModel.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 16,
),
),
BottomNavigationBarItem(
label: 'Лента',
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
width: 20,
height: 20,
ImageConstant.icnFeed,
),
),
BottomNavigationBarItem(
label: s.chat,
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnFeed,
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
BottomNavigationBarItem(
label: s.diary,
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
ImageConstant.icnDiary,
width: 20,
height: 20,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnDiary,
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
if (profileModel.roles.contains('consultant'))
BottomNavigationBarItem(
label: 'Консультации',
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
'assets/icons/consultations.svg',
width: 20,
height: 20,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
'assets/icons/consultations.svg',
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
BottomNavigationBarItem(
label: s.chat,
icon: BlocBuilder<ChatsCubit, ChatsState>(
builder: (BuildContext context, ChatsState state) {
final hasUnreadMessages = state.chatsWithUnreadMessagesCount > 0;
@@ -206,7 +212,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
ImageConstant.icnChat,
width: 20,
height: 20,
colorFilter: pages[currentBottomNavBarIndex] == '/chat-select-screen'
colorFilter: pages![currentBottomNavBarIndex] == '/chat-select-screen'
? ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
@@ -217,9 +223,9 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
);
},
),
),
BottomNavigationBarItem(
label: 'Уведомления',
),
BottomNavigationBarItem(
label: 'Уведомления',
icon: BlocBuilder<NotificationsCubit, NotificationsState>(
builder: (BuildContext context, NotificationsState state) {
return badges.Badge(
@@ -237,7 +243,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnBell,
colorFilter: pages[currentBottomNavBarIndex] == '/notifications-screen'
colorFilter: pages![currentBottomNavBarIndex] == '/notifications-screen'
? ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
@@ -251,15 +257,22 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
},
),
),
BottomNavigationBarItem(
label: '',
icon: SvgPicture.asset(
ImageConstant.imgLogo,
height: 32,
BottomNavigationBarItem(
label: '',
icon: SvgPicture.asset(
ImageConstant.imgLogo,
height: 32,
),
),
],
),
),
],
),
);
}
// Показываем child даже когда профиль еще загружается, чтобы экраны могли инициализироваться
return Scaffold(
body: widget.child,
);
},
);
}
}

View File

@@ -77,9 +77,11 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
_checkRegistrationDate(context);
// Инициализация глобальных кубитов при первом открытии экрана
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final feedCubit = context.read<FeedCubit>();
final state = feedCubit.state;
if (state is! FeedStateLoaded && state is! FeedStateLoading) {
// Загружаем данные, если они еще не загружены
if (state is! FeedStateLoaded) {
feedCubit.loadFeedBlogPostList();
}
});

View File

@@ -2,6 +2,8 @@ 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/theme/theme_helper.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
@@ -20,7 +22,7 @@ import 'package:network_image_mock/network_image_mock.dart';
import 'bottom_navigation_bar_widget_test.mocks.dart';
@GenerateNiceMocks([MockSpec<NotificationsCubit>(), MockSpec<ChatsCubit>()])
@GenerateNiceMocks([MockSpec<NotificationsCubit>(), MockSpec<ChatsCubit>(), MockSpec<ProfileAvatarCubit>()])
void main() {
group('BottomNavigationBarWidget smoke', () {
setUpAll(() async {
@@ -86,6 +88,7 @@ void main() {
mockNetworkImagesFor(() async {
final notificationsCubit = MockNotificationsCubit();
final chatsCubit = MockChatsCubit();
final profileAvatarCubit = MockProfileAvatarCubit();
when(notificationsCubit.state).thenReturn(const NotificationsState());
when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty());
@@ -93,16 +96,20 @@ void main() {
when(chatsCubit.state).thenReturn(ChatsState());
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(profileAvatarCubit.state).thenReturn(const ProfileAvatarState.loaded(profile: mockUserProfile));
when(profileAvatarCubit.stream).thenAnswer((_) => const Stream.empty());
when(profileAvatarCubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(
MultiBlocProvider(
providers: [
BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
BlocProvider<ChatsCubit>.value(value: chatsCubit),
BlocProvider<ProfileAvatarCubit>.value(value: profileAvatarCubit),
],
child: wrap(
const Scaffold(
body: BottomNavigationBarWidget(profileModel: mockUserProfile, child: Text('Test Content')),
body: BottomNavigationBarWidget(child: Text('Test Content')),
),
),
),
@@ -117,6 +124,7 @@ void main() {
mockNetworkImagesFor(() async {
final notificationsCubit = MockNotificationsCubit();
final chatsCubit = MockChatsCubit();
final profileAvatarCubit = MockProfileAvatarCubit();
when(notificationsCubit.state).thenReturn(const NotificationsState());
when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty());
@@ -124,16 +132,20 @@ void main() {
when(chatsCubit.state).thenReturn(ChatsState());
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(profileAvatarCubit.state).thenReturn(const ProfileAvatarState.loaded(profile: mockConsultantProfile));
when(profileAvatarCubit.stream).thenAnswer((_) => const Stream.empty());
when(profileAvatarCubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(
MultiBlocProvider(
providers: [
BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
BlocProvider<ChatsCubit>.value(value: chatsCubit),
BlocProvider<ProfileAvatarCubit>.value(value: profileAvatarCubit),
],
child: wrap(
const Scaffold(
body: BottomNavigationBarWidget(profileModel: mockConsultantProfile, child: Text('Test Content')),
body: BottomNavigationBarWidget(child: Text('Test Content')),
),
),
),