membre add
This commit is contained in:
273
app/lib/core/utils/api_exception.dart
Normal file
273
app/lib/core/utils/api_exception.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
/// Exception personnalisée pour les erreurs API avec méthodes d'affichage intégrées
|
||||
class ApiException implements Exception {
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
final String? errorCode;
|
||||
final Map<String, dynamic>? details;
|
||||
final Object? originalError;
|
||||
|
||||
const ApiException(
|
||||
this.message, {
|
||||
this.statusCode,
|
||||
this.errorCode,
|
||||
this.details,
|
||||
this.originalError,
|
||||
});
|
||||
|
||||
/// Créer une ApiException depuis une DioException
|
||||
factory ApiException.fromDioException(DioException dioException) {
|
||||
final response = dioException.response;
|
||||
final statusCode = response?.statusCode;
|
||||
|
||||
// Essayer d'extraire le message de la réponse API
|
||||
String message = 'Erreur de communication avec le serveur';
|
||||
String? errorCode;
|
||||
Map<String, dynamic>? details;
|
||||
|
||||
if (response?.data != null) {
|
||||
try {
|
||||
final data = response!.data as Map<String, dynamic>;
|
||||
|
||||
// Message spécifique de l'API
|
||||
if (data.containsKey('message')) {
|
||||
message = data['message'] as String;
|
||||
}
|
||||
|
||||
// Code d'erreur spécifique
|
||||
if (data.containsKey('error_code')) {
|
||||
errorCode = data['error_code'] as String;
|
||||
}
|
||||
|
||||
// Détails supplémentaires
|
||||
if (data.containsKey('errors')) {
|
||||
details = data['errors'] as Map<String, dynamic>?;
|
||||
}
|
||||
} catch (e) {
|
||||
// Si on ne peut pas parser la réponse, utiliser le message par défaut
|
||||
}
|
||||
}
|
||||
|
||||
// Messages par défaut selon le code de statut
|
||||
if (response?.data == null || message == 'Erreur de communication avec le serveur') {
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
message = 'Données invalides';
|
||||
break;
|
||||
case 401:
|
||||
message = 'Non autorisé : veuillez vous reconnecter';
|
||||
break;
|
||||
case 403:
|
||||
message = 'Accès interdit';
|
||||
break;
|
||||
case 404:
|
||||
message = 'Ressource non trouvée';
|
||||
break;
|
||||
case 409:
|
||||
message = 'Conflit : données déjà existantes';
|
||||
break;
|
||||
case 422:
|
||||
message = 'Données de validation incorrectes';
|
||||
break;
|
||||
case 500:
|
||||
message = 'Erreur serveur interne';
|
||||
break;
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
message = 'Service temporairement indisponible';
|
||||
break;
|
||||
default:
|
||||
switch (dioException.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
message = 'Délai d\'attente dépassé';
|
||||
break;
|
||||
case DioExceptionType.connectionError:
|
||||
message = 'Problème de connexion réseau';
|
||||
break;
|
||||
case DioExceptionType.cancel:
|
||||
message = 'Requête annulée';
|
||||
break;
|
||||
default:
|
||||
message = 'Erreur de communication avec le serveur';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ApiException(
|
||||
message,
|
||||
statusCode: statusCode,
|
||||
errorCode: errorCode,
|
||||
details: details,
|
||||
originalError: dioException,
|
||||
);
|
||||
}
|
||||
|
||||
/// Créer une ApiException depuis n'importe quelle erreur
|
||||
factory ApiException.fromError(Object error) {
|
||||
if (error is ApiException) {
|
||||
return error;
|
||||
}
|
||||
|
||||
final errorString = error.toString();
|
||||
|
||||
if (errorString.contains('SocketException') || errorString.contains('NetworkException')) {
|
||||
return const ApiException('Problème de connexion réseau');
|
||||
}
|
||||
if (errorString.contains('TimeoutException')) {
|
||||
return const ApiException('Délai d\'attente dépassé');
|
||||
}
|
||||
|
||||
return ApiException('Erreur inattendue', originalError: error);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
|
||||
/// Obtenir un message d'erreur formaté pour l'affichage
|
||||
String get displayMessage => message;
|
||||
|
||||
/// Vérifier si c'est une erreur de validation
|
||||
bool get isValidationError => statusCode == 422 || statusCode == 400;
|
||||
|
||||
/// Vérifier si c'est une erreur d'authentification
|
||||
bool get isAuthError => statusCode == 401 || statusCode == 403;
|
||||
|
||||
/// Vérifier si c'est une erreur de conflit (données déjà existantes)
|
||||
bool get isConflictError => statusCode == 409;
|
||||
|
||||
/// Vérifier si c'est une erreur réseau
|
||||
bool get isNetworkError =>
|
||||
statusCode == null ||
|
||||
statusCode == 502 ||
|
||||
statusCode == 503 ||
|
||||
statusCode == 504 ||
|
||||
originalError is DioException && (originalError as DioException).type == DioExceptionType.connectionError;
|
||||
|
||||
// === MÉTHODES D'AFFICHAGE INTÉGRÉES ===
|
||||
|
||||
/// Afficher cette erreur (méthode d'instance)
|
||||
void show(BuildContext context, {Duration? duration}) {
|
||||
if (context.mounted) {
|
||||
final isInDialog = _isInDialog(context);
|
||||
|
||||
if (isInDialog) {
|
||||
_showOverlaySnackBar(context, displayMessage, Colors.red, duration);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(displayMessage),
|
||||
backgroundColor: Colors.red,
|
||||
duration: duration ?? const Duration(seconds: 5),
|
||||
action: SnackBarAction(
|
||||
label: 'Fermer',
|
||||
textColor: Colors.white,
|
||||
onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Méthode statique pour afficher une erreur depuis n'importe quel objet
|
||||
static void showError(BuildContext context, Object error, {Duration? duration}) {
|
||||
final apiException = ApiException.fromError(error);
|
||||
apiException.show(context, duration: duration);
|
||||
}
|
||||
|
||||
/// Méthode statique pour afficher un message de succès (compatible avec les Dialogs)
|
||||
static void showSuccess(BuildContext context, String message, {Duration? duration}) {
|
||||
if (context.mounted) {
|
||||
final isInDialog = _isInDialog(context);
|
||||
|
||||
if (isInDialog) {
|
||||
_showOverlaySnackBar(context, message, Colors.green, duration);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.green,
|
||||
duration: duration ?? const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifier si le contexte est dans un Dialog
|
||||
static bool _isInDialog(BuildContext context) {
|
||||
return context.findAncestorWidgetOfExactType<Dialog>() != null;
|
||||
}
|
||||
|
||||
/// Afficher un SnackBar en overlay au-dessus de tout
|
||||
static void _showOverlaySnackBar(BuildContext context, String message, Color backgroundColor, Duration? duration) {
|
||||
final overlay = Overlay.of(context);
|
||||
late OverlayEntry overlayEntry;
|
||||
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 20, // En haut de l'écran
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
backgroundColor == Colors.red ? Icons.error_outline : Icons.check_circle_outline,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white, size: 18),
|
||||
onPressed: () => overlayEntry.remove(),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 30, minHeight: 30),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
overlay.insert(overlayEntry);
|
||||
|
||||
// Auto-suppression après la durée spécifiée
|
||||
Timer(duration ?? const Duration(seconds: 5), () {
|
||||
if (overlayEntry.mounted) {
|
||||
overlayEntry.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user