From cba2689406e0d3477a243f30e4dfa03e9c2a2bb1 Mon Sep 17 00:00:00 2001 From: alex-chevski Date: Fri, 30 Oct 2026 15:15:28 +0300 Subject: [PATCH] feat(session): centralize session teardown and refresh Add SessionLifecycleService to reset user-scoped cubits, analytics, socket id and account-scoped preferences on logout, 401 or account deletion. Run the same refresh after every successful auth path, including registration v1/v2, and ignore 401 responses from stale tokens so an old request cannot terminate the new user's session. Refs: #1113 --- lib/core/di/cubit_registrations.dart | 26 +++- lib/core/di/session_registrations.dart | 15 +++ lib/core/interceptors/token_interceptor.dart | 20 ++- lib/core/locator_service.dart | 3 + .../session/session_lifecycle_service.dart | 122 ++++++++++++++++++ .../bloc/auth_cubit/auth_cubit.dart | 19 ++- .../bloc/oauth_cubit/oauth_cubit.dart | 8 +- .../auth/presentation/pages/auth_screen.dart | 38 +----- .../pages/register_via_social_screen.dart | 27 +--- .../pages/scan_qr_code_screen.dart | 47 +++---- .../bloc/menu_cubit/menu_cubit.dart | 12 +- .../menu/presentation/pages/menu_screen.dart | 29 +---- .../registration_cubit.dart | 9 +- .../cubit/registration_cubit.dart | 5 +- .../bloc/settings_cubit/settings_cubit.dart | 53 +++++--- .../presentation/pages/settings_screen.dart | 1 - 16 files changed, 288 insertions(+), 146 deletions(-) create mode 100644 lib/core/di/session_registrations.dart create mode 100644 lib/core/session/session_lifecycle_service.dart diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index 05bfd7d1..8428bae8 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -97,6 +97,7 @@ class CubitRegistrations { systemPipCoordinator: sl(), ), ); + sl.registerLazySingleton(() => FeedCubit(feedRepository: sl())); } /// Регистрация Cubit'ов как фабрик @@ -108,10 +109,15 @@ class CubitRegistrations { firebaseApi: sl(), prefs: sl(), profileRepository: sl(), + sessionLifecycleService: sl(), ), ); sl.registerFactory( - () => RegistrationCubit(registrationRepository: sl(), prefs: sl()), + () => RegistrationCubit( + registrationRepository: sl(), + prefs: sl(), + sessionLifecycleService: sl(), + ), ); sl.registerFactory( () => ResetPasswordCubit(resetPasswordRepository: sl()), @@ -120,14 +126,21 @@ class CubitRegistrations { () => VerifyEmailCubit(profileRepository: sl(), prefs: sl()), ); sl.registerFactory( - () => OAuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl()), + () => OAuthCubit( + authRepository: sl(), + firebaseApi: sl(), + prefs: sl(), + sessionLifecycleService: sl(), + ), ); sl.registerFactory( - () => - RegistrationCubitV2(registrationRepository: sl(), firebaseApi: sl()), + () => RegistrationCubitV2( + registrationRepository: sl(), + firebaseApi: sl(), + sessionLifecycleService: sl(), + ), ); // Feed related cubits - sl.registerFactory(() => FeedCubit(feedRepository: sl())); sl.registerFactory( () => RecipesCubit( feedRepository: sl(), @@ -242,6 +255,7 @@ class CubitRegistrations { settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl(), + sessionLifecycleService: sl(), ), ); sl.registerFactory( @@ -297,7 +311,7 @@ class CubitRegistrations { sl.registerFactory( () => UserDetailsCubit(profileRepository: sl()), ); - sl.registerFactory(() => MenuCubit(sl())); + sl.registerFactory(() => MenuCubit(sl(), sl())); sl.registerFactory(() => MarketingCubit(sl())); sl.registerFactory( () => PersonalDietSuggestionCubit( diff --git a/lib/core/di/session_registrations.dart b/lib/core/di/session_registrations.dart new file mode 100644 index 00000000..04e27f60 --- /dev/null +++ b/lib/core/di/session_registrations.dart @@ -0,0 +1,15 @@ +import 'package:eatvio_app/core/error_reporting/error_reporter.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/session/session_lifecycle_service.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; + +class SessionRegistrations { + static void registerAll() { + sl.registerLazySingleton( + () => SessionLifecycleService( + wsManager: sl(), + errorReporter: sl(), + ), + ); + } +} diff --git a/lib/core/interceptors/token_interceptor.dart b/lib/core/interceptors/token_interceptor.dart index 05f6be0c..2d65d04f 100644 --- a/lib/core/interceptors/token_interceptor.dart +++ b/lib/core/interceptors/token_interceptor.dart @@ -1,9 +1,7 @@ import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/session/session_lifecycle_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; -import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/locator_service.dart'; -import 'package:go_router/go_router.dart'; class TokenInterceptor extends Interceptor { @override @@ -14,6 +12,7 @@ class TokenInterceptor extends Interceptor { final token = await TokenStorage.getTokenFromStorage(); if (token != null && token.isNotEmpty) { options.headers['Authorization'] = 'Bearer $token'; + options.extra['authToken'] = token; } super.onRequest(options, handler); @@ -25,9 +24,18 @@ class TokenInterceptor extends Interceptor { ErrorInterceptorHandler handler, ) async { if (err.response?.statusCode == 401) { - await sl().clear(); - await TokenStorage.removeTokenInStorage(); - AppRouter.navigatorKey.currentContext?.go('/auth-screen'); + final requestCarriedAuth = err.requestOptions.headers.containsKey( + 'Authorization', + ); + final requestToken = err.requestOptions.extra['authToken']; + final currentToken = await TokenStorage.getTokenFromStorage(); + if (requestCarriedAuth && + requestToken != null && + requestToken == currentToken) { + await sl().endSession( + reason: SessionEndReason.unauthorized, + ); + } } super.onError(err, handler); } diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index cb2e63cc..46128cd3 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -5,6 +5,7 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart'; import 'package:eatvio_app/core/di/external_registrations.dart'; import 'package:eatvio_app/core/di/repository_registrations.dart'; import 'package:eatvio_app/core/di/service_registrations.dart'; +import 'package:eatvio_app/core/di/session_registrations.dart'; import 'package:eatvio_app/core/prefs.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:get_it/get_it.dart'; @@ -38,4 +39,6 @@ Future init({required AppBuildType buildType}) async { sl.registerLazySingleton( () => WsManager(wsService: sl(), errorReporter: sl()), ); + + SessionRegistrations.registerAll(); } diff --git a/lib/core/session/session_lifecycle_service.dart b/lib/core/session/session_lifecycle_service.dart new file mode 100644 index 00000000..1019a338 --- /dev/null +++ b/lib/core/session/session_lifecycle_service.dart @@ -0,0 +1,122 @@ +import 'dart:async'; + +import 'package:appmetrica_plugin/appmetrica_plugin.dart'; +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/error_reporting/error_reporter.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/prefs.dart'; +import 'package:eatvio_app/core/prefs_keys.dart'; +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; +import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart'; +import 'package:go_router/go_router.dart'; + +enum SessionEndReason { logout, unauthorized, accountDeleted } + +class SessionLifecycleService { + SessionLifecycleService({ + required WsManager wsManager, + required ErrorReporter errorReporter, + }) : _wsManager = wsManager, + _errorReporter = errorReporter; + + final WsManager _wsManager; + final ErrorReporter _errorReporter; + + int? _authenticatedFingerprint; + bool _endingSession = false; + + Future endSession({required SessionEndReason reason}) async { + if (_endingSession) return; + _endingSession = true; + try { + await _run(() => _wsManager.clear()); + await _run(TokenStorage.removeTokenInStorage); + await _run(SocketIdStorage.removeSocketIdInStorage); + await _run(() => sl().reset()); + await _run(() => sl().reset()); + await _run(() => sl().reset()); + await _run(() => sl().reset()); + await _run(() => sl().reset()); + await _run(() async => ProfileCubit.clearMyProfileReference()); + await _run(() => sl().endCall()); + await _run(() async => AppMetrica.setUserProfileID(null)); + await _run( + () => sl().remove(PrefsKeys.isEmailVerificationSkipped), + ); + await _run(() => sl().remove(PrefsKeys.registrationDate)); + _authenticatedFingerprint = null; + await _run(() async { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + final path = + AppRouter.appRouter.routeInformationProvider.value.uri.path; + if (path != RouteConstants.authScreen) { + context.go(RouteConstants.authScreen); + } + }); + } finally { + _endingSession = false; + } + } + + Future onAuthenticated() async { + String? token; + try { + token = await TokenStorage.getTokenFromStorage(); + } catch (e, st) { + await _report(e, st); + return; + } + if (token == null || token.isEmpty) return; + final fingerprint = token.hashCode; + if (_authenticatedFingerprint == fingerprint) return; + _authenticatedFingerprint = fingerprint; + + await _run(() async { + await _wsManager.clear(); + await _wsManager.initialize(); + }); + await _run(() => sl().getMyProfile()); + await _run(() => sl().getChatRoomsWithLoading()); + await _run(() => sl().getNotifications()); + await _run(() => sl().registerPushToken()); + } + + Future logout() async { + try { + await _run(() => sl().unregisterPushTokens()); + await _run( + () => sl().post('${sl().baseUrl}/api/profile/logout'), + ); + } finally { + await endSession(reason: SessionEndReason.logout); + } + } + + Future _run(FutureOr Function() action) async { + try { + await action(); + } catch (e, st) { + await _report(e, st); + } + } + + Future _report(Object error, StackTrace stackTrace) { + return _errorReporter.report( + error, + stackTrace, + origin: 'SessionLifecycleService', + ); + } +} 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 f36f43cf..98865cf2 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/prefs.dart'; import 'package:eatvio_app/core/prefs_keys.dart'; +import 'package:eatvio_app/core/session/session_lifecycle_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; @@ -17,15 +18,26 @@ class AuthCubit extends Cubit { final ProfileRepository profileRepository; final FirebaseApi firebaseApi; final Prefs prefs; + final SessionLifecycleService sessionLifecycleService; - AuthCubit({required this.authRepository, required this.prefs, required this.profileRepository, required this.firebaseApi}) : super(const AuthState()); + AuthCubit({ + required this.authRepository, + required this.prefs, + required this.profileRepository, + required this.firebaseApi, + required this.sessionLifecycleService, + }) : super(const AuthState()); Future getTokenByCredentials(String email, String password) async { emit(const AuthState.loading()); try { - final token = (await authRepository.getTokenByCredentials(email, password)).token; + final token = (await authRepository.getTokenByCredentials( + email, + password, + )).token; emit(AuthState.loaded(token: token)); await TokenStorage.saveTokenInStorage(token); + await sessionLifecycleService.onAuthenticated(); return true; } catch (e, st) { await reportFrom(e, st, this); @@ -48,7 +60,8 @@ class AuthCubit extends Cubit { Future registerAnonymousPushToken() async { try { - final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false; + final hasAnonymousTokenBeenSent = + prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false; if (!hasAnonymousTokenBeenSent) { await firebaseApi.registerAnonymousPushToken(); prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true); 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 f6f27797..22193998 100644 --- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -3,6 +3,7 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart'; import 'package:eatvio_app/core/prefs.dart'; +import 'package:eatvio_app/core/session/session_lifecycle_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; @@ -13,10 +14,12 @@ import 'package:eatvio_app/core/error_reporting/report_from.dart'; class OAuthCubit extends Cubit with RegistrationDateCacheMixin { final AuthRepository authRepository; final FirebaseApi firebaseApi; + final SessionLifecycleService sessionLifecycleService; OAuthCubit({ required this.authRepository, required this.firebaseApi, + required this.sessionLifecycleService, required Prefs prefs, }) : super(const OAuthState()) { this.prefs = prefs; @@ -40,6 +43,7 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin { log('OAuthCubit: Got token model: ${tokenModel.token}'); await TokenStorage.saveTokenInStorage(tokenModel.token); + await sessionLifecycleService.onAuthenticated(); emit(OAuthState.loaded(token: tokenModel.token)); } on DioException catch (e, st) { await reportFrom(e, st, this); @@ -122,7 +126,7 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin { clientId: clientId, ); await TokenStorage.saveTokenInStorage(token); - await firebaseApi.registerPushToken(); + await sessionLifecycleService.onAuthenticated(); cacheRegistrationDate(DateTime.now()); emit(OAuthState.loaded(token: token)); } on DioException catch (e, st) { @@ -241,7 +245,7 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin { 'OAuthCubit: OAuth verification confirmed successfully, token received', ); await TokenStorage.saveTokenInStorage(response.token); - await firebaseApi.registerPushToken(); + await sessionLifecycleService.onAuthenticated(); emit(OAuthState.loaded(token: response.token)); } catch (e, st) { await reportFrom(e, st, this); diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index d1c1ccfd..80442c1f 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -4,12 +4,10 @@ import 'package:eatvio_app/app_keys.dart'; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; -import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; @@ -18,8 +16,6 @@ import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cub import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:eatvio_app/core/di/cubit_registrations.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/cupertino.dart'; @@ -68,23 +64,8 @@ class _AuthScreenState extends State { super.dispose(); } - Future _reconnectWebSocket(BuildContext context) async { - try { - await sl().clear(); - await sl().initialize(); - } catch (e) { - rethrow; - } - if (context.mounted) { - try { - await sl().getMyProfile(); - await sl().getChatRoomsWithLoading(); - await sl().registerPushToken(); - showSuccessToast('Успешный вход'); - } catch (e) { - rethrow; - } - } + Future _reconnectWebSocket() async { + showSuccessToast('Успешный вход'); AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); } @@ -202,18 +183,7 @@ class _AuthScreenState extends State { } Future _reconnectAfterAuth() async { - final context = AppRouter.navigatorKey.currentContext; - if (context == null) return; - try { - await sl().clear(); - await sl().initialize(); - await sl().getMyProfile(); - await sl().getChatRoomsWithLoading(); - await sl().registerPushToken(); - showSuccessToast('Успешный вход'); - } catch (e) { - log(e.toString()); - } + showSuccessToast('Успешный вход'); AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); } @@ -395,7 +365,7 @@ class _AuthScreenState extends State { final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text); if (isAuthenticated) { if (context.mounted) { - await _reconnectWebSocket(context); + await _reconnectWebSocket(); } } setState(() { diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index 1ea307f0..9aed6116 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -9,11 +9,7 @@ import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_sta import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/locator_service.dart'; -import 'package:eatvio_app/core/websockets/ws_manager.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; -import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/utils/validation_utils.dart'; import 'package:flutter/gestures.dart'; @@ -62,23 +58,8 @@ class _RegisterViaSocialScreenState extends State { _initializeFieldsFromAuthResult(); } - Future _reconnectWebSocket(BuildContext context) async { - try { - await sl().clear(); - await sl().initialize(); - } catch (e) { - rethrow; - } - if (context.mounted) { - try { - await sl().getMyProfile(); - await sl().getChatRoomsWithLoading(); - await sl().registerPushToken(); - showSuccessToast('Успешная регистрация'); - } catch (e) { - rethrow; - } - } + Future _reconnectWebSocket() async { + showSuccessToast('Успешная регистрация'); AppRouter.appRouter.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); } @@ -451,7 +432,7 @@ class _RegisterViaSocialScreenState extends State { Builder( builder: (context) { WidgetsBinding.instance.addPostFrameCallback((_) { - _reconnectWebSocket(context); + _reconnectWebSocket(); }); return const SizedBox.shrink(); }, @@ -795,7 +776,7 @@ class _RegisterViaSocialScreenState extends State { // Перезагружаем веб-сокет и переходим на главный экран if (widgetContext.mounted) { - _reconnectWebSocket(widgetContext); + _reconnectWebSocket(); } } catch (e, stackTrace) { log('OAuth Verification Confirm Error: $e', error: e, stackTrace: stackTrace); diff --git a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart index 4b90fe70..f4a1dd56 100644 --- a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart +++ b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart @@ -3,13 +3,10 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/session/session_lifecycle_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; -import 'package:eatvio_app/core/websockets/ws_manager.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; @@ -39,7 +36,10 @@ class _ScanQRCodeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Сканирование QR-кода'), centerTitle: true), + appBar: AppBar( + title: const Text('Сканирование QR-кода'), + centerTitle: true, + ), body: Column( children: [ Expanded( @@ -60,30 +60,33 @@ class _ScanQRCodeScreenState extends State { try { final scannedUrl = scanData.code; final uri = Uri.parse(scannedUrl ?? ''); - final scannedBaseUrl = (uri.hasAuthority) ? '${uri.scheme}://${uri.host}' : ''; + final scannedBaseUrl = (uri.hasAuthority) + ? '${uri.scheme}://${uri.host}' + : ''; sl().setBaseUrl(scannedBaseUrl); sl().options.baseUrl = scannedBaseUrl; - final response = await Dio().getUri(uri, options: Options(headers: {'accept': 'application/json', 'content-type': 'application/json'})); + final response = await Dio().getUri( + uri, + options: Options( + headers: { + 'accept': 'application/json', + 'content-type': 'application/json', + }, + ), + ); final token = response.data['token']; await TokenStorage.saveTokenInStorage(token); - try { - await sl().clear(); - await sl().initialize(); - } catch (e) { - rethrow; - } - try { - await sl().getMyProfile(); - await sl().getChatRoomsWithLoading(); - await sl().registerPushToken(); - } catch (e) { - rethrow; - } - AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); + await sl().onAuthenticated(); + AppRouter.navigatorKey.currentContext?.go( + '/feed-screen', + extra: {'needToInitializeWebSocket': false}, + ); showSuccessToast('Добро пожаловать!'); } catch (e) { await controller.resumeCamera(); - showErrorToast('Что-то пошло не так. Попробуйте обновить страницу с QR-кодом или войдите с помощью логина и пароля.'); + showErrorToast( + 'Что-то пошло не так. Попробуйте обновить страницу с QR-кодом или войдите с помощью логина и пароля.', + ); } }); } 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 3d5b94ed..c9e80d18 100644 --- a/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart +++ b/lib/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:eatvio_app/core/session/session_lifecycle_service.dart'; 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'; @@ -7,7 +8,11 @@ import 'package:eatvio_app/core/error_reporting/report_from.dart'; class MenuCubit extends Cubit { final ProfileRepository profileRepository; - MenuCubit(this.profileRepository) : super(const MenuState.initial()); + final SessionLifecycleService sessionLifecycleService; + MenuCubit(this.profileRepository, this.sessionLifecycleService) + : super(const MenuState.initial()); + + Future logout() => sessionLifecycleService.logout(); Future getMyProfile() async { emit(const MenuState.loading()); @@ -23,7 +28,10 @@ class MenuCubit extends Cubit { Future makePassKey() async { try { - final passKey = await profileRepository.getPassKey(routeName: 'product_nutritional_value.index', routeParams: {}); + final passKey = await profileRepository.getPassKey( + routeName: 'product_nutritional_value.index', + routeParams: {}, + ); return passKey; } 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 84d78dbb..ba53911f 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -1,22 +1,15 @@ -import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/app_config.dart'; -import 'package:eatvio_app/core/error_reporting/error_reporter.dart'; import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/firebase_api.dart'; 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'; import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; 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/video_calls/presentation/bloc/video_call_session/video_call_session_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'; @@ -155,6 +148,7 @@ class MenuScreen extends StatelessWidget { Future Function() _onTapLogout(BuildContext context) { return () async { + final menuCubit = context.read(); showDialog( context: context, builder: (context) => CupertinoAlertDialog( @@ -162,25 +156,8 @@ class MenuScreen extends StatelessWidget { actions: [ CupertinoDialogAction( onPressed: () async { - try { - // Logout revokes the Sanctum token. Unregister first, or - // FCM / VoIP tokens stay on the server and calls still ring. - await sl().unregisterPushTokens(); - } catch (e, s) { - await sl().report(e, s, origin: 'MenuScreen'); - } - try { - await sl().post('${sl().baseUrl}/api/profile/logout'); - } catch (e, s) { - await sl().report(e, s, origin: 'MenuScreen'); - } finally { - await sl().endCall(); - await sl().clear(); - await TokenStorage.removeTokenInStorage(); - if (context.mounted) { - GoRouter.of(context).go('/auth-screen', extra: true); - } - } + Navigator.of(context).pop(); + await menuCubit.logout(); }, isDestructiveAction: true, child: const Text("Выйти"), 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 e9260cfd..d02ec877 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart @@ -1,11 +1,9 @@ import 'dart:developer'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/firebase_api.dart'; -import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart'; import 'package:eatvio_app/core/prefs.dart'; - +import 'package:eatvio_app/core/session/session_lifecycle_service.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'; @@ -18,7 +16,8 @@ import 'package:go_router/go_router.dart'; class RegistrationCubit extends Cubit with RegistrationDateCacheMixin { final RegistrationRepository registrationRepository; - RegistrationCubit({required this.registrationRepository, required Prefs prefs}) + final SessionLifecycleService sessionLifecycleService; + RegistrationCubit({required this.registrationRepository, required this.sessionLifecycleService, required Prefs prefs}) : super( const RegistrationState( sheet: UserRegistrationSheet( @@ -62,7 +61,7 @@ class RegistrationCubit extends Cubit with RegistrationDateCa state.sheet.copyWith(phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''), responseBody: '{}'), ); await TokenStorage.saveTokenInStorage(token); - await sl().registerPushToken(); + await sessionLifecycleService.onAuthenticated(); RegistrationAnalyticsService.reportRegistrationComplete(); cacheRegistrationDate(DateTime.now()); AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen'); diff --git a/lib/features/registration_v2/cubit/registration_cubit.dart b/lib/features/registration_v2/cubit/registration_cubit.dart index 8dee14d3..9246a5b1 100644 --- a/lib/features/registration_v2/cubit/registration_cubit.dart +++ b/lib/features/registration_v2/cubit/registration_cubit.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/session/session_lifecycle_service.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'; @@ -15,9 +16,11 @@ import 'package:gpassword/gpassword.dart'; class RegistrationCubitV2 extends Cubit { final RegistrationRepositoryV2 registrationRepository; final FirebaseApi firebaseApi; + final SessionLifecycleService sessionLifecycleService; RegistrationCubitV2({ required this.registrationRepository, required this.firebaseApi, + required this.sessionLifecycleService, }) : super(const RegistrationStateV2()); void setFirstName(String firstName) { @@ -204,7 +207,7 @@ class RegistrationCubitV2 extends Cubit { ); if (token != null) { await TokenStorage.saveTokenInStorage(token); - await firebaseApi.registerPushToken(); + await sessionLifecycleService.onAuthenticated(); emit(state.copyWith(registrationError: null)); return (true, false); } 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 f468a1ce..a8479ffc 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart @@ -1,3 +1,5 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/session/session_lifecycle_service.dart'; import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; @@ -17,12 +19,14 @@ class SettingsCubit extends Cubit { final SettingsRepository settingsRepository; final ConsultantRepository consultantRepository; final ConsultationsRepository consultationsRepository; + final SessionLifecycleService sessionLifecycleService; SettingsCubit({ - required this.profileRepository, - required this.settingsRepository, + required this.profileRepository, + required this.settingsRepository, required this.consultantRepository, required this.consultationsRepository, + required this.sessionLifecycleService, }) : super(const SettingsState.loading()); Future init() async { @@ -30,10 +34,12 @@ class SettingsCubit extends Cubit { late final ConsultationRequestModel? consultantInfo; late final bool hasActiveConsultation; late final OAuthAccountsDataModel? oauthAccounts; - + try { if (profile.phoneConfirmationState.contains('confirmed')) { - consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug); + consultantInfo = await consultantRepository.getMyRequestStatus( + profile.slug, + ); } else { consultantInfo = null; } @@ -41,14 +47,14 @@ class SettingsCubit extends Cubit { await reportFrom(e, st, this); consultantInfo = null; } - + try { hasActiveConsultation = await _checkActiveConsultation(); } catch (e, st) { await reportFrom(e, st, this); hasActiveConsultation = false; } - + try { final oauthResponse = await settingsRepository.getOAuthAccounts(); oauthAccounts = oauthResponse.data; @@ -56,7 +62,7 @@ class SettingsCubit extends Cubit { await reportFrom(e, st, this); oauthAccounts = null; } - + try { emit( SettingsState.loaded( @@ -74,9 +80,13 @@ class SettingsCubit extends Cubit { } } - Future updateProfileDetails({required ProfileDetailsModel profileDetailsModel}) async { + Future updateProfileDetails({ + required ProfileDetailsModel profileDetailsModel, + }) async { try { - await settingsRepository.updateProfileDetails(profileDetailsModel: profileDetailsModel); + await settingsRepository.updateProfileDetails( + profileDetailsModel: profileDetailsModel, + ); } catch (e, st) { await reportFrom(e, st, this); } finally { @@ -84,9 +94,13 @@ class SettingsCubit extends Cubit { } } - Future updateProfileSettings({required ProfileSettingsModel profileSettingsModel}) async { + Future updateProfileSettings({ + required ProfileSettingsModel profileSettingsModel, + }) async { try { - await settingsRepository.updateProfileSettings(profileSettingsModel: profileSettingsModel); + await settingsRepository.updateProfileSettings( + profileSettingsModel: profileSettingsModel, + ); } catch (e, st) { await reportFrom(e, st, this); } finally { @@ -97,9 +111,15 @@ class SettingsCubit extends Cubit { Future deleteAccount() async { try { await profileRepository.deleteAccount(); + await sessionLifecycleService.endSession( + reason: SessionEndReason.accountDeleted, + ); return true; } catch (e, st) { await reportFrom(e, st, this); + if (e is DioException && e.response?.statusCode == 401) { + return true; + } return false; } } @@ -135,11 +155,14 @@ class SettingsCubit extends Cubit { Future _checkActiveConsultation() async { try { - final userPurchases = await consultationsRepository.getUserPurchases(null, null); + final userPurchases = await consultationsRepository.getUserPurchases( + null, + null, + ); final activeStates = ChosenPlanStateEnum.getActiveStates(); - - return userPurchases.any((purchase) => - activeStates.contains(purchase.chosenPlanState) + + return userPurchases.any( + (purchase) => activeStates.contains(purchase.chosenPlanState), ); } catch (e, st) { await reportFrom(e, st, this); diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index a1b0acab..1f110d2b 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -698,7 +698,6 @@ class _DeleteAccountBottomSheet extends State { onPressed: () async { final result = await widget.cubit.deleteAccount(); if (result == true) { - AppRouter.navigatorKey.currentContext?.go('/auth-screen'); return; } AppRouter.navigatorKey.currentContext?.pop();