import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../constants.dart'; /// Standard error format returned by the API. class ApiError { final String code; final String message; final int statusCode; ApiError({ required this.code, required this.message, required this.statusCode, }); factory ApiError.fromResponse(Response response) { final data = response.data; if (data is Map && data.containsKey('error')) { final error = data['error'] as Map; return ApiError( code: error['code'] as String? ?? 'UNKNOWN', message: error['message'] as String? ?? 'Unknown error', statusCode: response.statusCode ?? 500, ); } return ApiError( code: 'UNKNOWN', message: 'Unexpected error', statusCode: response.statusCode ?? 500, ); } @override String toString() => 'ApiError($code: $message)'; } class ApiException implements Exception { final ApiError error; ApiException(this.error); @override String toString() => error.toString(); } class ApiClient { late final Dio _dio; String _baseUrl; ApiClient({String? baseUrl}) : _baseUrl = baseUrl ?? AppConstants.defaultBackendUrl { _dio = Dio(BaseOptions( baseUrl: _baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 30), responseType: ResponseType.json, )); _dio.interceptors.add(InterceptorsWrapper( onError: (error, handler) { if (error.response != null) { final apiError = ApiError.fromResponse(error.response!); handler.reject(DioException( requestOptions: error.requestOptions, response: error.response, error: ApiException(apiError), )); } else { handler.next(error); } }, )); } String get baseUrl => _baseUrl; void updateBaseUrl(String newUrl) { _baseUrl = newUrl; _dio.options.baseUrl = newUrl; } /// GET request returning parsed JSON. Future get( String path, { Map? queryParameters, }) async { final response = await _dio.get(path, queryParameters: queryParameters); return response.data; } /// GET request returning raw Response (for downloads with progress). Future getRaw( String path, { Map? queryParameters, ResponseType? responseType, void Function(int, int)? onReceiveProgress, Options? options, }) async { return _dio.get( path, queryParameters: queryParameters, options: options ?? Options(responseType: responseType), onReceiveProgress: onReceiveProgress, ); } /// The underlying Dio instance, for advanced use cases. Dio get dio => _dio; } /// Riverpod provider for the API client. final apiClientProvider = Provider((ref) { return ApiClient(); });