feat: Version 3.3.4 - Nouvelle architecture pages, optimisations widgets Flutter et API

- Mise à jour VERSION vers 3.3.4
- Optimisations et révisions architecture API (deploy-api.sh, scripts de migration)
- Ajout documentation Stripe Tap to Pay complète
- Migration vers polices Inter Variable pour Flutter
- Optimisations build Android et nettoyage fichiers temporaires
- Amélioration système de déploiement avec gestion backups
- Ajout scripts CRON et migrations base de données

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-10-05 20:11:15 +02:00
parent 242a90720e
commit b6584c83fa
1625 changed files with 145669 additions and 51249 deletions

View File

@@ -10,9 +10,14 @@ import 'dart:math' as math;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:shared_preferences/shared_preferences.dart';
// Import conditionnel pour le web
import 'package:universal_html/html.dart' as html;
// Import des repositories pour reset du cache
import 'package:geosector_app/app.dart' show passageRepository, sectorRepository, membreRepository;
// Import des services pour la gestion de session F5
import 'package:geosector_app/core/services/current_user_service.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/data_loading_service.dart';
class SplashPage extends StatefulWidget {
/// Action à effectuer après l'initialisation (login ou register)
@@ -130,18 +135,29 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
});
}
// Étape 2: Sauvegarder les données de pending_requests
debugPrint('💾 Sauvegarde des requêtes en attente...');
// Étape 2: Sauvegarder les données critiques (pending_requests + app_version)
debugPrint('💾 Sauvegarde des données critiques...');
List<dynamic>? pendingRequests;
String? savedAppVersion;
try {
// Sauvegarder pending_requests
if (Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
final pendingBox = Hive.box(AppKeys.pendingRequestsBoxName);
pendingRequests = pendingBox.values.toList();
debugPrint('📊 ${pendingRequests.length} requêtes en attente sauvegardées');
await pendingBox.close();
}
// Sauvegarder app_version pour éviter de perdre l'info de version
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
final settingsBox = Hive.box(AppKeys.settingsBoxName);
savedAppVersion = settingsBox.get('app_version') as String?;
if (savedAppVersion != null) {
debugPrint('📦 Version sauvegardée: $savedAppVersion');
}
}
} catch (e) {
debugPrint('⚠️ Erreur lors de la sauvegarde des requêtes: $e');
debugPrint('⚠️ Erreur lors de la sauvegarde: $e');
}
if (mounted) {
@@ -194,7 +210,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
await Future.delayed(const Duration(milliseconds: 500));
await Hive.initFlutter();
// Étape 6: Restaurer les requêtes en attente
// Étape 6: Restaurer les données critiques
if (pendingRequests != null && pendingRequests.isNotEmpty) {
debugPrint('♻️ Restauration des requêtes en attente...');
final pendingBox = await Hive.openBox(AppKeys.pendingRequestsBoxName);
@@ -204,6 +220,14 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
debugPrint('${pendingRequests.length} requêtes restaurées');
}
// Restaurer app_version pour maintenir la détection de changement de version
if (savedAppVersion != null) {
debugPrint('♻️ Restauration de la version...');
final settingsBox = await Hive.openBox(AppKeys.settingsBoxName);
await settingsBox.put('app_version', savedAppVersion);
debugPrint('✅ Version restaurée: $savedAppVersion');
}
if (mounted) {
setState(() {
_statusMessage = "Nettoyage terminé !";
@@ -211,13 +235,6 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
});
}
// Étape 7: Sauvegarder la nouvelle version
if (!manual && kIsWeb) {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('app_version', _appVersion);
debugPrint('💾 Version $_appVersion sauvegardée');
}
debugPrint('🎉 === NETTOYAGE TERMINÉ AVEC SUCCÈS === 🎉');
// Petit délai pour voir le message de succès
@@ -250,6 +267,206 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
}
}
/// Réinitialise le cache de tous les repositories après nettoyage complet
void _resetAllRepositoriesCache() {
try {
debugPrint('🔄 === RESET DU CACHE DES REPOSITORIES === 🔄');
// Reset du cache des 3 repositories qui utilisent le pattern de cache
passageRepository.resetCache();
sectorRepository.resetCache();
membreRepository.resetCache();
debugPrint('✅ Cache de tous les repositories réinitialisé');
} catch (e) {
debugPrint('⚠️ Erreur lors du reset des caches: $e');
// Ne pas faire échouer le processus si le reset échoue
}
}
/// Détecte et gère le refresh (F5) avec session existante
/// Retourne true si une session a été restaurée, false sinon
Future<bool> _handleSessionRefreshIfNeeded() async {
if (!kIsWeb) {
debugPrint('📱 Plateforme mobile - pas de gestion F5');
return false;
}
try {
debugPrint('🔍 Vérification d\'une session existante (F5)...');
// Charger l'utilisateur depuis Hive
await CurrentUserService.instance.loadFromHive();
final isLoggedIn = CurrentUserService.instance.isLoggedIn;
final displayMode = CurrentUserService.instance.displayMode;
final sessionId = CurrentUserService.instance.sessionId;
if (!isLoggedIn || sessionId == null) {
debugPrint(' Aucune session active - affichage normal de la splash');
return false;
}
debugPrint('🔄 Session active détectée - mode: $displayMode');
debugPrint('🔄 Rechargement des données depuis l\'API...');
if (mounted) {
setState(() {
_statusMessage = "Restauration de votre session...";
_progress = 0.85;
});
}
// Configurer ApiService avec le sessionId existant
ApiService.instance.setSessionId(sessionId);
// Appeler le nouvel endpoint API pour restaurer la session
final response = await ApiService.instance.get(
'/api/user/session',
queryParameters: {'mode': displayMode},
);
// Gestion des codes de retour HTTP
final statusCode = response.statusCode ?? 0;
final data = response.data as Map<String, dynamic>?;
switch (statusCode) {
case 200:
// Succès - traiter les données
if (data == null || data['success'] != true) {
debugPrint('❌ Format de réponse invalide (200 mais pas success=true)');
await CurrentUserService.instance.clearUser();
return false;
}
debugPrint('✅ Données reçues de l\'API, traitement...');
if (mounted) {
setState(() {
_statusMessage = "Chargement de vos données...";
_progress = 0.90;
});
}
// Traiter les données avec DataLoadingService
final apiData = data['data'] as Map<String, dynamic>?;
if (apiData == null) {
debugPrint('❌ Données manquantes dans la réponse');
await CurrentUserService.instance.clearUser();
return false;
}
await DataLoadingService.instance.processLoginData(apiData);
debugPrint('✅ Session restaurée avec succès');
break;
case 400:
// Paramètre mode invalide - erreur technique
debugPrint('❌ Paramètre mode invalide: $displayMode');
await CurrentUserService.instance.clearUser();
if (mounted) {
setState(() {
_statusMessage = "Erreur technique - veuillez vous reconnecter";
});
}
return false;
case 401:
// Session invalide ou expirée
debugPrint('⚠️ Session invalide ou expirée');
await CurrentUserService.instance.clearUser();
if (mounted) {
setState(() {
_statusMessage = "Session expirée - veuillez vous reconnecter";
});
}
return false;
case 403:
// Accès interdit (membre → admin) ou entité inactive
final message = data?['message'] ?? 'Accès interdit';
debugPrint('🚫 Accès interdit: $message');
await CurrentUserService.instance.clearUser();
if (mounted) {
setState(() {
_statusMessage = "Accès interdit - veuillez vous reconnecter";
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 5),
),
);
}
return false;
case 500:
// Erreur serveur
final message = data?['message'] ?? 'Erreur serveur';
debugPrint('❌ Erreur serveur: $message');
if (mounted) {
setState(() {
_statusMessage = "Erreur serveur - veuillez réessayer";
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur serveur: $message'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 5),
),
);
}
// Ne pas effacer la session en cas d'erreur serveur
return false;
default:
// Code de retour inattendu
debugPrint('❌ Code HTTP inattendu: $statusCode');
await CurrentUserService.instance.clearUser();
return false;
}
if (mounted) {
setState(() {
_statusMessage = "Session restaurée !";
_progress = 0.95;
});
}
// Petit délai pour voir le message
await Future.delayed(const Duration(milliseconds: 500));
// Rediriger vers la bonne interface selon le mode
if (!mounted) return true;
if (displayMode == 'admin') {
debugPrint('🔀 Redirection vers interface admin');
context.go('/admin/home');
} else {
debugPrint('🔀 Redirection vers interface user');
context.go('/user/field-mode');
}
return true;
} catch (e) {
debugPrint('❌ Erreur lors de la restauration de session: $e');
// En cas d'erreur, effacer la session invalide
await CurrentUserService.instance.clearUser();
if (mounted) {
setState(() {
_statusMessage = "Erreur de restauration - veuillez vous reconnecter";
_progress = 0.0;
});
}
return false;
}
}
/// Vérifie si une nouvelle version est disponible et nettoie si nécessaire
Future<void> _checkVersionAndCleanIfNeeded() async {
if (!kIsWeb) {
@@ -258,9 +475,14 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
}
try {
final prefs = await SharedPreferences.getInstance();
final lastVersion = prefs.getString('app_version') ?? '';
String lastVersion = '';
// Lire la version depuis Hive settings
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
final settingsBox = Hive.box(AppKeys.settingsBoxName);
lastVersion = settingsBox.get('app_version', defaultValue: '') as String;
}
debugPrint('🔍 Vérification de version:');
debugPrint(' Version stockée: $lastVersion');
debugPrint(' Version actuelle: $_appVersion');
@@ -269,7 +491,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
if (lastVersion.isNotEmpty && lastVersion != _appVersion) {
debugPrint('🆕 NOUVELLE VERSION DÉTECTÉE !');
debugPrint(' Migration de $lastVersion vers $_appVersion');
if (mounted) {
setState(() {
_statusMessage = "Nouvelle version détectée, mise à jour...";
@@ -278,10 +500,17 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
// Effectuer le nettoyage automatique
await _performSelectiveCleanup(manual: false);
// Reset du cache des repositories après nettoyage
_resetAllRepositoriesCache();
} else if (lastVersion.isEmpty) {
// Première installation
debugPrint('🎉 Première installation détectée');
await prefs.setString('app_version', _appVersion);
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
final settingsBox = Hive.box(AppKeys.settingsBoxName);
await settingsBox.put('app_version', _appVersion);
debugPrint('💾 Version initiale sauvegardée dans Hive: $_appVersion');
}
} else {
debugPrint('✅ Même version - pas de nettoyage nécessaire');
}
@@ -325,9 +554,6 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
try {
debugPrint('🚀 Début de l\'initialisation complète de l\'application...');
// Étape 0: Vérifier et nettoyer si nouvelle version (Web uniquement)
await _checkVersionAndCleanIfNeeded();
// Étape 1: Vérification des permissions GPS (obligatoire) - 0 à 10%
if (!kIsWeb) {
if (mounted) {
@@ -402,7 +628,20 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
// Étape 3: Ouverture des Box - 60 à 80%
await HiveService.instance.ensureBoxesAreOpen();
// NOUVEAU : Vérifier et nettoyer si nouvelle version (Web uniquement)
// Maintenant que les boxes sont ouvertes, on peut vérifier la version dans Hive
await _checkVersionAndCleanIfNeeded();
// NOUVEAU : Détecter et gérer le F5 (refresh de page web avec session existante)
final sessionRestored = await _handleSessionRefreshIfNeeded();
if (sessionRestored) {
// Session restaurée avec succès, on arrête ici
// L'utilisateur a été redirigé vers son interface
debugPrint('✅ Session restaurée via F5 - fin de l\'initialisation');
return;
}
// Gérer la box pending_requests séparément pour préserver les données
try {
debugPrint('📦 Gestion de la box pending_requests...');
@@ -907,62 +1146,66 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
const SizedBox(height: 8),
// Bouton de nettoyage du cache (en noir)
AnimatedOpacity(
opacity: _showButtons ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
child: TextButton.icon(
onPressed: _isCleaningCache ? null : () async {
// Confirmation avant nettoyage
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Nettoyer le cache ?'),
content: const Text(
'Cette action va :\n'
'• Supprimer toutes les données locales\n'
'Préserver les requêtes en attente\n'
'Forcer le rechargement de l\'application\n\n'
'Continuer ?'
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
// Bouton de nettoyage du cache (Web uniquement)
if (kIsWeb)
AnimatedOpacity(
opacity: _showButtons ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
child: TextButton.icon(
onPressed: _isCleaningCache ? null : () async {
// Confirmation avant nettoyage
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Nettoyer le cache ?'),
content: const Text(
'Cette action va :\n'
'Supprimer toutes les données locales\n'
'Préserver les requêtes en attente\n'
'• Forcer le rechargement de l\'application\n\n'
'Continuer ?'
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
child: const Text('Nettoyer'),
),
],
),
);
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
child: const Text('Nettoyer'),
),
],
),
);
if (confirm == true) {
debugPrint('👤 Utilisateur a demandé un nettoyage manuel');
await _performSelectiveCleanup(manual: true);
// Après le nettoyage, relancer l'initialisation
_startInitialization();
}
},
icon: Icon(
Icons.cleaning_services,
size: 18,
color: _isCleaningCache ? Colors.grey : Colors.black87,
),
label: Text(
_isCleaningCache ? 'Nettoyage...' : 'Nettoyer le cache',
style: TextStyle(
if (confirm == true) {
debugPrint('👤 Utilisateur a demandé un nettoyage manuel');
await _performSelectiveCleanup(manual: true);
// Reset du cache des repositories après nettoyage
_resetAllRepositoriesCache();
// Après le nettoyage, relancer l'initialisation
_startInitialization();
}
},
icon: Icon(
Icons.cleaning_services,
size: 18,
color: _isCleaningCache ? Colors.grey : Colors.black87,
fontWeight: FontWeight.w500,
),
label: Text(
_isCleaningCache ? 'Nettoyage...' : 'Nettoyer le cache',
style: TextStyle(
color: _isCleaningCache ? Colors.grey : Colors.black87,
fontWeight: FontWeight.w500,
),
),
),
),
),
],
const Spacer(flex: 1),