Refactoring
This commit is contained in:
@@ -13,10 +13,10 @@ import 'package:dio/dio.dart';
|
||||
|
||||
@singleton
|
||||
class KeycloakWebViewAuthService {
|
||||
static const String _keycloakBaseUrl = 'http://192.168.1.145:8180';
|
||||
static const String _keycloakBaseUrl = 'http://192.168.1.11:8180';
|
||||
static const String _realm = 'unionflow';
|
||||
static const String _clientId = 'unionflow-mobile';
|
||||
static const String _redirectUrl = 'http://192.168.1.145:8080/auth/callback';
|
||||
static const String _redirectUrl = 'http://192.168.1.11:8080/auth/callback';
|
||||
|
||||
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
|
||||
final Dio _dio = Dio();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class AppConstants {
|
||||
// API Configuration
|
||||
static const String baseUrl = 'http://192.168.1.145:8080'; // Backend UnionFlow
|
||||
static const String baseUrl = 'http://192.168.1.11:8080'; // Backend UnionFlow
|
||||
static const String apiVersion = '/api';
|
||||
|
||||
// Timeout
|
||||
|
||||
@@ -19,7 +19,7 @@ class DioClient {
|
||||
void _configureOptions() {
|
||||
_dio.options = BaseOptions(
|
||||
// URL de base de l'API
|
||||
baseUrl: 'http://192.168.1.145:8080', // Adresse de votre API Quarkus
|
||||
baseUrl: 'http://192.168.1.11:8080', // Adresse de votre API Quarkus
|
||||
|
||||
// Timeouts
|
||||
connectTimeout: const Duration(seconds: 30),
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Service d'optimisation des performances pour l'application UnionFlow
|
||||
///
|
||||
/// Fournit des utilitaires pour :
|
||||
/// - Optimisation des widgets
|
||||
/// - Gestion de la mémoire
|
||||
/// - Mise en cache intelligente
|
||||
/// - Monitoring des performances
|
||||
class PerformanceOptimizer {
|
||||
static const String _tag = 'PerformanceOptimizer';
|
||||
|
||||
/// Singleton instance
|
||||
static final PerformanceOptimizer _instance = PerformanceOptimizer._internal();
|
||||
factory PerformanceOptimizer() => _instance;
|
||||
PerformanceOptimizer._internal();
|
||||
|
||||
/// Cache pour les widgets optimisés
|
||||
final Map<String, Widget> _widgetCache = {};
|
||||
|
||||
/// Cache pour les images
|
||||
final Map<String, ImageProvider> _imageCache = {};
|
||||
|
||||
/// Compteurs de performance
|
||||
final Map<String, int> _performanceCounters = {};
|
||||
|
||||
/// Temps de début pour les mesures
|
||||
final Map<String, DateTime> _performanceTimers = {};
|
||||
|
||||
// ========================================
|
||||
// OPTIMISATION DES WIDGETS
|
||||
// ========================================
|
||||
|
||||
/// Optimise un widget avec RepaintBoundary si nécessaire
|
||||
static Widget optimizeWidget(Widget child, {
|
||||
String? key,
|
||||
bool forceRepaintBoundary = false,
|
||||
bool addSemantics = true,
|
||||
}) {
|
||||
Widget optimized = child;
|
||||
|
||||
// Ajouter RepaintBoundary pour les widgets complexes
|
||||
if (forceRepaintBoundary || _shouldAddRepaintBoundary(child)) {
|
||||
optimized = RepaintBoundary(
|
||||
key: key != null ? Key('repaint_$key') : null,
|
||||
child: optimized,
|
||||
);
|
||||
}
|
||||
|
||||
// Ajouter Semantics pour l'accessibilité
|
||||
if (addSemantics && _shouldAddSemantics(child)) {
|
||||
optimized = Semantics(
|
||||
key: key != null ? Key('semantics_$key') : null,
|
||||
child: optimized,
|
||||
);
|
||||
}
|
||||
|
||||
return optimized;
|
||||
}
|
||||
|
||||
/// Détermine si un RepaintBoundary est nécessaire
|
||||
static bool _shouldAddRepaintBoundary(Widget widget) {
|
||||
// Ajouter RepaintBoundary pour les widgets qui changent fréquemment
|
||||
return widget is AnimatedWidget ||
|
||||
widget is CustomPaint ||
|
||||
widget is Image ||
|
||||
widget.runtimeType.toString().contains('Chart') ||
|
||||
widget.runtimeType.toString().contains('Graph');
|
||||
}
|
||||
|
||||
/// Détermine si Semantics est nécessaire
|
||||
static bool _shouldAddSemantics(Widget widget) {
|
||||
return widget is GestureDetector ||
|
||||
widget is InkWell ||
|
||||
widget is ElevatedButton ||
|
||||
widget is TextButton ||
|
||||
widget is IconButton;
|
||||
}
|
||||
|
||||
/// Crée un widget avec mise en cache
|
||||
Widget cachedWidget(String key, Widget Function() builder) {
|
||||
if (_widgetCache.containsKey(key)) {
|
||||
return _widgetCache[key]!;
|
||||
}
|
||||
|
||||
final widget = builder();
|
||||
_widgetCache[key] = widget;
|
||||
return widget;
|
||||
}
|
||||
|
||||
/// Nettoie le cache des widgets
|
||||
void clearWidgetCache() {
|
||||
_widgetCache.clear();
|
||||
debugPrint('$_tag: Widget cache cleared');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// OPTIMISATION DES IMAGES
|
||||
// ========================================
|
||||
|
||||
/// Optimise le chargement d'une image
|
||||
static ImageProvider optimizeImage(String path, {
|
||||
double? width,
|
||||
double? height,
|
||||
BoxFit fit = BoxFit.cover,
|
||||
}) {
|
||||
// Utiliser ResizeImage pour optimiser la mémoire
|
||||
if (width != null || height != null) {
|
||||
return ResizeImage(
|
||||
AssetImage(path),
|
||||
width: width?.round(),
|
||||
height: height?.round(),
|
||||
);
|
||||
}
|
||||
|
||||
return AssetImage(path);
|
||||
}
|
||||
|
||||
/// Met en cache une image
|
||||
ImageProvider cachedImage(String key, String path) {
|
||||
if (_imageCache.containsKey(key)) {
|
||||
return _imageCache[key]!;
|
||||
}
|
||||
|
||||
final image = AssetImage(path);
|
||||
_imageCache[key] = image;
|
||||
return image;
|
||||
}
|
||||
|
||||
/// Précharge les images critiques
|
||||
static Future<void> preloadCriticalImages(BuildContext context, List<String> imagePaths) async {
|
||||
final futures = imagePaths.map((path) =>
|
||||
precacheImage(AssetImage(path), context)
|
||||
).toList();
|
||||
|
||||
await Future.wait(futures);
|
||||
debugPrint('$_tag: ${imagePaths.length} critical images preloaded');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MONITORING DES PERFORMANCES
|
||||
// ========================================
|
||||
|
||||
/// Démarre un timer de performance
|
||||
void startTimer(String operation) {
|
||||
_performanceTimers[operation] = DateTime.now();
|
||||
}
|
||||
|
||||
/// Arrête un timer et log le résultat
|
||||
void stopTimer(String operation) {
|
||||
final startTime = _performanceTimers[operation];
|
||||
if (startTime != null) {
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
debugPrint('$_tag: $operation took ${duration.inMilliseconds}ms');
|
||||
_performanceTimers.remove(operation);
|
||||
|
||||
// Incrémenter le compteur
|
||||
_performanceCounters[operation] = (_performanceCounters[operation] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Incrémente un compteur de performance
|
||||
void incrementCounter(String metric) {
|
||||
_performanceCounters[metric] = (_performanceCounters[metric] ?? 0) + 1;
|
||||
}
|
||||
|
||||
/// Obtient les statistiques de performance
|
||||
Map<String, int> getPerformanceStats() {
|
||||
return Map.from(_performanceCounters);
|
||||
}
|
||||
|
||||
/// Réinitialise les statistiques
|
||||
void resetStats() {
|
||||
_performanceCounters.clear();
|
||||
_performanceTimers.clear();
|
||||
debugPrint('$_tag: Performance stats reset');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// OPTIMISATION MÉMOIRE
|
||||
// ========================================
|
||||
|
||||
/// Force le garbage collection (debug uniquement)
|
||||
static void forceGarbageCollection() {
|
||||
if (kDebugMode) {
|
||||
// Forcer le GC en créant et supprimant des objets
|
||||
final temp = List.generate(1000, (i) => Object());
|
||||
temp.clear();
|
||||
debugPrint('PerformanceOptimizer: Forced garbage collection');
|
||||
}
|
||||
}
|
||||
|
||||
/// Nettoie tous les caches
|
||||
void clearAllCaches() {
|
||||
clearWidgetCache();
|
||||
_imageCache.clear();
|
||||
debugPrint('$_tag: All caches cleared');
|
||||
}
|
||||
|
||||
/// Obtient la taille des caches
|
||||
Map<String, int> getCacheSizes() {
|
||||
return {
|
||||
'widgets': _widgetCache.length,
|
||||
'images': _imageCache.length,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// OPTIMISATION DES ANIMATIONS
|
||||
// ========================================
|
||||
|
||||
/// Crée un AnimationController optimisé
|
||||
static AnimationController createOptimizedController({
|
||||
required Duration duration,
|
||||
required TickerProvider vsync,
|
||||
double? value,
|
||||
Duration? reverseDuration,
|
||||
String? debugLabel,
|
||||
}) {
|
||||
return AnimationController(
|
||||
duration: duration,
|
||||
reverseDuration: reverseDuration,
|
||||
vsync: vsync,
|
||||
value: value,
|
||||
debugLabel: debugLabel ?? 'OptimizedController',
|
||||
);
|
||||
}
|
||||
|
||||
/// Dispose proprement une liste d'AnimationControllers
|
||||
static void disposeControllers(List<AnimationController> controllers) {
|
||||
for (final controller in controllers) {
|
||||
try {
|
||||
controller.dispose();
|
||||
} catch (e) {
|
||||
// Controller déjà disposé, ignorer l'erreur
|
||||
debugPrint('$_tag: Controller already disposed: $e');
|
||||
}
|
||||
}
|
||||
controllers.clear();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// UTILITAIRES DE PERFORMANCE
|
||||
// ========================================
|
||||
|
||||
/// Vérifie si l'appareil est performant
|
||||
static bool isHighPerformanceDevice() {
|
||||
// Logique basée sur les capacités de l'appareil
|
||||
// Pour l'instant, retourne true par défaut
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Obtient le niveau de performance recommandé
|
||||
static PerformanceLevel getRecommendedPerformanceLevel() {
|
||||
if (isHighPerformanceDevice()) {
|
||||
return PerformanceLevel.high;
|
||||
} else {
|
||||
return PerformanceLevel.medium;
|
||||
}
|
||||
}
|
||||
|
||||
/// Applique les optimisations selon le niveau de performance
|
||||
static void applyPerformanceLevel(PerformanceLevel level) {
|
||||
switch (level) {
|
||||
case PerformanceLevel.high:
|
||||
// Toutes les animations et effets activés
|
||||
debugPrint('$_tag: High performance mode enabled');
|
||||
break;
|
||||
case PerformanceLevel.medium:
|
||||
// Animations réduites
|
||||
debugPrint('$_tag: Medium performance mode enabled');
|
||||
break;
|
||||
case PerformanceLevel.low:
|
||||
// Animations désactivées
|
||||
debugPrint('$_tag: Low performance mode enabled');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MONITORING EN TEMPS RÉEL
|
||||
// ========================================
|
||||
|
||||
/// Démarre le monitoring des performances
|
||||
void startPerformanceMonitoring() {
|
||||
// Monitoring du frame rate
|
||||
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) {
|
||||
_monitorFrameRate();
|
||||
});
|
||||
|
||||
// Monitoring de la mémoire (toutes les 30 secondes)
|
||||
Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
_monitorMemoryUsage();
|
||||
});
|
||||
|
||||
debugPrint('$_tag: Performance monitoring started');
|
||||
}
|
||||
|
||||
void _monitorFrameRate() {
|
||||
// Logique de monitoring du frame rate
|
||||
// Pour l'instant, juste incrémenter un compteur
|
||||
incrementCounter('frames_rendered');
|
||||
}
|
||||
|
||||
void _monitorMemoryUsage() {
|
||||
// Logique de monitoring de la mémoire
|
||||
if (kDebugMode) {
|
||||
final cacheSize = getCacheSizes();
|
||||
debugPrint('$_tag: Cache sizes - Widgets: ${cacheSize['widgets']}, Images: ${cacheSize['images']}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Niveaux de performance
|
||||
enum PerformanceLevel {
|
||||
low,
|
||||
medium,
|
||||
high,
|
||||
}
|
||||
|
||||
/// Extension pour optimiser les widgets
|
||||
extension WidgetOptimization on Widget {
|
||||
/// Optimise ce widget
|
||||
Widget optimized({
|
||||
String? key,
|
||||
bool forceRepaintBoundary = false,
|
||||
bool addSemantics = true,
|
||||
}) {
|
||||
return PerformanceOptimizer.optimizeWidget(
|
||||
this,
|
||||
key: key,
|
||||
forceRepaintBoundary: forceRepaintBoundary,
|
||||
addSemantics: addSemantics,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
/// Service de mise en cache intelligent pour optimiser les performances
|
||||
///
|
||||
/// Fonctionnalités :
|
||||
/// - Cache multi-niveaux (mémoire + stockage)
|
||||
/// - Expiration automatique des données
|
||||
/// - Invalidation intelligente
|
||||
/// - Compression des données
|
||||
/// - Statistiques de cache
|
||||
@singleton
|
||||
class SmartCacheService {
|
||||
static const String _tag = 'SmartCacheService';
|
||||
|
||||
/// Cache en mémoire (niveau 1)
|
||||
final Map<String, CacheEntry> _memoryCache = {};
|
||||
|
||||
/// Instance SharedPreferences pour le cache persistant
|
||||
SharedPreferences? _prefs;
|
||||
|
||||
/// Statistiques du cache
|
||||
final CacheStats _stats = CacheStats();
|
||||
|
||||
/// Taille maximale du cache mémoire (nombre d'entrées)
|
||||
static const int _maxMemoryCacheSize = 100;
|
||||
|
||||
/// Durée par défaut de validité du cache
|
||||
static const Duration _defaultCacheDuration = Duration(minutes: 15);
|
||||
|
||||
/// Initialise le service de cache
|
||||
Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
await _cleanExpiredEntries();
|
||||
debugPrint('$_tag: Service initialized');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// OPÉRATIONS DE CACHE PRINCIPALES
|
||||
// ========================================
|
||||
|
||||
/// Met en cache une valeur avec une clé
|
||||
Future<void> put<T>(
|
||||
String key,
|
||||
T value, {
|
||||
Duration? duration,
|
||||
CacheLevel level = CacheLevel.both,
|
||||
bool compress = false,
|
||||
}) async {
|
||||
final entry = CacheEntry(
|
||||
key: key,
|
||||
value: value,
|
||||
timestamp: DateTime.now(),
|
||||
duration: duration ?? _defaultCacheDuration,
|
||||
compressed: compress,
|
||||
);
|
||||
|
||||
// Cache mémoire
|
||||
if (level == CacheLevel.memory || level == CacheLevel.both) {
|
||||
_putInMemory(key, entry);
|
||||
}
|
||||
|
||||
// Cache persistant
|
||||
if (level == CacheLevel.storage || level == CacheLevel.both) {
|
||||
await _putInStorage(key, entry);
|
||||
}
|
||||
|
||||
_stats.incrementWrites();
|
||||
debugPrint('$_tag: Cached $key (level: $level)');
|
||||
}
|
||||
|
||||
/// Récupère une valeur du cache
|
||||
Future<T?> get<T>(String key, {CacheLevel level = CacheLevel.both}) async {
|
||||
CacheEntry? entry;
|
||||
|
||||
// Essayer d'abord le cache mémoire (plus rapide)
|
||||
if (level == CacheLevel.memory || level == CacheLevel.both) {
|
||||
entry = _getFromMemory(key);
|
||||
if (entry != null && !entry.isExpired) {
|
||||
_stats.incrementHits();
|
||||
debugPrint('$_tag: Memory cache hit for $key');
|
||||
return entry.value as T?;
|
||||
}
|
||||
}
|
||||
|
||||
// Essayer le cache persistant
|
||||
if (level == CacheLevel.storage || level == CacheLevel.both) {
|
||||
entry = await _getFromStorage(key);
|
||||
if (entry != null && !entry.isExpired) {
|
||||
// Remettre en cache mémoire pour les prochains accès
|
||||
_putInMemory(key, entry);
|
||||
_stats.incrementHits();
|
||||
debugPrint('$_tag: Storage cache hit for $key');
|
||||
return entry.value as T?;
|
||||
}
|
||||
}
|
||||
|
||||
_stats.incrementMisses();
|
||||
debugPrint('$_tag: Cache miss for $key');
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Vérifie si une clé existe dans le cache
|
||||
Future<bool> contains(String key, {CacheLevel level = CacheLevel.both}) async {
|
||||
if (level == CacheLevel.memory || level == CacheLevel.both) {
|
||||
final entry = _getFromMemory(key);
|
||||
if (entry != null && !entry.isExpired) return true;
|
||||
}
|
||||
|
||||
if (level == CacheLevel.storage || level == CacheLevel.both) {
|
||||
final entry = await _getFromStorage(key);
|
||||
if (entry != null && !entry.isExpired) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Supprime une entrée du cache
|
||||
Future<void> remove(String key, {CacheLevel level = CacheLevel.both}) async {
|
||||
if (level == CacheLevel.memory || level == CacheLevel.both) {
|
||||
_memoryCache.remove(key);
|
||||
}
|
||||
|
||||
if (level == CacheLevel.storage || level == CacheLevel.both) {
|
||||
await _prefs?.remove(_getStorageKey(key));
|
||||
}
|
||||
|
||||
debugPrint('$_tag: Removed $key from cache');
|
||||
}
|
||||
|
||||
/// Vide complètement le cache
|
||||
Future<void> clear({CacheLevel level = CacheLevel.both}) async {
|
||||
if (level == CacheLevel.memory || level == CacheLevel.both) {
|
||||
_memoryCache.clear();
|
||||
}
|
||||
|
||||
if (level == CacheLevel.storage || level == CacheLevel.both) {
|
||||
final keys = _prefs?.getKeys().where((k) => k.startsWith('cache_')).toList() ?? [];
|
||||
for (final key in keys) {
|
||||
await _prefs?.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
_stats.reset();
|
||||
debugPrint('$_tag: Cache cleared (level: $level)');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CACHE MÉMOIRE
|
||||
// ========================================
|
||||
|
||||
void _putInMemory(String key, CacheEntry entry) {
|
||||
// Vérifier la taille du cache et nettoyer si nécessaire
|
||||
if (_memoryCache.length >= _maxMemoryCacheSize) {
|
||||
_evictOldestMemoryEntry();
|
||||
}
|
||||
|
||||
_memoryCache[key] = entry;
|
||||
}
|
||||
|
||||
CacheEntry? _getFromMemory(String key) {
|
||||
return _memoryCache[key];
|
||||
}
|
||||
|
||||
void _evictOldestMemoryEntry() {
|
||||
if (_memoryCache.isEmpty) return;
|
||||
|
||||
String? oldestKey;
|
||||
DateTime? oldestTime;
|
||||
|
||||
for (final entry in _memoryCache.entries) {
|
||||
if (oldestTime == null || entry.value.timestamp.isBefore(oldestTime)) {
|
||||
oldestTime = entry.value.timestamp;
|
||||
oldestKey = entry.key;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldestKey != null) {
|
||||
_memoryCache.remove(oldestKey);
|
||||
debugPrint('$_tag: Evicted oldest memory entry: $oldestKey');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CACHE PERSISTANT
|
||||
// ========================================
|
||||
|
||||
Future<void> _putInStorage(String key, CacheEntry entry) async {
|
||||
final storageKey = _getStorageKey(key);
|
||||
final jsonData = entry.toJson();
|
||||
await _prefs?.setString(storageKey, jsonEncode(jsonData));
|
||||
}
|
||||
|
||||
Future<CacheEntry?> _getFromStorage(String key) async {
|
||||
final storageKey = _getStorageKey(key);
|
||||
final jsonString = _prefs?.getString(storageKey);
|
||||
|
||||
if (jsonString == null) return null;
|
||||
|
||||
try {
|
||||
final jsonData = jsonDecode(jsonString) as Map<String, dynamic>;
|
||||
return CacheEntry.fromJson(jsonData);
|
||||
} catch (e) {
|
||||
debugPrint('$_tag: Error deserializing cache entry $key: $e');
|
||||
await _prefs?.remove(storageKey);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _getStorageKey(String key) => 'cache_$key';
|
||||
|
||||
// ========================================
|
||||
// NETTOYAGE ET MAINTENANCE
|
||||
// ========================================
|
||||
|
||||
/// Nettoie les entrées expirées
|
||||
Future<void> _cleanExpiredEntries() async {
|
||||
// Nettoyer le cache mémoire
|
||||
final expiredMemoryKeys = _memoryCache.entries
|
||||
.where((entry) => entry.value.isExpired)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
|
||||
for (final key in expiredMemoryKeys) {
|
||||
_memoryCache.remove(key);
|
||||
}
|
||||
|
||||
// Nettoyer le cache persistant
|
||||
final allKeys = _prefs?.getKeys().where((k) => k.startsWith('cache_')).toList() ?? [];
|
||||
int cleanedCount = 0;
|
||||
|
||||
for (final storageKey in allKeys) {
|
||||
final key = storageKey.substring(6); // Enlever 'cache_'
|
||||
final entry = await _getFromStorage(key);
|
||||
if (entry?.isExpired == true) {
|
||||
await _prefs?.remove(storageKey);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('$_tag: Cleaned ${expiredMemoryKeys.length} memory entries and $cleanedCount storage entries');
|
||||
}
|
||||
|
||||
/// Nettoie périodiquement le cache
|
||||
void startPeriodicCleanup() {
|
||||
Timer.periodic(const Duration(minutes: 30), (_) {
|
||||
_cleanExpiredEntries();
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// STATISTIQUES
|
||||
// ========================================
|
||||
|
||||
/// Obtient les statistiques du cache
|
||||
CacheStats getStats() => _stats;
|
||||
|
||||
/// Obtient des informations détaillées sur le cache
|
||||
Future<CacheInfo> getCacheInfo() async {
|
||||
final memorySize = _memoryCache.length;
|
||||
final storageKeys = _prefs?.getKeys().where((k) => k.startsWith('cache_')).length ?? 0;
|
||||
|
||||
return CacheInfo(
|
||||
memoryEntries: memorySize,
|
||||
storageEntries: storageKeys,
|
||||
stats: _stats,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Niveaux de cache
|
||||
enum CacheLevel {
|
||||
memory, // Cache en mémoire uniquement
|
||||
storage, // Cache persistant uniquement
|
||||
both, // Les deux niveaux
|
||||
}
|
||||
|
||||
/// Entrée de cache
|
||||
class CacheEntry {
|
||||
final String key;
|
||||
final dynamic value;
|
||||
final DateTime timestamp;
|
||||
final Duration duration;
|
||||
final bool compressed;
|
||||
|
||||
CacheEntry({
|
||||
required this.key,
|
||||
required this.value,
|
||||
required this.timestamp,
|
||||
required this.duration,
|
||||
this.compressed = false,
|
||||
});
|
||||
|
||||
bool get isExpired => DateTime.now().difference(timestamp) > duration;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'key': key,
|
||||
'value': value,
|
||||
'timestamp': timestamp.millisecondsSinceEpoch,
|
||||
'duration': duration.inMilliseconds,
|
||||
'compressed': compressed,
|
||||
};
|
||||
|
||||
factory CacheEntry.fromJson(Map<String, dynamic> json) => CacheEntry(
|
||||
key: json['key'],
|
||||
value: json['value'],
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp']),
|
||||
duration: Duration(milliseconds: json['duration']),
|
||||
compressed: json['compressed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Statistiques du cache
|
||||
class CacheStats {
|
||||
int _hits = 0;
|
||||
int _misses = 0;
|
||||
int _writes = 0;
|
||||
|
||||
int get hits => _hits;
|
||||
int get misses => _misses;
|
||||
int get writes => _writes;
|
||||
|
||||
double get hitRate => (_hits + _misses) > 0 ? _hits / (_hits + _misses) : 0.0;
|
||||
|
||||
void incrementHits() => _hits++;
|
||||
void incrementMisses() => _misses++;
|
||||
void incrementWrites() => _writes++;
|
||||
|
||||
void reset() {
|
||||
_hits = 0;
|
||||
_misses = 0;
|
||||
_writes = 0;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'CacheStats(hits: $_hits, misses: $_misses, writes: $_writes, hitRate: ${(hitRate * 100).toStringAsFixed(1)}%)';
|
||||
}
|
||||
|
||||
/// Informations sur le cache
|
||||
class CacheInfo {
|
||||
final int memoryEntries;
|
||||
final int storageEntries;
|
||||
final CacheStats stats;
|
||||
|
||||
CacheInfo({
|
||||
required this.memoryEntries,
|
||||
required this.storageEntries,
|
||||
required this.stats,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => 'CacheInfo(memory: $memoryEntries, storage: $storageEntries, $stats)';
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/payment_model.dart';
|
||||
import '../models/wave_checkout_session_model.dart';
|
||||
import 'wave_payment_service.dart';
|
||||
import 'api_service.dart';
|
||||
|
||||
/// Service d'intégration complète Wave Money
|
||||
/// Gère les paiements, webhooks, et synchronisation
|
||||
@LazySingleton()
|
||||
class WaveIntegrationService {
|
||||
final WavePaymentService _wavePaymentService;
|
||||
final ApiService _apiService;
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
// Stream controllers pour les événements de paiement
|
||||
final _paymentStatusController = StreamController<PaymentStatusUpdate>.broadcast();
|
||||
final _webhookController = StreamController<WaveWebhookData>.broadcast();
|
||||
|
||||
WaveIntegrationService(
|
||||
this._wavePaymentService,
|
||||
this._apiService,
|
||||
this._prefs,
|
||||
);
|
||||
|
||||
/// Stream des mises à jour de statut de paiement
|
||||
Stream<PaymentStatusUpdate> get paymentStatusUpdates => _paymentStatusController.stream;
|
||||
|
||||
/// Stream des webhooks Wave
|
||||
Stream<WaveWebhookData> get webhookUpdates => _webhookController.stream;
|
||||
|
||||
/// Initie un paiement Wave complet avec suivi
|
||||
Future<WavePaymentResult> initiateWavePayment({
|
||||
required String cotisationId,
|
||||
required double montant,
|
||||
required String numeroTelephone,
|
||||
String? nomPayeur,
|
||||
String? emailPayeur,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) async {
|
||||
try {
|
||||
// 1. Créer la session Wave
|
||||
final session = await _wavePaymentService.createCheckoutSession(
|
||||
montant: montant,
|
||||
devise: 'XOF',
|
||||
successUrl: 'https://unionflow.app/payment/success',
|
||||
errorUrl: 'https://unionflow.app/payment/error',
|
||||
typePaiement: 'COTISATION',
|
||||
description: 'Paiement cotisation $cotisationId',
|
||||
referenceExterne: cotisationId,
|
||||
);
|
||||
|
||||
// 2. Créer le modèle de paiement
|
||||
final payment = PaymentModel(
|
||||
id: session.id ?? session.waveSessionId,
|
||||
cotisationId: cotisationId,
|
||||
numeroReference: session.waveSessionId,
|
||||
montant: montant,
|
||||
codeDevise: 'XOF',
|
||||
methodePaiement: 'WAVE',
|
||||
statut: 'EN_ATTENTE',
|
||||
dateTransaction: DateTime.now(),
|
||||
numeroTransaction: session.waveSessionId,
|
||||
referencePaiement: session.referenceExterne,
|
||||
operateurMobileMoney: 'WAVE',
|
||||
numeroTelephone: numeroTelephone,
|
||||
nomPayeur: nomPayeur,
|
||||
emailPayeur: emailPayeur,
|
||||
metadonnees: {
|
||||
'wave_session_id': session.waveSessionId,
|
||||
'wave_checkout_url': session.waveUrl,
|
||||
'cotisation_id': cotisationId,
|
||||
'numero_telephone': numeroTelephone,
|
||||
'source': 'unionflow_mobile',
|
||||
...?metadata,
|
||||
},
|
||||
dateCreation: DateTime.now(),
|
||||
);
|
||||
|
||||
// 3. Sauvegarder localement pour suivi
|
||||
await _savePaymentLocally(payment);
|
||||
|
||||
// 4. Démarrer le suivi du paiement
|
||||
_startPaymentTracking(payment.id, session.waveSessionId);
|
||||
|
||||
return WavePaymentResult(
|
||||
success: true,
|
||||
payment: payment,
|
||||
session: session,
|
||||
checkoutUrl: session.waveUrl,
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
return WavePaymentResult(
|
||||
success: false,
|
||||
errorMessage: 'Erreur lors de l\'initiation du paiement: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifie le statut d'un paiement Wave
|
||||
Future<PaymentModel?> checkPaymentStatus(String paymentId) async {
|
||||
try {
|
||||
// Récupérer depuis le cache local d'abord
|
||||
final localPayment = await _getLocalPayment(paymentId);
|
||||
if (localPayment != null && localPayment.isCompleted) {
|
||||
return localPayment;
|
||||
}
|
||||
|
||||
// Vérifier avec l'API Wave
|
||||
final sessionId = localPayment?.metadonnees?['wave_session_id'] as String?;
|
||||
if (sessionId != null) {
|
||||
final session = await _wavePaymentService.getCheckoutSession(sessionId);
|
||||
final updatedPayment = await _wavePaymentService.getPaymentStatus(sessionId);
|
||||
|
||||
// Mettre à jour le cache local
|
||||
await _updateLocalPayment(updatedPayment);
|
||||
|
||||
// Notifier les listeners
|
||||
_paymentStatusController.add(PaymentStatusUpdate(
|
||||
paymentId: paymentId,
|
||||
status: updatedPayment.statut,
|
||||
payment: updatedPayment,
|
||||
));
|
||||
|
||||
return updatedPayment;
|
||||
}
|
||||
|
||||
return localPayment;
|
||||
} catch (e) {
|
||||
throw WavePaymentException('Erreur lors de la vérification du statut: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Traite un webhook Wave reçu
|
||||
Future<void> processWaveWebhook(Map<String, dynamic> webhookData) async {
|
||||
try {
|
||||
final webhook = WaveWebhookData.fromJson(webhookData);
|
||||
|
||||
// Valider la signature du webhook (sécurité)
|
||||
if (!await _validateWebhookSignature(webhookData)) {
|
||||
throw WavePaymentException('Signature webhook invalide');
|
||||
}
|
||||
|
||||
// Traiter selon le type d'événement
|
||||
switch (webhook.eventType) {
|
||||
case 'payment.completed':
|
||||
await _handlePaymentCompleted(webhook);
|
||||
break;
|
||||
case 'payment.failed':
|
||||
await _handlePaymentFailed(webhook);
|
||||
break;
|
||||
case 'payment.cancelled':
|
||||
await _handlePaymentCancelled(webhook);
|
||||
break;
|
||||
default:
|
||||
print('Type de webhook non géré: ${webhook.eventType}');
|
||||
}
|
||||
|
||||
// Notifier les listeners
|
||||
_webhookController.add(webhook);
|
||||
|
||||
} catch (e) {
|
||||
throw WavePaymentException('Erreur lors du traitement du webhook: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère l'historique des paiements Wave
|
||||
Future<List<PaymentModel>> getWavePaymentHistory({
|
||||
String? cotisationId,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
int limit = 50,
|
||||
}) async {
|
||||
try {
|
||||
// Récupérer depuis le cache local
|
||||
final localPayments = await _getLocalPayments(
|
||||
cotisationId: cotisationId,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
limit: limit,
|
||||
);
|
||||
|
||||
// Synchroniser avec le serveur si nécessaire
|
||||
if (await _shouldSyncWithServer()) {
|
||||
final serverPayments = await _apiService.getPaymentHistory(
|
||||
methodePaiement: 'WAVE',
|
||||
cotisationId: cotisationId,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
limit: limit,
|
||||
);
|
||||
|
||||
// Fusionner et mettre à jour le cache
|
||||
await _mergeAndCachePayments(serverPayments);
|
||||
return serverPayments;
|
||||
}
|
||||
|
||||
return localPayments;
|
||||
} catch (e) {
|
||||
throw WavePaymentException('Erreur lors de la récupération de l\'historique: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Calcule les statistiques des paiements Wave
|
||||
Future<WavePaymentStats> getWavePaymentStats({
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
}) async {
|
||||
try {
|
||||
final payments = await getWavePaymentHistory(
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
);
|
||||
|
||||
final completedPayments = payments.where((p) => p.isSuccessful).toList();
|
||||
final failedPayments = payments.where((p) => p.isFailed).toList();
|
||||
final pendingPayments = payments.where((p) => p.isPending).toList();
|
||||
|
||||
final totalAmount = completedPayments.fold<double>(
|
||||
0.0,
|
||||
(sum, payment) => sum + payment.montant,
|
||||
);
|
||||
|
||||
final totalFees = completedPayments.fold<double>(
|
||||
0.0,
|
||||
(sum, payment) => sum + (payment.fraisTransaction ?? 0.0),
|
||||
);
|
||||
|
||||
return WavePaymentStats(
|
||||
totalPayments: payments.length,
|
||||
completedPayments: completedPayments.length,
|
||||
failedPayments: failedPayments.length,
|
||||
pendingPayments: pendingPayments.length,
|
||||
totalAmount: totalAmount,
|
||||
totalFees: totalFees,
|
||||
averageAmount: completedPayments.isNotEmpty
|
||||
? totalAmount / completedPayments.length
|
||||
: 0.0,
|
||||
successRate: payments.isNotEmpty
|
||||
? (completedPayments.length / payments.length) * 100
|
||||
: 0.0,
|
||||
);
|
||||
} catch (e) {
|
||||
throw WavePaymentException('Erreur lors du calcul des statistiques: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Démarre le suivi d'un paiement
|
||||
void _startPaymentTracking(String paymentId, String sessionId) {
|
||||
Timer.periodic(const Duration(seconds: 10), (timer) async {
|
||||
try {
|
||||
final payment = await checkPaymentStatus(paymentId);
|
||||
if (payment != null && (payment.isCompleted || payment.isFailed)) {
|
||||
timer.cancel();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Erreur lors du suivi du paiement $paymentId: $e');
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Gestion des événements webhook
|
||||
Future<void> _handlePaymentCompleted(WaveWebhookData webhook) async {
|
||||
final paymentId = webhook.data['payment_id'] as String?;
|
||||
if (paymentId != null) {
|
||||
final payment = await _getLocalPayment(paymentId);
|
||||
if (payment != null) {
|
||||
final updatedPayment = payment.copyWith(
|
||||
statut: 'CONFIRME',
|
||||
dateModification: DateTime.now(),
|
||||
);
|
||||
await _updateLocalPayment(updatedPayment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePaymentFailed(WaveWebhookData webhook) async {
|
||||
final paymentId = webhook.data['payment_id'] as String?;
|
||||
if (paymentId != null) {
|
||||
final payment = await _getLocalPayment(paymentId);
|
||||
if (payment != null) {
|
||||
final updatedPayment = payment.copyWith(
|
||||
statut: 'ECHEC',
|
||||
messageErreur: webhook.data['error_message'] as String?,
|
||||
dateModification: DateTime.now(),
|
||||
);
|
||||
await _updateLocalPayment(updatedPayment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePaymentCancelled(WaveWebhookData webhook) async {
|
||||
final paymentId = webhook.data['payment_id'] as String?;
|
||||
if (paymentId != null) {
|
||||
final payment = await _getLocalPayment(paymentId);
|
||||
if (payment != null) {
|
||||
final updatedPayment = payment.copyWith(
|
||||
statut: 'ANNULE',
|
||||
dateModification: DateTime.now(),
|
||||
);
|
||||
await _updateLocalPayment(updatedPayment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Méthodes de cache local
|
||||
Future<void> _savePaymentLocally(PaymentModel payment) async {
|
||||
final payments = await _getLocalPayments();
|
||||
payments.add(payment);
|
||||
await _prefs.setString('wave_payments', jsonEncode(payments.map((p) => p.toJson()).toList()));
|
||||
}
|
||||
|
||||
Future<PaymentModel?> _getLocalPayment(String paymentId) async {
|
||||
final payments = await _getLocalPayments();
|
||||
try {
|
||||
return payments.firstWhere((p) => p.id == paymentId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<PaymentModel>> _getLocalPayments({
|
||||
String? cotisationId,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
int? limit,
|
||||
}) async {
|
||||
final paymentsJson = _prefs.getString('wave_payments');
|
||||
if (paymentsJson == null) return [];
|
||||
|
||||
final paymentsList = jsonDecode(paymentsJson) as List;
|
||||
var payments = paymentsList.map((json) => PaymentModel.fromJson(json)).toList();
|
||||
|
||||
// Filtrer selon les critères
|
||||
if (cotisationId != null) {
|
||||
payments = payments.where((p) => p.cotisationId == cotisationId).toList();
|
||||
}
|
||||
if (startDate != null) {
|
||||
payments = payments.where((p) => p.dateTransaction.isAfter(startDate)).toList();
|
||||
}
|
||||
if (endDate != null) {
|
||||
payments = payments.where((p) => p.dateTransaction.isBefore(endDate)).toList();
|
||||
}
|
||||
|
||||
// Trier par date décroissante
|
||||
payments.sort((a, b) => b.dateTransaction.compareTo(a.dateTransaction));
|
||||
|
||||
// Limiter le nombre de résultats
|
||||
if (limit != null && payments.length > limit) {
|
||||
payments = payments.take(limit).toList();
|
||||
}
|
||||
|
||||
return payments;
|
||||
}
|
||||
|
||||
Future<void> _updateLocalPayment(PaymentModel payment) async {
|
||||
final payments = await _getLocalPayments();
|
||||
final index = payments.indexWhere((p) => p.id == payment.id);
|
||||
if (index != -1) {
|
||||
payments[index] = payment;
|
||||
await _prefs.setString('wave_payments', jsonEncode(payments.map((p) => p.toJson()).toList()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _mergeAndCachePayments(List<PaymentModel> serverPayments) async {
|
||||
final localPayments = await _getLocalPayments();
|
||||
final mergedPayments = <String, PaymentModel>{};
|
||||
|
||||
// Ajouter les paiements locaux
|
||||
for (final payment in localPayments) {
|
||||
mergedPayments[payment.id] = payment;
|
||||
}
|
||||
|
||||
// Fusionner avec les paiements du serveur (priorité au serveur)
|
||||
for (final payment in serverPayments) {
|
||||
mergedPayments[payment.id] = payment;
|
||||
}
|
||||
|
||||
await _prefs.setString(
|
||||
'wave_payments',
|
||||
jsonEncode(mergedPayments.values.map((p) => p.toJson()).toList()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _shouldSyncWithServer() async {
|
||||
final lastSync = _prefs.getInt('last_wave_sync') ?? 0;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
const syncInterval = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
return (now - lastSync) > syncInterval;
|
||||
}
|
||||
|
||||
Future<bool> _validateWebhookSignature(Map<String, dynamic> webhookData) async {
|
||||
// TODO: Implémenter la validation de signature Wave
|
||||
// Pour l'instant, on retourne true (à sécuriser en production)
|
||||
return true;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_paymentStatusController.close();
|
||||
_webhookController.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Résultat d'un paiement Wave
|
||||
class WavePaymentResult {
|
||||
final bool success;
|
||||
final PaymentModel? payment;
|
||||
final WaveCheckoutSessionModel? session;
|
||||
final String? checkoutUrl;
|
||||
final String? errorMessage;
|
||||
|
||||
WavePaymentResult({
|
||||
required this.success,
|
||||
this.payment,
|
||||
this.session,
|
||||
this.checkoutUrl,
|
||||
this.errorMessage,
|
||||
});
|
||||
}
|
||||
|
||||
/// Mise à jour de statut de paiement
|
||||
class PaymentStatusUpdate {
|
||||
final String paymentId;
|
||||
final String status;
|
||||
final PaymentModel payment;
|
||||
|
||||
PaymentStatusUpdate({
|
||||
required this.paymentId,
|
||||
required this.status,
|
||||
required this.payment,
|
||||
});
|
||||
}
|
||||
|
||||
/// Données de webhook Wave
|
||||
class WaveWebhookData {
|
||||
final String eventType;
|
||||
final String eventId;
|
||||
final DateTime timestamp;
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
WaveWebhookData({
|
||||
required this.eventType,
|
||||
required this.eventId,
|
||||
required this.timestamp,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
factory WaveWebhookData.fromJson(Map<String, dynamic> json) {
|
||||
return WaveWebhookData(
|
||||
eventType: json['event_type'] as String,
|
||||
eventId: json['event_id'] as String,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
data: json['data'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques des paiements Wave
|
||||
class WavePaymentStats {
|
||||
final int totalPayments;
|
||||
final int completedPayments;
|
||||
final int failedPayments;
|
||||
final int pendingPayments;
|
||||
final double totalAmount;
|
||||
final double totalFees;
|
||||
final double averageAmount;
|
||||
final double successRate;
|
||||
|
||||
WavePaymentStats({
|
||||
required this.totalPayments,
|
||||
required this.completedPayments,
|
||||
required this.failedPayments,
|
||||
required this.pendingPayments,
|
||||
required this.totalAmount,
|
||||
required this.totalFees,
|
||||
required this.averageAmount,
|
||||
required this.successRate,
|
||||
});
|
||||
}
|
||||
|
||||
/// Exception spécifique aux paiements Wave
|
||||
class WavePaymentException implements Exception {
|
||||
final String message;
|
||||
final String? code;
|
||||
final dynamic originalError;
|
||||
|
||||
WavePaymentException(this.message, {this.code, this.originalError});
|
||||
|
||||
@override
|
||||
String toString() => 'WavePaymentException: $message';
|
||||
}
|
||||
Reference in New Issue
Block a user