import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/constants/app_constants.dart'; import 'endpoints.dart'; import 'exceptions.dart'; class ApiClient { late final Dio _dio; final FlutterSecureStorage _storage = const FlutterSecureStorage(); static const _tokenKey = 'access_token'; static const _refreshKey = 'refresh_token'; ApiClient() { _dio = Dio(BaseOptions( baseUrl: AppConstants.apiBaseUrl, connectTimeout: AppConstants.connectTimeout, receiveTimeout: AppConstants.receiveTimeout, headers: {'Content-Type': 'application/json'}, )); _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) async { final token = await _storage.read(key: _tokenKey); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); }, onError: (error, handler) async { if (error.response?.statusCode == 401) { final refreshed = await _tryRefresh(); if (refreshed) { final retryResponse = await _retry(error.requestOptions); handler.resolve(retryResponse); return; } } handler.next(error); }, )); } Future _tryRefresh() async { try { final refreshToken = await _storage.read(key: _refreshKey); if (refreshToken == null) return false; final response = await Dio(BaseOptions( baseUrl: AppConstants.apiBaseUrl, )).post(Endpoints.refresh, data: {'refresh_token': refreshToken}); if (response.statusCode == 200) { await _storage.write( key: _tokenKey, value: response.data['access_token']); await _storage.write( key: _refreshKey, value: response.data['refresh_token']); return true; } } catch (_) {} return false; } Future _retry(RequestOptions requestOptions) async { final token = await _storage.read(key: _tokenKey); final options = Options( method: requestOptions.method, headers: { ...requestOptions.headers, 'Authorization': 'Bearer $token', }, ); return _dio.request(requestOptions.path, data: requestOptions.data, queryParameters: requestOptions.queryParameters, options: options); } Future> _request( String method, String path, { Map? data, Map? queryParameters, }) async { try { final response = await _dio.request( path, data: data, queryParameters: queryParameters, options: Options(method: method), ); return response; } on DioException catch (e) { throw _mapError(e); } } Future get(String path, {Map? params}) => _request('GET', path, queryParameters: params); Future post(String path, {Map? data}) => _request('POST', path, data: data); Future put(String path, {Map? data}) => _request('PUT', path, data: data); Future patch(String path, {Map? data}) => _request('PATCH', path, data: data); Future delete(String path) => _request('DELETE', path); Future saveTokens( String accessToken, String refreshToken) async { await _storage.write(key: _tokenKey, value: accessToken); await _storage.write(key: _refreshKey, value: refreshToken); } Future clearTokens() async { await _storage.delete(key: _tokenKey); await _storage.delete(key: _refreshKey); } Future getAccessToken() => _storage.read(key: _tokenKey); ApiException _mapError(DioException e) { final statusCode = e.response?.statusCode; final data = e.response?.data; final message = data is Map ? data['detail']?.toString() ?? '' : ''; switch (statusCode) { case 401: return UnauthorizedException(message); case 404: return NotFoundException(message); case 422: return ValidationException( message, data is Map ? data['errors'] : null); case 403: return TariffLimitException(message); case 500: return ServerException(message); default: if (e.type == DioExceptionType.connectionTimeout || e.type == DioExceptionType.receiveTimeout || e.type == DioExceptionType.connectionError) { return NetworkException(); } return ApiException(message, statusCode: statusCode); } } } final apiClientProvider = Provider((ref) => ApiClient());