Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
95
lib/core/utils/error_formatter.dart
Normal file
95
lib/core/utils/error_formatter.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
/// Utilitaire pour formater les messages d'erreur venant du backend.
|
||||
/// Gère notamment les erreurs LCB-FT (anti-blanchiment).
|
||||
class ErrorFormatter {
|
||||
/// Formate une erreur en message utilisateur convivial.
|
||||
///
|
||||
/// Détecte et formate spécialement les erreurs LCB-FT (origine des fonds manquante).
|
||||
/// Supprime les préfixes techniques comme "Exception: " ou "DioException: ".
|
||||
static String format(dynamic error) {
|
||||
if (error == null) return 'Une erreur inconnue est survenue';
|
||||
|
||||
final errorString = error.toString();
|
||||
|
||||
// Erreur LCB-FT : origine des fonds manquante
|
||||
if (errorString.contains('origine des fonds') ||
|
||||
errorString.contains('LCB-FT') ||
|
||||
errorString.contains('au-dessus du seuil')) {
|
||||
return '🛡️ L\'origine des fonds est obligatoire pour cette opération (conformité LCB-FT anti-blanchiment).\n\nVeuillez préciser d\'où proviennent les fonds.';
|
||||
}
|
||||
|
||||
// Erreur KYC
|
||||
if (errorString.contains('KYC') || errorString.contains('vérification identité')) {
|
||||
return '🛡️ Votre identité doit être vérifiée pour cette opération (conformité KYC).\n\nContactez votre administrateur.';
|
||||
}
|
||||
|
||||
// Erreur solde insuffisant
|
||||
if (errorString.contains('solde') && errorString.contains('insuffisant')) {
|
||||
return '💳 Solde insuffisant pour effectuer cette opération.';
|
||||
}
|
||||
|
||||
// Erreur réseau / timeout
|
||||
if (errorString.contains('SocketException') ||
|
||||
errorString.contains('timeout') ||
|
||||
errorString.contains('network')) {
|
||||
return '📡 Erreur de connexion. Vérifiez votre connexion internet et réessayez.';
|
||||
}
|
||||
|
||||
// Erreur 400 générique (validation backend)
|
||||
if (errorString.contains('400') || errorString.contains('Bad Request')) {
|
||||
// Essayer d'extraire le message du backend
|
||||
final match = RegExp(r'message["\s:]+([^"}\n]+)', caseSensitive: false)
|
||||
.firstMatch(errorString);
|
||||
if (match != null && match.group(1) != null) {
|
||||
return match.group(1)!.trim();
|
||||
}
|
||||
return 'Données invalides. Vérifiez les informations saisies.';
|
||||
}
|
||||
|
||||
// Erreur 401 / 403 (authentification / autorisation)
|
||||
if (errorString.contains('401') || errorString.contains('403')) {
|
||||
return '🔒 Vous n\'avez pas les autorisations nécessaires pour cette opération.';
|
||||
}
|
||||
|
||||
// Erreur 404 (ressource non trouvée)
|
||||
if (errorString.contains('404')) {
|
||||
return 'Ressource non trouvée. Elle a peut-être été supprimée.';
|
||||
}
|
||||
|
||||
// Erreur 500 (erreur serveur)
|
||||
if (errorString.contains('500') || errorString.contains('Internal Server')) {
|
||||
return '🔧 Erreur serveur. Nos équipes ont été notifiées. Réessayez plus tard.';
|
||||
}
|
||||
|
||||
// Nettoyer les préfixes techniques
|
||||
String cleaned = errorString
|
||||
.replaceFirst('Exception: ', '')
|
||||
.replaceFirst('DioException: ', '')
|
||||
.replaceFirst('DioError: ', '')
|
||||
.replaceFirst('Error: ', '')
|
||||
.trim();
|
||||
|
||||
// Si le message est trop long, le tronquer
|
||||
if (cleaned.length > 200) {
|
||||
cleaned = '${cleaned.substring(0, 197)}...';
|
||||
}
|
||||
|
||||
return cleaned.isNotEmpty ? cleaned : 'Une erreur est survenue';
|
||||
}
|
||||
|
||||
/// Détermine si une erreur est critique (nécessite intervention admin).
|
||||
static bool isCritical(dynamic error) {
|
||||
final errorString = error.toString().toLowerCase();
|
||||
return errorString.contains('kyc') ||
|
||||
errorString.contains('vérification identité') ||
|
||||
errorString.contains('401') ||
|
||||
errorString.contains('403');
|
||||
}
|
||||
|
||||
/// Détermine si une erreur est liée au LCB-FT.
|
||||
static bool isLcbFtError(dynamic error) {
|
||||
final errorString = error.toString().toLowerCase();
|
||||
return errorString.contains('origine des fonds') ||
|
||||
errorString.contains('lcb-ft') ||
|
||||
errorString.contains('anti-blanchiment');
|
||||
}
|
||||
}
|
||||
325
lib/core/utils/logger.dart
Normal file
325
lib/core/utils/logger.dart
Normal file
@@ -0,0 +1,325 @@
|
||||
/// Logger centralisé pour l'application
|
||||
library logger;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../config/environment.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
|
||||
/// Niveaux de log
|
||||
enum LogLevel {
|
||||
debug,
|
||||
info,
|
||||
warning,
|
||||
error,
|
||||
fatal,
|
||||
}
|
||||
|
||||
/// Logger centralisé pour toute l'application
|
||||
class AppLogger {
|
||||
// Empêcher l'instanciation
|
||||
AppLogger._();
|
||||
|
||||
/// Couleurs ANSI pour les logs en console
|
||||
static const String _reset = '\x1B[0m';
|
||||
static const String _red = '\x1B[31m';
|
||||
static const String _green = '\x1B[32m';
|
||||
static const String _yellow = '\x1B[33m';
|
||||
static const String _blue = '\x1B[34m';
|
||||
static const String _magenta = '\x1B[35m';
|
||||
static const String _cyan = '\x1B[36m';
|
||||
static const String _white = '\x1B[37m';
|
||||
|
||||
/// Log de niveau DEBUG (bleu)
|
||||
static void debug(String message, {String? tag}) {
|
||||
if (AppConfig.enableLogging && kDebugMode) {
|
||||
_log(LogLevel.debug, message, tag: tag, color: _blue);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de niveau INFO (vert)
|
||||
static void info(String message, {String? tag}) {
|
||||
if (AppConfig.enableLogging && kDebugMode) {
|
||||
_log(LogLevel.info, message, tag: tag, color: _green);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de niveau WARNING (jaune)
|
||||
static void warning(String message, {String? tag}) {
|
||||
if (AppConfig.enableLogging && kDebugMode) {
|
||||
_log(LogLevel.warning, message, tag: tag, color: _yellow);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de niveau ERROR (rouge)
|
||||
static void error(
|
||||
String message, {
|
||||
String? tag,
|
||||
dynamic error,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
if (AppConfig.enableLogging) {
|
||||
_log(LogLevel.error, message, tag: tag, color: _red);
|
||||
|
||||
if (error != null) {
|
||||
_log(LogLevel.error, 'Error: $error', tag: tag, color: _red);
|
||||
}
|
||||
|
||||
if (stackTrace != null) {
|
||||
_log(LogLevel.error, 'StackTrace:\n$stackTrace', tag: tag, color: _red);
|
||||
}
|
||||
|
||||
// Envoi au service de monitoring si configuré
|
||||
if (AppConfig.enableCrashReporting) {
|
||||
_sendToMonitoring(message, error, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Log de niveau FATAL (magenta)
|
||||
static void fatal(
|
||||
String message, {
|
||||
String? tag,
|
||||
dynamic error,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
if (AppConfig.enableLogging) {
|
||||
_log(LogLevel.fatal, message, tag: tag, color: _magenta);
|
||||
|
||||
if (error != null) {
|
||||
_log(LogLevel.fatal, 'Error: $error', tag: tag, color: _magenta);
|
||||
}
|
||||
|
||||
if (stackTrace != null) {
|
||||
_log(LogLevel.fatal, 'StackTrace:\n$stackTrace', tag: tag, color: _magenta);
|
||||
}
|
||||
|
||||
// Envoi au service de monitoring si configuré
|
||||
if (AppConfig.enableCrashReporting) {
|
||||
_sendToMonitoring(message, error, stackTrace, isFatal: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'une requête HTTP
|
||||
static void httpRequest({
|
||||
required String method,
|
||||
required String url,
|
||||
Map<String, dynamic>? headers,
|
||||
dynamic body,
|
||||
}) {
|
||||
if (AppConfig.enableLogging && kDebugMode) {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('┌─────────────────────────────────────────────────');
|
||||
buffer.writeln('│ HTTP REQUEST');
|
||||
buffer.writeln('├─────────────────────────────────────────────────');
|
||||
buffer.writeln('│ Method: $method');
|
||||
buffer.writeln('│ URL: $url');
|
||||
|
||||
if (headers != null && headers.isNotEmpty) {
|
||||
buffer.writeln('│ Headers:');
|
||||
headers.forEach((key, value) {
|
||||
buffer.writeln('│ $key: $value');
|
||||
});
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
buffer.writeln('│ Body: $body');
|
||||
}
|
||||
|
||||
buffer.writeln('└─────────────────────────────────────────────────');
|
||||
|
||||
_log(LogLevel.debug, buffer.toString(), color: _cyan);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'une réponse HTTP
|
||||
static void httpResponse({
|
||||
required int statusCode,
|
||||
required String url,
|
||||
Map<String, dynamic>? headers,
|
||||
dynamic body,
|
||||
Duration? duration,
|
||||
}) {
|
||||
if (AppConfig.enableLogging && kDebugMode) {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('┌─────────────────────────────────────────────────');
|
||||
buffer.writeln('│ HTTP RESPONSE');
|
||||
buffer.writeln('├─────────────────────────────────────────────────');
|
||||
buffer.writeln('│ Status: $statusCode');
|
||||
buffer.writeln('│ URL: $url');
|
||||
|
||||
if (duration != null) {
|
||||
buffer.writeln('│ Duration: ${duration.inMilliseconds}ms');
|
||||
}
|
||||
|
||||
if (headers != null && headers.isNotEmpty) {
|
||||
buffer.writeln('│ Headers:');
|
||||
headers.forEach((key, value) {
|
||||
buffer.writeln('│ $key: $value');
|
||||
});
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
buffer.writeln('│ Body: $body');
|
||||
}
|
||||
|
||||
buffer.writeln('└─────────────────────────────────────────────────');
|
||||
|
||||
final color = statusCode >= 200 && statusCode < 300 ? _green : _red;
|
||||
_log(LogLevel.debug, buffer.toString(), color: color);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'un événement BLoC
|
||||
static void blocEvent(String blocName, String eventName, {dynamic data}) {
|
||||
if (AppConfig.enableLogging && kDebugMode) {
|
||||
final message = data != null
|
||||
? '[$blocName] Event: $eventName | Data: $data'
|
||||
: '[$blocName] Event: $eventName';
|
||||
_log(LogLevel.debug, message, color: _cyan);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'un changement d'état BLoC
|
||||
static void blocState(String blocName, String stateName, {dynamic data}) {
|
||||
if (AppConfig.enableLogging && kDebugMode) {
|
||||
final message = data != null
|
||||
? '[$blocName] State: $stateName | Data: $data'
|
||||
: '[$blocName] State: $stateName';
|
||||
_log(LogLevel.debug, message, color: _magenta);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'une navigation
|
||||
static void navigation(String from, String to) {
|
||||
if (AppConfig.enableLogging && kDebugMode) {
|
||||
_log(LogLevel.debug, 'Navigation: $from → $to', color: _yellow);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log d'une action utilisateur
|
||||
static void userAction(String action, {Map<String, dynamic>? data}) {
|
||||
if (AppConfig.enableLogging && kDebugMode) {
|
||||
final message = data != null
|
||||
? 'User Action: $action | Data: $data'
|
||||
: 'User Action: $action';
|
||||
_log(LogLevel.info, message, color: _green);
|
||||
}
|
||||
|
||||
// Envoi au service d'analytics si configuré
|
||||
if (AppConfig.enableAnalytics) {
|
||||
_sendToAnalytics(action, data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Méthode privée pour logger avec formatage
|
||||
static void _log(
|
||||
LogLevel level,
|
||||
String message, {
|
||||
String? tag,
|
||||
String color = _white,
|
||||
}) {
|
||||
final timestamp = DateTime.now().toIso8601String();
|
||||
final levelStr = level.name.toUpperCase().padRight(7);
|
||||
final tagStr = tag != null ? '[$tag] ' : '';
|
||||
|
||||
if (kDebugMode) {
|
||||
// En mode debug, utiliser les couleurs
|
||||
debugPrint('$color$timestamp | $levelStr | $tagStr$message$_reset');
|
||||
} else {
|
||||
// En mode release, pas de couleurs
|
||||
debugPrint('$timestamp | $levelStr | $tagStr$message');
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback optionnel pour envoyer les erreurs au monitoring (Sentry / Firebase Crashlytics).
|
||||
/// À enregistrer au démarrage de l'app quand le SDK est intégré.
|
||||
static void Function(String message, dynamic error, StackTrace? stackTrace, {bool isFatal})? onMonitoringReport;
|
||||
|
||||
/// Callback optionnel pour envoyer les événements analytics (Firebase Analytics / Mixpanel).
|
||||
/// À enregistrer au démarrage de l'app quand le SDK est intégré.
|
||||
static void Function(String action, Map<String, dynamic>? data)? onAnalyticsEvent;
|
||||
|
||||
/// Envoyer les erreurs à un service de monitoring
|
||||
static void _sendToMonitoring(
|
||||
String message,
|
||||
dynamic error,
|
||||
StackTrace? stackTrace, {
|
||||
bool isFatal = false,
|
||||
}) {
|
||||
if (onMonitoringReport != null) {
|
||||
try {
|
||||
onMonitoringReport!(message, error, stackTrace, isFatal: isFatal);
|
||||
} catch (e, st) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('AppLogger: échec envoi monitoring: $e');
|
||||
debugPrint('$st');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (kDebugMode && (error != null || stackTrace != null)) {
|
||||
debugPrint('AppLogger: monitoring non configuré (enregistrer onMonitoringReport pour Sentry/Crashlytics)');
|
||||
}
|
||||
}
|
||||
|
||||
/// Envoyer les événements à un service d'analytics
|
||||
static void _sendToAnalytics(String action, Map<String, dynamic>? data) {
|
||||
if (onAnalyticsEvent != null) {
|
||||
try {
|
||||
onAnalyticsEvent!(action, data);
|
||||
} catch (e, st) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('AppLogger: échec envoi analytics: $e');
|
||||
debugPrint('$st');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (kDebugMode) {
|
||||
debugPrint('AppLogger: analytics non configuré (enregistrer onAnalyticsEvent pour Firebase/Mixpanel)');
|
||||
}
|
||||
}
|
||||
|
||||
/// Divider pour séparer visuellement les logs
|
||||
static void divider({String? title}) {
|
||||
if (AppConfig.enableLogging && kDebugMode) {
|
||||
if (title != null) {
|
||||
debugPrint('$_cyan═══════════════════════════════════════════════════$_reset');
|
||||
debugPrint('$_cyan $title$_reset');
|
||||
debugPrint('$_cyan═══════════════════════════════════════════════════$_reset');
|
||||
} else {
|
||||
debugPrint('$_cyan═══════════════════════════════════════════════════$_reset');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension pour faciliter le logging depuis n'importe où
|
||||
extension LoggerExtension on Object {
|
||||
/// Log debug
|
||||
void logDebug(String message) {
|
||||
AppLogger.debug(message, tag: runtimeType.toString());
|
||||
}
|
||||
|
||||
/// Log info
|
||||
void logInfo(String message) {
|
||||
AppLogger.info(message, tag: runtimeType.toString());
|
||||
}
|
||||
|
||||
/// Log warning
|
||||
void logWarning(String message) {
|
||||
AppLogger.warning(message, tag: runtimeType.toString());
|
||||
}
|
||||
|
||||
/// Log error
|
||||
void logError(String message, {dynamic error, StackTrace? stackTrace}) {
|
||||
AppLogger.error(
|
||||
message,
|
||||
tag: runtimeType.toString(),
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user