From 05b9ca17c7e1f1e58dc874fd743ba2bd3f76763d Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 29 Jan 2026 15:00:04 +0300 Subject: [PATCH] feat: Introduce token interceptor for authentication, WebSocket manager for real-time updates, and a new menu screen. --- lib/core/constants/app_constants.dart | 5 +++ lib/core/di/external_registrations.dart | 19 ++++++----- lib/core/interceptors/token_interceptor.dart | 3 ++ lib/core/websockets/ws_manager.dart | 33 ++++++++++++++----- .../menu/presentation/pages/menu_screen.dart | 2 ++ 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart index f9c945c9..317ddb94 100644 --- a/lib/core/constants/app_constants.dart +++ b/lib/core/constants/app_constants.dart @@ -69,4 +69,9 @@ class AppConstants { static const String usersEndpoint = '/users'; static const String postsEndpoint = '/posts'; static const String commentsEndpoint = '/comments'; + + // Delay of pending requests + /// 30 секунд стоит потому что запрос может идти 12-15 секунд + /// Особенно если производится отладка на сервере разработчика + static const Duration pendingDelay = Duration(seconds: 30); } diff --git a/lib/core/di/external_registrations.dart b/lib/core/di/external_registrations.dart index aecd2707..2e60e650 100644 --- a/lib/core/di/external_registrations.dart +++ b/lib/core/di/external_registrations.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/constants/app_constants.dart'; import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; import 'package:eatvio_app/core/prefs.dart'; @@ -19,15 +20,15 @@ class ExternalRegistrations { /// Регистрация Dio HTTP клиента static void _registerDio() { sl.registerLazySingleton( - () => - Dio(BaseOptions( - baseUrl: sl().baseUrl, - headers: {'accept': 'application/json', 'content-type': 'application/json'}, - connectTimeout: const Duration(seconds: 10), - receiveTimeout: const Duration(seconds: 10), - sendTimeout: const Duration(seconds: 10), - )) - ..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]), + () => Dio( + BaseOptions( + baseUrl: sl().baseUrl, + headers: {'accept': 'application/json', 'content-type': 'application/json'}, + connectTimeout: AppConstants.pendingDelay, + receiveTimeout: AppConstants.pendingDelay, + sendTimeout: AppConstants.pendingDelay, + ), + )..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]), ); } diff --git a/lib/core/interceptors/token_interceptor.dart b/lib/core/interceptors/token_interceptor.dart index b2cd9207..05f6be0c 100644 --- a/lib/core/interceptors/token_interceptor.dart +++ b/lib/core/interceptors/token_interceptor.dart @@ -1,6 +1,8 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/app_router.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 { @@ -23,6 +25,7 @@ class TokenInterceptor extends Interceptor { ErrorInterceptorHandler handler, ) async { if (err.response?.statusCode == 401) { + await sl().clear(); await TokenStorage.removeTokenInStorage(); AppRouter.navigatorKey.currentContext?.go('/auth-screen'); } diff --git a/lib/core/websockets/ws_manager.dart b/lib/core/websockets/ws_manager.dart index 24430f6c..04ebfcd1 100644 --- a/lib/core/websockets/ws_manager.dart +++ b/lib/core/websockets/ws_manager.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:developer'; @@ -22,6 +23,10 @@ class WsManager { PrivateChannel? _privateMessagesChannel; PrivateChannel? _privateDirectChatChannel; + StreamSubscription? _lifecycleSubscription; + StreamSubscription? _eventSubscription; + StreamSubscription? _connectionEstablishedSubscription; + Stream? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated'); Stream? get newNotificationsStream => _privateNotificationChannel?.bind('Illuminate\\Notifications\\Events\\BroadcastNotificationCreated'); @@ -33,7 +38,7 @@ class WsManager { Future initialize() async { if (client == null) { try { - await TokenStorage.haveTokenInStorage(); + if (!await TokenStorage.haveTokenInStorage()) return; final wsUrl = await wsService.getWebsocket(); final options = PusherChannelsOptions.fromHost( @@ -71,7 +76,7 @@ class WsManager { } } - client?.eventStream.listen((event) async { + _connectionEstablishedSubscription = client?.eventStream.listen((event) async { if (event.name == 'pusher:connection_established') { final data = event.data; final socketId = extractSocketId(data); @@ -87,18 +92,22 @@ class WsManager { await _initPrivateNotificationsChannel(); await _initPrivateMessagesChannel(); - client?.lifecycleStream.listen((data) { + _lifecycleSubscription = client?.lifecycleStream.listen((data) { if (data == PusherChannelsClientLifeCycleState.establishedConnection) { _unsubscribe(); _subscribe(); } }); - client?.eventStream.listen((event) { - EasyDebounce.debounce( - 'searchChatRoom', - const Duration(milliseconds: 300), - () => sl().getChatRooms(), - ); + _eventSubscription = client?.eventStream.listen((event) { + log('WsManager received event: ${event.name}'); + // Только если это не системные сообщения Pusher, обновляем список чатов + if (!event.name.startsWith('pusher:')) { + EasyDebounce.debounce( + 'searchChatRoom', + const Duration(milliseconds: 300), + () => sl().getChatRooms(), + ); + } }); _subscribe(); } catch (e, st) { @@ -166,6 +175,12 @@ class WsManager { Future clear() async { await client?.disconnect(); + await _lifecycleSubscription?.cancel(); + await _eventSubscription?.cancel(); + await _connectionEstablishedSubscription?.cancel(); + _lifecycleSubscription = null; + _eventSubscription = null; + _connectionEstablishedSubscription = null; client = null; userId = null; _privateDirectChatChannel?.unsubscribe(); diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 293f5485..05cfa2c6 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/utils/date_time_utils.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'; @@ -142,6 +143,7 @@ class MenuScreen extends StatelessWidget { try { await sl().post('${sl().baseUrl}/api/profile/logout'); } finally { + await sl().clear(); await TokenStorage.removeTokenInStorage(); if (context.mounted) { GoRouter.of(context).go('/auth-screen', extra: true);