import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../config/app_config.dart'; import 'api_exception.dart'; class ApiClient { late final Dio _dio; ApiClient() { _dio = Dio( BaseOptions( baseUrl: AppConfig.apiBaseUrl, connectTimeout: const Duration(seconds: 10), // Upload triggers a synchronous OCR pass server-side that can take up to // 210s worst case (see v1/documents/upload/route.ts's AbortSignal.timeout). // Keep a safety margin above that so a legitimately slow-but-successful // parse isn't killed client-side as a false "connection timeout". receiveTimeout: const Duration(seconds: 240), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, ), ); _setupInterceptors(); } void _setupInterceptors() { // Request interceptor to append JWT token _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) async { final prefs = await SharedPreferences.getInstance(); final token = prefs.getString('auth_token'); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } return handler.next(options); }, )); // Logging interceptor for debugging _dio.interceptors.add(LogInterceptor( requestBody: true, responseBody: true, )); // Standardized Error Interceptor: wraps every failure into an ApiException // (attached as DioException.error) so call sites read one consistent shape // instead of reverse-engineering DioException.toString(). _dio.interceptors.add(InterceptorsWrapper( onError: (DioException e, handler) { final apiException = ApiException.fromDioException(e); final customError = DioException( requestOptions: e.requestOptions, response: e.response, type: e.type, error: apiException, ); return handler.next(customError); }, )); // Dummy Response Interceptor for Development // TODO: Remove this interceptor before production release // _dio.interceptors.add(DummyResponseInterceptor()); } Dio get client => _dio; } // Single shared instance so every screen/notifier reuses the same Dio HTTP client // (connection keep-alive) instead of paying a fresh TCP/TLS handshake per call site. final apiClientProvider = Provider((ref) => ApiClient());