part of '../app/app.dart'; class ApiClient { ApiClient({required this.baseUrl, this.token}); final String baseUrl; final String? token; Uri _uri(String path, [Map? query]) { final normalizedBase = normalizeBaseUrl(baseUrl); final base = Uri.parse(normalizedBase); return base.replace( path: '${base.path.replaceAll(RegExp(r'/$'), '')}/$path', queryParameters: query, ); } Map get _headers => { 'Accept': 'application/json', 'Content-Type': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }; Future> post( String path, Map body, ) async { final uri = _uri(path); final client = _clientFor(uri); try { final response = await client .post(uri, headers: _headers, body: jsonEncode(body)) .timeout(const Duration(seconds: 15)); return _decode(response, uri); } catch (error, stackTrace) { throw _connectionException(uri, error, stackTrace); } finally { client.close(); } } Future> get( String path, [ Map? query, ]) async { final uri = _uri(path, query); final client = _clientFor(uri); try { final response = await client .get(uri, headers: _headers) .timeout(const Duration(seconds: 15)); return _decode(response, uri); } catch (error, stackTrace) { throw _connectionException(uri, error, stackTrace); } finally { client.close(); } } http.Client _clientFor(Uri uri) { final client = HttpClient() ..badCertificateCallback = (certificate, host, port) { debugPrint( 'MyLIS menerima sertifikat internal untuk $host:$port ' 'issuer=${certificate.issuer}', ); return true; }; return IOClient(client); } Map _decode(http.Response response, Uri uri) { final bodyPreview = response.body.length > 240 ? '${response.body.substring(0, 240)}...' : response.body; if (response.statusCode < 200 || response.statusCode >= 300) { Object? decoded; if (_looksLikeJson(response)) { try { decoded = response.body.isEmpty ? {} : jsonDecode(response.body); } catch (_) { decoded = null; } } final message = decoded is Map ? decoded['message'] : null; debugPrint( 'MyLIS API gagal: ${response.request?.method ?? '-'} $uri ' 'HTTP ${response.statusCode}; body=$bodyPreview', ); throw ApiException( message?.toString() ?? 'Server mengembalikan HTTP ${response.statusCode}. ' 'URL: $uri. Kemungkinan request masuk ke virtual host/proxy yang salah. ' 'Detail awal respons: $bodyPreview', ); } final Object decoded; try { decoded = response.body.isEmpty ? {} : jsonDecode(response.body); } catch (error, stackTrace) { debugPrint( 'MyLIS API decode gagal: ${response.request?.method ?? '-'} $uri ' 'HTTP ${response.statusCode}; error=$error', ); debugPrintStack(stackTrace: stackTrace); throw ApiException( 'Respons server tidak bisa dibaca. ' 'URL: $uri. Status HTTP: ${response.statusCode}.', ); } if (response.statusCode >= 200 && response.statusCode < 300) { final cleanDecoded = _decodeHtmlEntitiesIn(decoded); return cleanDecoded is Map ? cleanDecoded : {'data': cleanDecoded}; } return {'data': _decodeHtmlEntitiesIn(decoded)}; } bool _looksLikeJson(http.Response response) { final contentType = response.headers['content-type']?.toLowerCase() ?? ''; final body = response.body.trimLeft(); return contentType.contains('json') || body.startsWith('{') || body.startsWith('['); } ApiException _connectionException( Uri uri, Object error, StackTrace stackTrace, ) { if (error is ApiException) { return error; } final reason = _connectionReason(error); debugPrint('MyLIS koneksi gagal: $uri; $reason; error=$error'); debugPrintStack(stackTrace: stackTrace); return ApiException('Koneksi server gagal. URL: $uri. Penyebab: $reason'); } String _connectionReason(Object error) { if (error is TimeoutException) { return 'timeout lebih dari 15 detik. Periksa jaringan, DNS lokal, atau server Laravel belum aktif.'; } if (error is http.ClientException) { final message = error.message.toLowerCase(); if (message.contains('certificate') || message.contains('handshake') || message.contains('tls')) { return 'sertifikat HTTPS lokal belum dipercaya oleh device/simulator.'; } if (message.contains('failed host lookup') || message.contains('nodename') || message.contains('name or service')) { return 'host tidak ditemukan. Periksa URL atau DNS/hosts lokal.'; } if (message.contains('connection refused')) { return 'server menolak koneksi. Periksa apakah Laravel/web server aktif.'; } if (message.contains('connection closed')) { return 'koneksi ditutup oleh server.'; } return 'client HTTP gagal: ${error.message}'; } if (error is FormatException) { return 'format URL atau respons tidak valid: ${error.message}'; } return error.toString(); } Object? _decodeHtmlEntitiesIn(Object? value) { if (value is String) { return decodeHtmlEntities(value); } if (value is List) { return value.map(_decodeHtmlEntitiesIn).toList(); } if (value is Map) { return value.map( (key, item) => MapEntry(key.toString(), _decodeHtmlEntitiesIn(item)), ); } return value; } } class ApiException implements Exception { ApiException(this.message); final String message; }