151 lines
4.7 KiB
Dart
151 lines
4.7 KiB
Dart
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<bool> _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<Response> _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<Response<T>> _request<T>(
|
|
String method,
|
|
String path, {
|
|
Map<String, dynamic>? data,
|
|
Map<String, dynamic>? queryParameters,
|
|
}) async {
|
|
try {
|
|
final response = await _dio.request<T>(
|
|
path,
|
|
data: data,
|
|
queryParameters: queryParameters,
|
|
options: Options(method: method),
|
|
);
|
|
return response;
|
|
} on DioException catch (e) {
|
|
throw _mapError(e);
|
|
}
|
|
}
|
|
|
|
Future<Response> get(String path, {Map<String, dynamic>? params}) =>
|
|
_request('GET', path, queryParameters: params);
|
|
|
|
Future<Response> post(String path, {Map<String, dynamic>? data}) =>
|
|
_request('POST', path, data: data);
|
|
|
|
Future<Response> put(String path, {Map<String, dynamic>? data}) =>
|
|
_request('PUT', path, data: data);
|
|
|
|
Future<Response> patch(String path, {Map<String, dynamic>? data}) =>
|
|
_request('PATCH', path, data: data);
|
|
|
|
Future<Response> delete(String path) => _request('DELETE', path);
|
|
|
|
Future<void> saveTokens(
|
|
String accessToken, String refreshToken) async {
|
|
await _storage.write(key: _tokenKey, value: accessToken);
|
|
await _storage.write(key: _refreshKey, value: refreshToken);
|
|
}
|
|
|
|
Future<void> clearTokens() async {
|
|
await _storage.delete(key: _tokenKey);
|
|
await _storage.delete(key: _refreshKey);
|
|
}
|
|
|
|
Future<String?> 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<ApiClient>((ref) => ApiClient());
|