import 'dart:convert'; import 'package:http/http.dart' as http; typedef TokenProvider = String? Function(); typedef UnauthorizedHandler = Future Function(); class ApiService { final String baseUrl; final http.Client _client; final Duration timeout; TokenProvider? _getToken; UnauthorizedHandler? _onUnauthorized; ApiService({ required this.baseUrl, http.Client? client, this.timeout = const Duration(seconds: 30), }) : _client = client ?? http.Client(); void setTokenProvider(TokenProvider provider) { _getToken = provider; } void setUnauthorizedHandler(UnauthorizedHandler handler) { _onUnauthorized = handler; } void dispose() { _client.close(); } Map _buildHeaders(Map? extra) { final token = _getToken?.call(); final headers = {'accept': 'application/json', ...?extra}; if (token != null && token.isNotEmpty) { headers['Authorization'] = 'Bearer $token'; } return headers; } Future get(String endpoint, {Map? headers}) async { final response = await _client .get( Uri.parse('$baseUrl$endpoint'), headers: _buildHeaders(headers), ) .timeout(timeout); return _processResponse(response); } Future post( String endpoint, dynamic data, { Map? headers, }) async { final hasBody = data != null; final response = await _client .post( Uri.parse('$baseUrl$endpoint'), headers: _buildHeaders(hasBody ? _jsonHeaders(headers) : headers), body: hasBody ? jsonEncode(data) : null, ) .timeout(timeout); return _processResponse(response); } Future postForm(String endpoint, Map data) async { final response = await _client .post( Uri.parse('$baseUrl$endpoint'), headers: _buildHeaders({ 'Content-Type': 'application/x-www-form-urlencoded', 'accept': 'application/json', }), body: data, // http package handles form-encoding for Map ) .timeout(timeout); return _processResponse(response); } Future put( String endpoint, dynamic data, { Map? headers, }) async { final hasBody = data != null; final response = await _client .put( Uri.parse('$baseUrl$endpoint'), headers: _buildHeaders(hasBody ? _jsonHeaders(headers) : headers), body: hasBody ? jsonEncode(data) : null, ) .timeout(timeout); return _processResponse(response); } Future delete( String endpoint, { Map? headers, }) async { final response = await _client .delete( Uri.parse('$baseUrl$endpoint'), headers: _buildHeaders(headers), ) .timeout(timeout); return _processResponse(response); } Map _jsonHeaders(Map? extra) { return {'Content-Type': 'application/json', if (extra != null) ...extra}; } Future _processResponse(http.Response res) async { final body = _decodeBody(res); if (res.statusCode >= 200 && res.statusCode < 300) { return body; } if (res.statusCode == 401 && _onUnauthorized != null) { await _onUnauthorized!(); } throw Exception('API error ${res.statusCode}: $body'); } dynamic _decodeBody(http.Response res) { if (res.body.isEmpty) return null; final contentType = res.headers['content-type'] ?? ''; final shouldTryJson = contentType.contains('application/json') || contentType.contains('+json') || res.body.trimLeft().startsWith('{') || res.body.trimLeft().startsWith('['); if (!shouldTryJson) return res.body; try { return jsonDecode(res.body); } catch (_) { // Avoid turning a server-side error body into a client-side crash. return res.body; } } }