feat(App): update Finlytic Flutter app UI and blocs
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../services/secure_storage_service.dart';
|
||||
|
||||
/// Central HTTP ApiClient backed by Dio with automatic 401 Unauthorized handling.
|
||||
class ApiClient {
|
||||
final SecureStorageService _storageService;
|
||||
late final Dio _dio;
|
||||
Function()? onUnauthorized;
|
||||
|
||||
static const String baseUrl = 'http://localhost:5000';
|
||||
|
||||
ApiClient(this._storageService) {
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
),
|
||||
);
|
||||
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
final token = await _storageService.getToken();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
return handler.next(options);
|
||||
},
|
||||
onError: (DioException error, handler) async {
|
||||
if (error.response?.statusCode == 401) {
|
||||
await _storageService.clearAll();
|
||||
onUnauthorized?.call();
|
||||
}
|
||||
return handler.next(error);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
|
||||
return await _dio.get(path, queryParameters: queryParameters);
|
||||
}
|
||||
|
||||
Future<Response> post(String path, {dynamic data}) async {
|
||||
return await _dio.post(path, data: data);
|
||||
}
|
||||
|
||||
Future<Response> put(String path, {dynamic data}) async {
|
||||
return await _dio.put(path, data: data);
|
||||
}
|
||||
|
||||
Future<Response> delete(String path) async {
|
||||
return await _dio.delete(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:signalr_core/signalr_core.dart';
|
||||
import '../services/secure_storage_service.dart';
|
||||
|
||||
/// Central Real-Time WebSocket Service utilizing SignalR (`signalr_core`).
|
||||
/// Connects persistently to `/hubs/health` and `/hubs/favorites-prices` WebSockets.
|
||||
class SignalRService extends ChangeNotifier {
|
||||
final SecureStorageService storageService;
|
||||
|
||||
HubConnection? _healthConnection;
|
||||
HubConnection? _favoritesConnection;
|
||||
|
||||
bool _isConnected = false;
|
||||
final _statusController = StreamController<bool>.broadcast();
|
||||
final _healthController = StreamController<List<Map<String, dynamic>>>.broadcast();
|
||||
final _favoritePricesController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
|
||||
bool get isConnected => _isConnected;
|
||||
Stream<bool> get connectionStream => _statusController.stream;
|
||||
Stream<List<Map<String, dynamic>>> get healthStream => _healthController.stream;
|
||||
Stream<Map<String, dynamic>> get favoritePricesStream => _favoritePricesController.stream;
|
||||
|
||||
static const String baseUrl = 'http://localhost:5000';
|
||||
|
||||
SignalRService(this.storageService);
|
||||
|
||||
Future<void> initSignalR() async {
|
||||
if (_isConnected) return;
|
||||
|
||||
try {
|
||||
final token = await storageService.getToken();
|
||||
|
||||
// 1. Connect SystemHealthHub over WebSockets
|
||||
_healthConnection = HubConnectionBuilder()
|
||||
.withUrl(
|
||||
'$baseUrl/hubs/health',
|
||||
HttpConnectionOptions(
|
||||
accessTokenFactory: () async => token,
|
||||
transport: HttpTransportType.webSockets,
|
||||
logging: (level, message) {
|
||||
if (kDebugMode) debugPrint('[SignalR Health WS] $message');
|
||||
},
|
||||
),
|
||||
)
|
||||
.withAutomaticReconnect()
|
||||
.build();
|
||||
|
||||
_healthConnection!.on('ReceiveSystemHealth', (arguments) {
|
||||
if (arguments != null && arguments.isNotEmpty) {
|
||||
try {
|
||||
final List<dynamic> list = arguments.first as List<dynamic>;
|
||||
final mappedList = list.map((item) => Map<String, dynamic>.from(item as Map)).toList();
|
||||
_healthController.add(mappedList);
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint('[SignalR Health Parsing Error] $e');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_healthConnection!.onclose((error) {
|
||||
if (kDebugMode) debugPrint('[SignalR Health WS] Closed: $error');
|
||||
});
|
||||
|
||||
await _healthConnection!.start();
|
||||
if (kDebugMode) debugPrint('[SignalR Health WS] Connected via WebSocket to /hubs/health.');
|
||||
|
||||
// 2. Connect FavoritesPriceHub over WebSockets
|
||||
_favoritesConnection = HubConnectionBuilder()
|
||||
.withUrl(
|
||||
'$baseUrl/hubs/favorites-prices',
|
||||
HttpConnectionOptions(
|
||||
accessTokenFactory: () async => token,
|
||||
transport: HttpTransportType.webSockets,
|
||||
logging: (level, message) {
|
||||
if (kDebugMode) debugPrint('[SignalR Favorites WS] $message');
|
||||
},
|
||||
),
|
||||
)
|
||||
.withAutomaticReconnect()
|
||||
.build();
|
||||
|
||||
_favoritesConnection!.on('ReceiveFavoritePrices', (arguments) {
|
||||
if (arguments != null && arguments.isNotEmpty) {
|
||||
try {
|
||||
final Map<String, dynamic> priceMap = Map<String, dynamic>.from(arguments.first as Map);
|
||||
_favoritePricesController.add(priceMap);
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint('[SignalR Favorites Parsing Error] $e');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await _favoritesConnection!.start();
|
||||
if (kDebugMode) debugPrint('[SignalR Favorites WS] Connected via WebSocket to /hubs/favorites-prices.');
|
||||
|
||||
_isConnected = true;
|
||||
_statusController.add(true);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint('[SignalR WebSocket Connection Error] $e');
|
||||
_isConnected = false;
|
||||
_statusController.add(false);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void broadcastHealthUpdate(List<Map<String, dynamic>> healthData) {
|
||||
_healthController.add(healthData);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void broadcastFavoritePrices(Map<String, dynamic> priceMap) {
|
||||
_favoritePricesController.add(priceMap);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void disconnect() async {
|
||||
try {
|
||||
await _healthConnection?.stop();
|
||||
await _favoritesConnection?.stop();
|
||||
} catch (_) {}
|
||||
_isConnected = false;
|
||||
_statusController.add(false);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disconnect();
|
||||
_statusController.close();
|
||||
_healthController.close();
|
||||
_favoritePricesController.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user