feat: optimisation du démarrage de l'application et de la gestion de l'authentification
- **Refonte du démarrage** : Mise en place d'un `AppInitializer` pour gérer l'initialisation asynchrone de Firebase et du cache en arrière-plan, réduisant le travail synchrone au lancement.
- **Sécurisation de l'authentification** :
- Création d'un `AppStartGate` pour gérer proprement la restauration de la session Firebase Auth et les erreurs potentielles sur le Web.
- Amélioration du `LocalUserProvider` avec un "bootstrap léger" permettant de rendre l'UID disponible immédiatement avant le chargement complet du profil.
- Ajout de protections contre les erreurs d'accès à `FirebaseAuth.instance` (notamment pour les problèmes d'interop JS sur le Web).
- **Optimisation de l'UI** :
- Remplacement du `AutoLoginWrapper` par une gestion plus robuste de la navigation post-authentification.
- Amélioration de l'`AuthGuard` pour permettre l'affichage de certains écrans (comme le calendrier) pendant le chargement des données utilisateur (`allowWhileLoading`).
- Ajout d'un écran de splash screen uniformisé (`StartupSplashScreen`).
- **Services & Cache** :
- Introduction de `CacheService` utilisant `shared_preferences` pour le stockage local léger.
- Refactoring des services (`AlertService`, `EmailService`, `FirebaseStorageManager`) pour accéder aux instances Firebase de manière plus flexible via des getters.
- Mise à jour des dépendances dans `pubspec.yaml` pour inclure `shared_preferences`.
- **Calendrier** : Ajout d'une logique de chargement initial différé des événements (`_scheduleInitialEventsLoad`) pour éviter les appels redondants au démarrage.
- **Maintenance** : Mise à jour de la version de l'application à `1.1.23` et nettoyage des fichiers de cache de déploiement.
This commit is contained in:
@@ -34,16 +34,16 @@ assets/assets/images/tshirt-incrust.webp,1737393735487,af7cb34adfca19c0b41c8eb63
|
||||
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
||||
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
||||
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
||||
version.json,1776853346038,b9cb334972abfae63e76477e574d02e1b3cdf4210fa3edf744a8d33c6250a12e
|
||||
index.html,1776853378875,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
||||
flutter_service_worker.js,1776853476352,5524fe7e227e1a0ecdd4c9b14a764638a86fe836ced8d3f80ab0817d043d436a
|
||||
flutter_bootstrap.js,1776853378859,a9fbceaa97579d418c548aaa1b4fc94284bc33ef5fc2a835d80f9a96d8d6bbd8
|
||||
assets/FontManifest.json,1776853472496,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||
assets/AssetManifest.json,1776853472495,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6
|
||||
assets/AssetManifest.bin.json,1776853472495,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53
|
||||
assets/AssetManifest.bin,1776853472495,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907
|
||||
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1776853475437,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||
assets/shaders/ink_sparkle.frag,1776853472722,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||
assets/fonts/MaterialIcons-Regular.otf,1776853475444,213705f583b626b88f6f0c7a122a13567b6492f560ad5284176ef149f0b51fef
|
||||
assets/NOTICES,1776853472497,1d9a08da58db7959b9607f0f1f342f96243af76dc608ed659614d586ec58cd79
|
||||
main.dart.js,1776853469906,1fc20606c99d6d4dde1e7d2a8a0b2e8f2e6c6f81317b8e7e4dd17d54f71a23b2
|
||||
version.json,1777974738862,518123ebb7461c8343d5ad7d08a9bc31ca5555df3d9e09d36442cad4e5a4dcaa
|
||||
index.html,1777974744949,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
||||
flutter_service_worker.js,1777974838520,527efc67156a0e2688a3aca09ef3f967cbb514258c91dc1d8ad1d6a4935e2c65
|
||||
assets/FontManifest.json,1777974834864,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||
flutter_bootstrap.js,1777974744934,09a1770005261de742912a7cf492d739d3e263d2383f53cda5ba5bac6896c39c
|
||||
assets/AssetManifest.json,1777974834864,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6
|
||||
assets/AssetManifest.bin,1777974834864,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907
|
||||
assets/AssetManifest.bin.json,1777974834864,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53
|
||||
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1777974837564,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||
assets/shaders/ink_sparkle.frag,1777974835049,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||
assets/fonts/MaterialIcons-Regular.otf,1777974837570,213705f583b626b88f6f0c7a122a13567b6492f560ad5284176ef149f0b51fef
|
||||
assets/NOTICES,1777974834870,5522e1307c65771d1fbf26fcd9dc0548c751413f42196c4acaba5ee674eede1e
|
||||
main.dart.js,1777974833676,fbb6da7a84cb69d9dfb2a92eac87571303dadec0af700067d2d66ed69db416e8
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
Toutes les modifications notables de ce projet seront documentées dans ce fichier.
|
||||
|
||||
## 04/05/2026
|
||||
Optimisation du lancement de l'application et amélioration de la gestion du cache.
|
||||
|
||||
## 22/04/2026
|
||||
Ajout de la recherche d'événements et gestion avancée de la suppression d'équipement
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// Configuration de la version de l'application
|
||||
class AppVersion {
|
||||
static const String version = '1.1.21';
|
||||
static const String version = '1.1.23';
|
||||
|
||||
/// Retourne la version complète de l'application
|
||||
static String get fullVersion => 'v$version';
|
||||
|
||||
+81
-173
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:em2rp/providers/users_provider.dart';
|
||||
import 'package:em2rp/providers/event_provider.dart';
|
||||
import 'package:em2rp/providers/equipment_provider.dart';
|
||||
@@ -19,10 +21,8 @@ import 'package:em2rp/views/event_statistics_page.dart';
|
||||
import 'package:em2rp/models/container_model.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'firebase_options.dart';
|
||||
import 'package:em2rp/services/app_initializer.dart';
|
||||
import 'utils/colors.dart';
|
||||
import 'views/my_account_page.dart';
|
||||
import 'views/user_management_page.dart';
|
||||
@@ -30,35 +30,21 @@ import 'package:provider/provider.dart';
|
||||
import 'providers/local_user_provider.dart';
|
||||
import 'views/reset_password_page.dart';
|
||||
import 'config/env.dart';
|
||||
import 'services/update_service.dart';
|
||||
import 'views/widgets/common/update_dialog.dart';
|
||||
import 'config/api_config.dart';
|
||||
import 'utils/app_start_gate.dart';
|
||||
import 'views/widgets/common/startup_splash_screen.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
||||
void main() async {
|
||||
void main() {
|
||||
// Ne pas effectuer d'initialisations asynchrones lourdes ici.
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
|
||||
// Configuration des émulateurs en mode développement
|
||||
if (ApiConfig.isDevelopment) {
|
||||
print('🔧 Mode développement activé - Utilisation des émulateurs');
|
||||
|
||||
// Configurer l'émulateur Auth
|
||||
await FirebaseAuth.instance.useAuthEmulator('localhost', 9199);
|
||||
print('✓ Auth émulateur configuré: localhost:9199');
|
||||
|
||||
// Configurer l'émulateur Firestore
|
||||
FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8088);
|
||||
print('✓ Firestore émulateur configuré: localhost:8088');
|
||||
}
|
||||
|
||||
await FirebaseAuth.instance.setPersistence(Persistence.LOCAL);
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
// Fournisseur d'initialisation de l'application (initialise Firebase et cache en tâche de fond)
|
||||
ChangeNotifierProvider<AppInitializer>(
|
||||
create: (_) => AppInitializer(),
|
||||
),
|
||||
// LocalUserProvider pour la gestion de l'authentification
|
||||
ChangeNotifierProvider<LocalUserProvider>(
|
||||
create: (context) => LocalUserProvider()),
|
||||
@@ -96,11 +82,67 @@ void main() async {
|
||||
);
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
late final Future<void> _startupFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startupFuture = _bootstrapApp();
|
||||
}
|
||||
|
||||
Future<void> _bootstrapApp() async {
|
||||
final initializer = context.read<AppInitializer>();
|
||||
final localAuthProvider = context.read<LocalUserProvider>();
|
||||
|
||||
await initializer.initialize();
|
||||
|
||||
// Attendre la première valeur d'authentification avant toute décision
|
||||
// de navigation, afin d'éviter un flash de la page login.
|
||||
await FirebaseAuth.instance.authStateChanges().first;
|
||||
|
||||
if (FirebaseAuth.instance.currentUser != null) {
|
||||
unawaited(
|
||||
localAuthProvider.loadUserData().catchError((e) {
|
||||
print('User data bootstrap failed: $e');
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// En développement, on garde la connexion automatique existante.
|
||||
if (Env.isDevelopment) {
|
||||
await localAuthProvider.signInWithEmailAndPassword(
|
||||
Env.devAdminEmail,
|
||||
Env.devAdminPassword,
|
||||
);
|
||||
unawaited(
|
||||
localAuthProvider.loadUserData().catchError((e) {
|
||||
print('Dev user bootstrap failed: $e');
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<void>(
|
||||
future: _startupFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: StartupSplashScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
return MaterialApp(
|
||||
title: 'EM2 Hub',
|
||||
theme: ThemeData(
|
||||
@@ -137,15 +179,15 @@ class MyApp extends StatelessWidget {
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
initialRoute: '/',
|
||||
routes: {
|
||||
'/': (context) => const AutoLoginWrapper(),
|
||||
'/login': (context) => const LoginPage(),
|
||||
'/alerts': (context) => const AuthGuard(child: AlertsPage()),
|
||||
'/calendar': (context) => const AuthGuard(child: CalendarPage()),
|
||||
'/calendar': (context) => const AuthGuard(
|
||||
allowWhileLoading: true, child: CalendarPage()),
|
||||
'/my_account': (context) => const AuthGuard(child: MyAccountPage()),
|
||||
'/user_management': (context) => const AuthGuard(
|
||||
requiredPermission: "view_all_users", child: UserManagementPage()),
|
||||
requiredPermission: "view_all_users",
|
||||
child: UserManagementPage()),
|
||||
'/reset_password': (context) {
|
||||
final args = ModalRoute.of(context)!.settings.arguments
|
||||
as Map<String, dynamic>;
|
||||
@@ -173,14 +215,16 @@ class MyApp extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
'/container_detail': (context) {
|
||||
final container = ModalRoute.of(context)!.settings.arguments as ContainerModel;
|
||||
final container = ModalRoute.of(context)!.settings.arguments
|
||||
as ContainerModel;
|
||||
return AuthGuard(
|
||||
requiredPermission: "view_equipment",
|
||||
child: ContainerDetailPage(container: container),
|
||||
);
|
||||
},
|
||||
'/event_preparation': (context) {
|
||||
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
|
||||
final args = ModalRoute.of(context)!.settings.arguments
|
||||
as Map<String, dynamic>;
|
||||
final event = args['event'] as EventModel;
|
||||
return AuthGuard(
|
||||
child: EventPreparationPage(
|
||||
@@ -189,148 +233,12 @@ class MyApp extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
'/event_statistics': (context) => const AuthGuard(
|
||||
requiredPermission: 'generate_reports', child: EventStatisticsPage()),
|
||||
requiredPermission: 'generate_reports',
|
||||
child: EventStatisticsPage()),
|
||||
},
|
||||
home: const AppStartGate(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AutoLoginWrapper extends StatefulWidget {
|
||||
const AutoLoginWrapper({super.key});
|
||||
|
||||
@override
|
||||
State<AutoLoginWrapper> createState() => _AutoLoginWrapperState();
|
||||
}
|
||||
|
||||
class _AutoLoginWrapperState extends State<AutoLoginWrapper> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Attendre la fin du premier build avant de naviguer
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_autoLogin();
|
||||
// Vérifier les mises à jour après un délai pour ne pas interférer avec l'autologin
|
||||
_checkForUpdateDelayed();
|
||||
});
|
||||
}
|
||||
|
||||
/// Vérifie les mises à jour après un délai
|
||||
Future<void> _checkForUpdateDelayed() async {
|
||||
try {
|
||||
// Attendre que l'app soit complètement chargée (navigation effectuée, etc.)
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final updateInfo = await UpdateService.checkForUpdate();
|
||||
|
||||
if (updateInfo != null && mounted) {
|
||||
// Attendre encore un peu pour être sûr que le bon contexte est disponible
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
if (mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: !updateInfo.forceUpdate,
|
||||
builder: (context) => UpdateDialog(updateInfo: updateInfo),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('[AutoLoginWrapper] Error checking for update: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _autoLogin() async {
|
||||
PerformanceMonitor.start('App.autoLogin');
|
||||
try {
|
||||
final localAuthProvider =
|
||||
Provider.of<LocalUserProvider>(context, listen: false);
|
||||
|
||||
// Vérifier si l'utilisateur est déjà connecté
|
||||
if (FirebaseAuth.instance.currentUser == null && Env.isDevelopment) {
|
||||
PerformanceMonitor.start('App.signIn');
|
||||
// Connexion automatique en mode développement
|
||||
await localAuthProvider.signInWithEmailAndPassword(
|
||||
Env.devAdminEmail,
|
||||
Env.devAdminPassword,
|
||||
);
|
||||
PerformanceMonitor.end('App.signIn');
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
// MODIFIÉ : Vérifier si une route spécifique est demandée dans l'URL
|
||||
// En Flutter Web, on peut vérifier window.location.hash
|
||||
final currentUri = Uri.base;
|
||||
final fragment = currentUri.fragment; // Ex: "/alerts" si URL est /#/alerts
|
||||
|
||||
print('[AutoLoginWrapper] Fragment URL: $fragment');
|
||||
|
||||
// Navigation immédiate sans attendre le chargement des données
|
||||
if (fragment.isNotEmpty && fragment != '/' && fragment != '/calendar') {
|
||||
print('[AutoLoginWrapper] Redirection vers: $fragment');
|
||||
Navigator.of(context).pushReplacementNamed(fragment);
|
||||
} else {
|
||||
// Route par défaut : calendrier
|
||||
print('[AutoLoginWrapper] Redirection vers: /calendar (défaut)');
|
||||
Navigator.of(context).pushReplacementNamed('/calendar');
|
||||
}
|
||||
|
||||
PerformanceMonitor.end('App.autoLogin');
|
||||
PerformanceMonitor.printSummary();
|
||||
|
||||
// Charger les données utilisateur en arrière-plan
|
||||
localAuthProvider.loadUserData().catchError((e) {
|
||||
print('Error loading user data: $e');
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print('Auto login failed: $e');
|
||||
PerformanceMonitor.end('App.autoLogin');
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Logo de l'application
|
||||
Image.asset(
|
||||
'assets/logos/RectangleLogoBlack.png',
|
||||
width: 200,
|
||||
height: 200,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(
|
||||
Icons.event_available,
|
||||
size: 80,
|
||||
color: AppColors.rouge,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
const CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.rouge),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Chargement...',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import '../utils/performance_monitor.dart';
|
||||
class LocalUserProvider with ChangeNotifier {
|
||||
UserModel? _currentUser;
|
||||
RoleModel? _currentRole;
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
FirebaseAuth? _auth;
|
||||
final FirebaseStorageManager _storageManager = FirebaseStorageManager();
|
||||
final DataService _dataService = DataService(apiService);
|
||||
|
||||
@@ -43,11 +43,41 @@ class LocalUserProvider with ChangeNotifier {
|
||||
|
||||
/// Charge les données de l'utilisateur actuel via Cloud Function
|
||||
Future<void> loadUserData({bool forceReload = false}) async {
|
||||
if (_auth.currentUser == null) {
|
||||
// Si FirebaseAuth n'est pas encore disponible
|
||||
final FirebaseAuth auth;
|
||||
try {
|
||||
auth = _getAuthInstance();
|
||||
} catch (e) {
|
||||
print('Auth instance not ready in loadUserData: $e');
|
||||
return;
|
||||
}
|
||||
|
||||
if (auth.currentUser == null) {
|
||||
print('No current user in Auth');
|
||||
return;
|
||||
}
|
||||
|
||||
// Bootstrap léger : rendre l'UID disponible tout de suite pour les écrans
|
||||
// qui en ont besoin, même si le profil complet n'est pas encore chargé.
|
||||
if (_currentUser == null) {
|
||||
final firebaseUser = auth.currentUser!;
|
||||
_currentUser = UserModel(
|
||||
uid: firebaseUser.uid,
|
||||
email: firebaseUser.email ?? '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
role: 'USER',
|
||||
phoneNumber: '',
|
||||
profilePhotoUrl: firebaseUser.photoURL ?? '',
|
||||
);
|
||||
_currentRole = RoleModel(
|
||||
id: 'USER',
|
||||
name: '',
|
||||
permissions: const [],
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Éviter les rechargements inutiles
|
||||
if (!forceReload && !_shouldReloadUserData()) {
|
||||
print('Using cached user data');
|
||||
@@ -62,7 +92,7 @@ class LocalUserProvider with ChangeNotifier {
|
||||
|
||||
_isLoadingUserData = true;
|
||||
PerformanceMonitor.start('LocalUserProvider.loadUserData');
|
||||
print('Loading user data for: ${_auth.currentUser!.uid}');
|
||||
print('Loading user data for: ${_auth!.currentUser!.uid}');
|
||||
try {
|
||||
// Utiliser la Cloud Function getCurrentUser
|
||||
PerformanceMonitor.start('LocalUserProvider.getCurrentUser_API');
|
||||
@@ -194,7 +224,8 @@ class LocalUserProvider with ChangeNotifier {
|
||||
Future<UserCredential> signInWithEmailAndPassword(
|
||||
String email, String password) async {
|
||||
try {
|
||||
UserCredential userCredential = await _auth.signInWithEmailAndPassword(
|
||||
final auth = _getAuthInstance();
|
||||
UserCredential userCredential = await auth.signInWithEmailAndPassword(
|
||||
email: email, password: password);
|
||||
// Note: loadUserData() sera appelé en arrière-plan dans main.dart
|
||||
// pour ne pas bloquer la navigation
|
||||
@@ -206,10 +237,25 @@ class LocalUserProvider with ChangeNotifier {
|
||||
|
||||
/// Déconnexion
|
||||
Future<void> signOut() async {
|
||||
await _auth.signOut();
|
||||
try {
|
||||
final auth = _getAuthInstance();
|
||||
await auth.signOut();
|
||||
} catch (e) {
|
||||
debugPrint('Error during signOut: $e');
|
||||
}
|
||||
clearUser();
|
||||
}
|
||||
|
||||
FirebaseAuth _getAuthInstance() {
|
||||
try {
|
||||
_auth ??= FirebaseAuth.instance;
|
||||
return _auth!;
|
||||
} catch (e, st) {
|
||||
debugPrint('[LocalUserProvider] FirebaseAuth.instance access error: $e\n$st');
|
||||
throw Exception('FirebaseAuth not available');
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifie si l'utilisateur a une permission spécifique
|
||||
bool hasPermission(String permission) {
|
||||
return _currentRole?.permissions.contains(permission) ?? false;
|
||||
|
||||
@@ -7,8 +7,8 @@ import 'api_service.dart' show FirebaseFunctionsApiService;
|
||||
/// Architecture simplifiée : le client appelle uniquement les Cloud Functions
|
||||
/// Toute la logique métier est gérée côté backend
|
||||
class AlertService {
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
FirebaseFirestore get _firestore => FirebaseFirestore.instance;
|
||||
FirebaseAuth get _auth => FirebaseAuth.instance;
|
||||
|
||||
/// Stream des alertes pour l'utilisateur connecté
|
||||
Stream<List<AlertModel>> getAlertsStream() {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../firebase_options.dart';
|
||||
import '../config/api_config.dart';
|
||||
import 'cache_service.dart';
|
||||
|
||||
/// Service responsable des initialisations lourdes en tâche de fond.
|
||||
///
|
||||
/// Objectif : réduire au maximum le travail synchrone dans main(),
|
||||
/// afficher immédiatement une UI minimale, puis effectuer l'init asynchrone.
|
||||
class AppInitializer with ChangeNotifier {
|
||||
bool _isInitialized = false;
|
||||
bool _isInitializing = false;
|
||||
|
||||
bool get isInitialized => _isInitialized;
|
||||
bool get isInitializing => _isInitializing;
|
||||
|
||||
final CacheService cacheService = CacheService();
|
||||
|
||||
/// Démarre l'initialisation asynchrone. Idempotent.
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized || _isInitializing) return;
|
||||
_isInitializing = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// Initialiser Firebase
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
|
||||
// Configurer les émulateurs en dev si demandé
|
||||
if (ApiConfig.isDevelopment) {
|
||||
try {
|
||||
await FirebaseAuth.instance.useAuthEmulator('localhost', 9199);
|
||||
FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8088);
|
||||
} catch (e) {
|
||||
// Ignorer si non supporté
|
||||
if (kDebugMode) print('Emulator setup failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialiser le cache local sans bloquer l'écran de démarrage.
|
||||
unawaited(cacheService.init());
|
||||
|
||||
// Précharger des assets critiques de façon asynchrone
|
||||
unawaited(_preloadAssets());
|
||||
|
||||
// TODO: lancer ici d'autres initialisations non bloquantes
|
||||
|
||||
_isInitialized = true;
|
||||
_isInitializing = false;
|
||||
notifyListeners();
|
||||
} catch (e, st) {
|
||||
if (kDebugMode) print('AppInitializer failed: $e\n$st');
|
||||
_isInitializing = false;
|
||||
// Ne rethrow pas pour éviter de planter l'app; laisser l'UI gérer les erreurs.
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _preloadAssets() async {
|
||||
try {
|
||||
// Charger quelques assets en mémoire pour rendre l'affichage initial fluide
|
||||
await rootBundle.load('assets/logos/RectangleLogoBlack.png');
|
||||
await rootBundle.load('assets/logos/SquareLogoWhite.png');
|
||||
} catch (e) {
|
||||
if (kDebugMode) print('Preload assets failed: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Service simple de cache local basé sur SharedPreferences.
|
||||
///
|
||||
/// Fonctionne sur mobile et sur Flutter Web pour conserver des données
|
||||
/// locales légères quand cela apporte une vraie valeur.
|
||||
class CacheService {
|
||||
SharedPreferences? _prefs;
|
||||
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
bool ready() => _prefs != null;
|
||||
|
||||
Future<void> setJson(String key, Map<String, dynamic> value) async {
|
||||
if (_prefs == null) return;
|
||||
await _prefs!.setString(key, jsonEncode(value));
|
||||
}
|
||||
|
||||
Map<String, dynamic>? getJson(String key) {
|
||||
if (_prefs == null) return null;
|
||||
final s = _prefs!.getString(key);
|
||||
if (s == null) return null;
|
||||
try {
|
||||
return jsonDecode(s) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
if (kDebugMode) print('CacheService getJson error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setString(String key, String value) async {
|
||||
if (_prefs == null) return;
|
||||
await _prefs!.setString(key, value);
|
||||
}
|
||||
|
||||
String? getString(String key) => _prefs?.getString(key);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:firebase_auth/firebase_auth.dart';
|
||||
|
||||
/// Service d'envoi d'emails via Cloud Functions
|
||||
class EmailService {
|
||||
final FirebaseFunctions _functions = FirebaseFunctions.instanceFor(region: 'europe-west9');
|
||||
FirebaseFunctions get _functions => FirebaseFunctions.instanceFor(region: 'europe-west9');
|
||||
|
||||
/// Envoie un email d'alerte à un utilisateur
|
||||
///
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../views/login_page.dart';
|
||||
import '../utils/colors.dart';
|
||||
|
||||
/// Gate de démarrage qui attend la restauration Firebase Auth avant
|
||||
/// d'afficher soit le contenu connecté, soit la page de connexion.
|
||||
class AppStartGate extends StatelessWidget {
|
||||
const AppStartGate({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Sur le web, certaines erreurs natives (ex: cookies tiers bloqués)
|
||||
// peuvent faire remonter une FirebaseException sur le stream d'auth.
|
||||
// Pour éviter que StreamBuilder reçoive une erreur qui casse le build
|
||||
// (TypeError JS interop), on "handleError" et on transforme l'erreur
|
||||
// en une valeur nulle (pas d'utilisateur) afin de garder l'app stable.
|
||||
// Accès protégé à `FirebaseAuth.instance` — sur le web certaines erreurs
|
||||
// d'interop JS peuvent produire des TypeError non compatibles. Nous
|
||||
// attrapons toute exception lors de l'accès et fournissons un stream
|
||||
// neutre (pas d'utilisateur) afin de garder l'UI stable.
|
||||
late final Stream<User?> safeAuthStream;
|
||||
try {
|
||||
safeAuthStream = FirebaseAuth.instance
|
||||
.authStateChanges()
|
||||
.handleError((error, stack) {
|
||||
// Log pour debug ; ne rethrow pas
|
||||
debugPrint('[AppStartGate] authStateChanges error: $error');
|
||||
});
|
||||
} catch (e, st) {
|
||||
// Sur certaines configurations web l'accès à FirebaseAuth.instance
|
||||
// peut échouer au niveau JS interop. On log puis on fournit un stream
|
||||
// qui émet une seule valeur nulle pour indiquer "pas d'utilisateur".
|
||||
debugPrint('[AppStartGate] FirebaseAuth.instance access error: $e\n$st');
|
||||
safeAuthStream = Stream<User?>.value(null);
|
||||
}
|
||||
|
||||
return StreamBuilder<User?>(
|
||||
stream: safeAuthStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const _StartupSplashScreen();
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
// En théorie handleError évite d'arriver ici, mais on garde
|
||||
// une protection supplémentaire.
|
||||
debugPrint('[AppStartGate] snapshot error: ${snapshot.error}');
|
||||
return const _StartupSplashScreen(message: 'Erreur de connexion');
|
||||
}
|
||||
|
||||
if (snapshot.data != null) {
|
||||
return const _AuthenticatedBootstrap();
|
||||
}
|
||||
|
||||
return const LoginPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AuthenticatedBootstrap extends StatefulWidget {
|
||||
const _AuthenticatedBootstrap();
|
||||
|
||||
@override
|
||||
State<_AuthenticatedBootstrap> createState() =>
|
||||
_AuthenticatedBootstrapState();
|
||||
}
|
||||
|
||||
class _AuthenticatedBootstrapState extends State<_AuthenticatedBootstrap> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_redirectAfterAuth();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _redirectAfterAuth() async {
|
||||
final fragment = Uri.base.fragment;
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (fragment.isNotEmpty && fragment != '/' && fragment != '/calendar') {
|
||||
Navigator.of(context).pushReplacementNamed(fragment);
|
||||
} else {
|
||||
Navigator.of(context).pushReplacementNamed('/calendar');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const _StartupSplashScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class _StartupSplashScreen extends StatelessWidget {
|
||||
final String message;
|
||||
|
||||
const _StartupSplashScreen({this.message = 'Démarrage...'});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/logos/RectangleLogoBlack.png',
|
||||
width: 160,
|
||||
height: 160,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(
|
||||
Icons.event_available,
|
||||
size: 72,
|
||||
color: AppColors.rouge,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
Text(message),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,48 @@
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/views/login_page.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/views/login_page.dart';
|
||||
|
||||
class AuthGuard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final String? requiredPermission;
|
||||
final bool allowWhileLoading;
|
||||
|
||||
const AuthGuard({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.requiredPermission,
|
||||
this.allowWhileLoading = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localAuthProvider = Provider.of<LocalUserProvider>(context);
|
||||
final firebaseUser = FirebaseAuth.instance.currentUser;
|
||||
|
||||
// Log pour débug
|
||||
print('[AuthGuard] Vérification accès - User: ${localAuthProvider.currentUser?.uid}, Permission requise: $requiredPermission');
|
||||
|
||||
// Si Firebase n'a pas encore restauré la session ou si le profil charge,
|
||||
// afficher un écran neutre plutôt que la page de connexion.
|
||||
if (firebaseUser != null &&
|
||||
(localAuthProvider.currentUser == null ||
|
||||
localAuthProvider.isLoadingUserData)) {
|
||||
if (allowWhileLoading) {
|
||||
return child;
|
||||
}
|
||||
|
||||
return const Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Si l'utilisateur n'est pas connecté
|
||||
if (localAuthProvider.currentUser == null) {
|
||||
if (firebaseUser == null || localAuthProvider.currentUser == null) {
|
||||
print('[AuthGuard] Utilisateur non connecté, redirection vers LoginPage');
|
||||
return const LoginPage();
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
|
||||
class FirebaseStorageManager {
|
||||
final FirebaseStorage _storage = FirebaseStorage.instance;
|
||||
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
||||
FirebaseStorage get _storage => FirebaseStorage.instance;
|
||||
final DataService _dataService = DataService(apiService);
|
||||
|
||||
/// Upload ou remplace la photo de profil d'un utilisateur dans Firebase Storage.
|
||||
/// Pour le Web, on fixe l'extension .jpg.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import '../providers/local_user_provider.dart';
|
||||
@@ -33,22 +35,17 @@ class LoginViewModel extends ChangeNotifier {
|
||||
passwordController.text,
|
||||
);
|
||||
|
||||
// --- Étape 2: Charger les données utilisateur depuis Firestore ---
|
||||
await localAuthProvider.loadUserData();
|
||||
// --- Étape 2: Charger les données utilisateur en arrière-plan ---
|
||||
unawaited(
|
||||
localAuthProvider.loadUserData().catchError((e) {
|
||||
debugPrint('Erreur chargement profil après connexion : $e');
|
||||
}),
|
||||
);
|
||||
|
||||
// Vérifier si le contexte est toujours valide
|
||||
if (context.mounted) {
|
||||
// Vérifier si l'utilisateur a bien été chargé dans le provider
|
||||
if (localAuthProvider.currentUser != null) {
|
||||
// Utiliser pushReplacementNamed pour une transition propre
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pushReplacementNamed('/calendar');
|
||||
} else {
|
||||
errorMessage =
|
||||
'Erreur inattendue après connexion: Données utilisateur non chargées.';
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
} on FirebaseAuthException catch (e) {
|
||||
// Gestion spécifique des erreurs d'authentification (email/mot de passe incorrects, etc.)
|
||||
|
||||
@@ -52,6 +52,8 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
bool _isMobileSearchVisible = false;
|
||||
bool _isRefreshing = false;
|
||||
double _detailsPaneFraction = 0.35;
|
||||
String? _lastLoadedUserId;
|
||||
bool _initialLoadScheduled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -115,6 +117,24 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleInitialEventsLoad(String? userId) {
|
||||
if (userId == null || userId == _lastLoadedUserId || _initialLoadScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_initialLoadScheduled = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
if (!mounted) return;
|
||||
if (_lastLoadedUserId == userId) return;
|
||||
await _loadCurrentMonthEvents();
|
||||
_lastLoadedUserId = userId;
|
||||
} finally {
|
||||
_initialLoadScheduled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Sélectionne automatiquement l'événement le plus proche de maintenant
|
||||
void _selectDefaultEvent() {
|
||||
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||
@@ -813,6 +833,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final eventProvider = Provider.of<EventProvider>(context);
|
||||
final localUserProvider = Provider.of<LocalUserProvider>(context);
|
||||
_scheduleInitialEventsLoad(localUserProvider.uid);
|
||||
final canCreateEvents = localUserProvider.hasPermission('create_events');
|
||||
final canViewAllUserEvents =
|
||||
localUserProvider.hasPermission('view_all_user_events');
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
class StartupSplashScreen extends StatelessWidget {
|
||||
final String message;
|
||||
const StartupSplashScreen({super.key, this.message = 'Démarrage...'});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/logos/RectangleLogoBlack.png',
|
||||
width: 160,
|
||||
height: 160,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(
|
||||
Icons.event_available,
|
||||
size: 72,
|
||||
color: AppColors.rouge,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.rouge),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
color: AppColors.noir,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ dependencies:
|
||||
cloud_functions: ^6.0.4
|
||||
google_sign_in: ^7.2.0
|
||||
firebase_storage: ^13.0.3
|
||||
shared_preferences: ^2.0.15
|
||||
|
||||
# State Management
|
||||
provider: ^6.1.2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.23",
|
||||
"updateUrl": "https://app.em2events.fr",
|
||||
"forceUpdate": true,
|
||||
"releaseNotes": "Ajout de la recherche d'événements et gestion avancée de la suppression d'équipement",
|
||||
"timestamp": "2026-04-22T10:22:26.036Z"
|
||||
"releaseNotes": "Optimisation du lancement de l'application et amélioration de la gestion du cache.",
|
||||
"timestamp": "2026-05-05T09:52:18.860Z"
|
||||
}
|
||||
Reference in New Issue
Block a user