import 'package:dio/dio.dart'; import '../../core/logging/logger.dart'; import '../../core/network/api_problem.dart'; class ApiClient { ApiClient({ required String baseUrl, Future Function()? tokenProvider, Future Function()? onUnauthorized, }) : _dio = Dio( BaseOptions( baseUrl: baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 15), headers: const {'Content-Type': 'application/json'}, ), ) { if (tokenProvider != null) { _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { if (options.extra['skipAuth'] == true) { handler.next(options); return; } final token = await tokenProvider(); if (token != null && token.isNotEmpty) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); }, onError: (error, handler) async { final status = error.response?.statusCode; final authHeader = error.requestOptions.headers['Authorization'] as String?; final hasAuthHeader = authHeader != null && authHeader.isNotEmpty; final isLogoutEndpoint = error.requestOptions.method.toUpperCase() == 'DELETE' && error.requestOptions.path == '/api/v1/auth/sessions'; if (status == 401 && hasAuthHeader && onUnauthorized != null && !isLogoutEndpoint) { await onUnauthorized(); } handler.next(error); }, ), ); } } final Dio _dio; final Logger _logger = getLogger('data.network.api_client'); Dio get rawDio => _dio; Future postNoContent(String path, {Map? data}) async { try { await _dio.post(path, data: data); } on DioException catch (error, stackTrace) { _logger.error( message: 'POST no-content failed', error: error, stackTrace: stackTrace, ); throw _mapProblem(error); } } Future deleteNoContent( String path, { Map? data, }) async { try { await _dio.delete(path, data: data); } on DioException catch (error, stackTrace) { _logger.error( message: 'DELETE no-content failed', error: error, stackTrace: stackTrace, ); throw _mapProblem(error); } } Future> postJson( String path, { Map? data, }) async { try { final response = await _dio.post>(path, data: data); return response.data ?? {}; } on DioException catch (error, stackTrace) { _logger.error( message: 'POST json failed', error: error, stackTrace: stackTrace, ); throw _mapProblem(error); } } Future> getJson(String path) async { try { final response = await _dio.get>(path); return response.data ?? {}; } on DioException catch (error, stackTrace) { _logger.error( message: 'GET json failed', error: error, stackTrace: stackTrace, ); throw _mapProblem(error); } } ApiProblem _mapProblem(DioException error) { final status = error.response?.statusCode ?? 500; final data = error.response?.data; if (data is Map) { return ApiProblem( status: status, title: (data['title'] as String?) ?? 'Request failed', detail: (data['detail'] as String?) ?? '', code: data['code'] as String?, ); } return ApiProblem( status: status, title: 'Network error', detail: error.message ?? 'Request failed', ); } }