fix(profile): refresh bottom-nav avatar on account switch

Reset ProfileAvatarCubit and user-scoped feed, chat and notification state so stale data from the previous account cannot be rendered. Refresh the avatar when the stored token changes and rebuild nav tabs when the profile identity or consultant role changes.

Refs: #1113
This commit is contained in:
2026-10-30 15:15:21 +03:00
parent 3a41a85f9d
commit 5f0b7e026b
8 changed files with 129 additions and 25 deletions

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
@@ -24,14 +23,10 @@ class MainNavigationRoutes {
return [
ShellRoute(
builder: (context, state, child) {
// Инициализация ProfileAvatarCubit при первом использовании ShellRoute
final profileAvatarCubit = context.read<ProfileAvatarCubit>();
if (profileAvatarCubit.state is ProfileAvatarStateLoading) {
// Вызываем getMyProfile() только если состояние loading (первый запуск)
WidgetsBinding.instance.addPostFrameCallback((_) {
profileAvatarCubit.getMyProfile();
});
}
WidgetsBinding.instance.addPostFrameCallback((_) {
profileAvatarCubit.ensureFreshForCurrentSession();
});
return _MainShellWithNavScope(currentLocation: state.matchedLocation, child: child);
},

View File

@@ -6,6 +6,13 @@ class NewMessagesCubit extends Cubit<NewMessagesState> {
NewMessagesCubit() : super(const NewMessagesState.empty());
void reset() {
_messagesCount = 0;
if (!isClosed) {
emit(const NewMessagesState.empty());
}
}
void incrementCounter() {
_messagesCount++;
emit(NewMessagesState.loaded(messagesCount: _messagesCount));

View File

@@ -1,18 +1,22 @@
import 'dart:async';
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/token_storage.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';
class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
final ProfileRepository profileRepository;
ProfileAvatarCubit({required this.profileRepository}) : super(const ProfileAvatarState.loading());
ProfileAvatarCubit({required this.profileRepository})
: super(const ProfileAvatarState.loading());
int _requestId = 0;
int? _sessionFingerprint;
bool _ensuring = false;
int _calculateAge(String birthdate) {
try {
@@ -25,7 +29,8 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
final now = DateTime.now();
int age = now.year - birthYear;
if (now.month < birthMonth || (now.month == birthMonth && now.day < birthDay)) {
if (now.month < birthMonth ||
(now.month == birthMonth && now.day < birthDay)) {
age--;
}
@@ -38,30 +43,82 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
return 0;
}
void reset() {
_requestId++;
_sessionFingerprint = null;
unawaited(AppMetrica.setUserProfileID(null).catchError((Object _) {}));
if (!isClosed) emit(const ProfileAvatarState.loading());
}
Future<void> ensureFreshForCurrentSession() async {
final token = await TokenStorage.getTokenFromStorage();
if (token == null || token.isEmpty) {
reset();
return;
}
final fingerprint = token.hashCode;
if (_sessionFingerprint == fingerprint &&
state is ProfileAvatarStateLoaded) {
return;
}
if (_ensuring) {
return;
}
_ensuring = true;
try {
await getMyProfile();
} finally {
_ensuring = false;
}
}
Future<void> getMyProfile() async {
final requestId = ++_requestId;
final token = await TokenStorage.getTokenFromStorage();
final fingerprint = token?.hashCode;
if (isClosed || requestId != _requestId) return;
emit(const ProfileAvatarState.loading());
try {
final profile = await profileRepository.getMyProfile();
if (isClosed || requestId != _requestId) return;
final profileDetails = await profileRepository.getMyProfileDetails();
if (isClosed || requestId != _requestId) return;
_sessionFingerprint = fingerprint;
emit(ProfileAvatarState.loaded(profile: profile));
try {
final notificationStatus = await Permission.notification.status;
if (isClosed || requestId != _requestId) return;
final isNotificationEnabled = notificationStatus.isGranted;
AppMetricaUserProfile userProfile = AppMetricaUserProfile([
AppMetricaNameAttribute.withValue(profile.name),
AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female),
AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)),
AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled),
AppMetricaGenderAttribute.withValue(
profileDetails.gender == 'male'
? AppMetricaGender.male
: AppMetricaGender.female,
),
AppMetricaBirthDateAttribute.withAge(
_calculateAge(profileDetails.birthdate),
),
AppMetricaNotificationEnabledAttribute.withValue(
isNotificationEnabled,
),
]);
AppMetrica.setUserProfileID(profile.slug);
AppMetrica.reportUserProfile(userProfile);
} catch (e, st) {
await reportFrom(e, st, this);
// Игнорируем ошибки плагинов, профиль уже загружен
}
} catch (e, st) {
if (isClosed || requestId != _requestId) return;
await reportFrom(e, st, this);
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
if (isClosed || requestId != _requestId) return;
emit(ProfileAvatarState.error(errorMessage: e.toString()));
}
}

View File

@@ -39,6 +39,8 @@ class BottomNavigationBarWidget extends StatefulWidget {
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
int currentBottomNavBarIndex = 1;
List<String>? pages;
int? _pagesProfileId;
bool? _pagesIsConsultant;
int tapsCount = 0;
List<String> _getPages(bool isConsultant) {
@@ -179,7 +181,16 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
final isConsultant = profileModel.roles.contains('consultant');
// Инициализируем pages на основе profileModel
pages ??= _getPages(isConsultant);
if (pages == null ||
_pagesProfileId != profileModel.id ||
_pagesIsConsultant != isConsultant) {
_pagesProfileId = profileModel.id;
_pagesIsConsultant = isConsultant;
pages = _getPages(isConsultant);
if (currentBottomNavBarIndex >= pages!.length) {
currentBottomNavBarIndex = 0;
}
}
// Expose tab switching to descendants (e.g. notification widget)
final controller = BottomNavScope.maybeOf(context);

View File

@@ -57,6 +57,17 @@ class ChatsCubit extends Cubit<ChatsState> {
int page = 1;
late int lastPage;
/// Clears all user-scoped state. WS subscriptions stay intact so that
/// `getChatRoomsWithLoading` can re-load the new user's chats.
void reset() {
_query = null;
page = 1;
lastPage = 1;
if (!isClosed) {
emit(ChatsState());
}
}
Future<void> connectToWebSocket() async {
try {
await newIncomingMessagesSubscription?.cancel();

View File

@@ -12,9 +12,19 @@ class FeedCubit extends Cubit<FeedState> {
bool _hasMore = true;
bool isLoading = false;
String? _query;
int _requestId = 0;
FeedCubit({required this.feedRepository}) : super(const FeedState.loading());
void reset() {
_requestId++;
_nextCursor = null;
_hasMore = true;
isLoading = false;
_query = null;
if (!isClosed) emit(const FeedState.loading());
}
void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) {
if (feedBlogPostModel == null || isClosed || state is! FeedStateLoaded) return;
final List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
@@ -39,17 +49,18 @@ class FeedCubit extends Cubit<FeedState> {
}
Future<void> loadFeedBlogPostList({String? query, int? currentUserId}) async {
final requestId = ++_requestId;
try {
_query = query;
_nextCursor = null;
_hasMore = true;
final feed = await feedRepository.getFeed(search: query, currentUserId: currentUserId);
if (isClosed || requestId != _requestId) return;
_nextCursor = feed.nextCursor;
_hasMore = _nextCursor != null;
if (!isClosed) {
emit(FeedState.loaded(feedBlogPostList: feed.posts, isLoading: false));
}
emit(FeedState.loaded(feedBlogPostList: feed.posts, isLoading: false));
} catch (e, st) {
if (isClosed || requestId != _requestId) return;
await reportFrom(e, st, this);
if (!isClosed) {
emit(FeedState.error(errorMessage: e.toString()));
@@ -62,14 +73,16 @@ class FeedCubit extends Cubit<FeedState> {
return;
}
final requestId = _requestId;
isLoading = true;
emit((state as FeedStateLoaded).copyWith(isLoading: true));
try {
final feed = await feedRepository.getFeed(search: _query, cursor: _nextCursor, currentUserId: currentUserId);
if (isClosed || requestId != _requestId) return;
_nextCursor = feed.nextCursor;
_hasMore = _nextCursor != null;
if (!isClosed && state is FeedStateLoaded) {
if (state is FeedStateLoaded) {
emit(
FeedState.loaded(
feedBlogPostList: [...(state as FeedStateLoaded).feedBlogPostList, ...feed.posts],
@@ -78,14 +91,15 @@ class FeedCubit extends Cubit<FeedState> {
);
}
} catch (e, st) {
if (isClosed || requestId != _requestId) return;
await reportFrom(e, st, this);
if (!isClosed && state is FeedStateLoaded) {
if (state is FeedStateLoaded) {
emit((state as FeedStateLoaded).copyWith(isLoading: false));
} else if (!isClosed) {
} else {
emit(FeedState.error(errorMessage: e.toString()));
}
} finally {
isLoading = false;
if (requestId == _requestId) isLoading = false;
}
}

View File

@@ -16,6 +16,11 @@ class NotificationsCubit extends Cubit<NotificationsState> {
StreamSubscription? _newNotificationsSubscription;
NotificationsCubit({required this.wsManager, required this.notificationsRepository}) : super(const NotificationsState.empty());
void reset() {
_notifications.clear();
if (!isClosed) emit(const NotificationsState.empty());
}
Future<void> connectToWebSocket() async {
try {
await _newNotificationsSubscription?.cancel();

View File

@@ -22,6 +22,10 @@ class ProfileCubit extends Cubit<ProfileState> {
/// Bottom-nav / own profile instance, used to refresh posts after create/update/delete.
static ProfileCubit? myProfileCubit;
static void clearMyProfileReference() {
myProfileCubit = null;
}
ProfileCubit({required this.profileRepository, required this.chatRepository, required this.consultantRepository}) : super(const ProfileState.loading());
Future<void> getProfileBySlug(String slug) async {