import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; // Callback set by AuthNotifier so the API client can trigger logout on 401 void Function()? onUnauthorized; const String _kBaseUrl = String.fromEnvironment( 'BASE_URL', defaultValue: 'https://jarvis.goteku.com/api/v1', ); class ApiClient { static ApiClient? _instance; late final Dio dio; final FlutterSecureStorage _storage = const FlutterSecureStorage(); ApiClient._() { dio = Dio(BaseOptions( baseUrl: _kBaseUrl, connectTimeout: const Duration(seconds: 30), receiveTimeout: const Duration(seconds: 300), // Gemini needs 300s headers: {'Content-Type': 'application/json'}, )); dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) async { final token = await _storage.read(key: 'jwt_token'); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); }, onError: (error, handler) { if (error.response?.statusCode == 401) { _storage.deleteAll(); onUnauthorized?.call(); } handler.next(error); }, )); } static ApiClient get instance => _instance ??= ApiClient._(); Dio get client => dio; } // Convenience getter Dio get apiDio => ApiClient.instance.client; class ApiException implements Exception { final int statusCode; final String message; ApiException(this.statusCode, this.message); @override String toString() => message; static ApiException fromDioError(DioException e) { final data = e.response?.data; String msg = 'Terjadi kesalahan. Coba lagi.'; if (data is Map && data['detail'] != null) { final detail = data['detail']; if (detail is String) msg = detail; else if (detail is List && detail.isNotEmpty) { msg = detail.map((d) => d['msg'] ?? '').join(', '); } } return ApiException(e.response?.statusCode ?? 0, msg); } }