import 'dart:convert'; import 'package:dio/dio.dart'; import '../../core/l10n/l10n.dart'; import 'error_code_mapper.dart'; abstract class ApiException implements Exception { final String message; final int? statusCode; final String? errorCode; final Map? errorParams; const ApiException( this.message, { this.statusCode, this.errorCode, this.errorParams, }); @override String toString() => message; factory ApiException.fromDioError(Object error) { if (error is ApiException) return error; if (error is DioException) { final response = error.response; final statusCode = response?.statusCode; final data = response?.data; String detail; String? errorCode; Map? errorParams; final decodedData = _normalizeErrorData(data); if (decodedData is Map) { detail = (decodedData['detail'] ?? decodedData['message'] ?? decodedData['error']) ?.toString() ?? L10n.current.errorRequestFailed; final code = decodedData['code']; if (code is String && code.trim().isNotEmpty) { errorCode = code; } final params = decodedData['params']; if (params is Map) { errorParams = params; } else if (params is Map) { errorParams = params.map( (key, value) => MapEntry(key.toString(), value), ); } } else { detail = _networkErrorMessage(error); } final localized = _localizeError( detail, statusCode, errorCode: errorCode, errorParams: errorParams, ); if (statusCode == 401) { return UnauthorizedException(message: localized, errorCode: errorCode); } if (statusCode == 422) { return ValidationException( localized, errors: data, statusCode: statusCode, errorCode: errorCode, errorParams: errorParams, ); } return ServerException( localized, statusCode: statusCode, errorCode: errorCode, errorParams: errorParams, ); } return ServerException(L10n.current.errorNetwork); } static Map? _normalizeErrorData(dynamic data) { if (data is Map) { return data; } if (data is Map) { return data.map((key, value) => MapEntry(key.toString(), value)); } if (data is String && data.trim().isNotEmpty) { try { final decoded = jsonDecode(data); if (decoded is Map) { return decoded; } if (decoded is Map) { return decoded.map((key, value) => MapEntry(key.toString(), value)); } } catch (_) { return null; } } return null; } static String _localizeError( String detail, int? statusCode, { String? errorCode, Map? errorParams, }) { final mapped = resolveErrorCodeMessage(errorCode, params: errorParams); if (mapped != null && mapped.isNotEmpty) { return mapped; } if (statusCode == 403) { return L10n.current.errorForbidden; } if (statusCode == 404) { return L10n.current.errorNotFound; } if (statusCode == 429) { return L10n.current.errorTooManyRequests; } if (statusCode != null && statusCode >= 500) { return L10n.current.errorServer; } return L10n.current.errorGenericSafe; } static String _networkErrorMessage(DioException error) { if (error.type == DioExceptionType.connectionTimeout || error.type == DioExceptionType.sendTimeout || error.type == DioExceptionType.receiveTimeout) { return L10n.current.errorNetworkTimeout; } if (error.type == DioExceptionType.connectionError || error.type == DioExceptionType.unknown) { return L10n.current.errorNetworkUnavailable; } return L10n.current.errorRequestFailed; } } class ServerException extends ApiException { const ServerException( super.message, { super.statusCode, super.errorCode, super.errorParams, }); } class UnauthorizedException extends ApiException { UnauthorizedException({String? message, String? errorCode}) : super( message ?? L10n.current.errorReLogin, statusCode: 401, errorCode: errorCode, ); } class ValidationException extends ApiException { final Map? errors; const ValidationException( super.message, { this.errors, super.statusCode, super.errorCode, super.errorParams, }); }