refactor: Centralize error and notification handling using a dedicated _errorService across various components.

This commit is contained in:
Van Leemput Dayron
2025-12-03 14:50:03 +01:00
parent 6757cb013a
commit f3ae91ccf9
10 changed files with 91 additions and 249 deletions

View File

@@ -63,6 +63,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:travel_mate/models/expense_split.dart';
import '../../services/error_service.dart';
import '../../blocs/expense/expense_bloc.dart';
import '../../blocs/expense/expense_event.dart';
import '../../blocs/expense/expense_state.dart';
@@ -191,11 +192,8 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
if (fileSize > 5 * 1024 * 1024) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('L\'image ne doit pas dépasser 5 Mo'),
backgroundColor: Colors.red,
),
ErrorService().showError(
message: 'L\'image ne doit pas dépasser 5 Mo',
);
}
return;
@@ -247,11 +245,8 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
}).toList();
if (selectedSplits.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez sélectionner au moins un participant'),
backgroundColor: Colors.red,
),
ErrorService().showError(
message: 'Veuillez sélectionner au moins un participant',
);
return;
}
@@ -299,22 +294,16 @@ class _AddExpenseDialogState extends State<AddExpenseDialog> {
if (mounted) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
widget.expenseToEdit == null
? 'Dépense ajoutée'
: 'Dépense modifiée',
),
backgroundColor: Colors.green,
),
ErrorService().showSnackbar(
message: widget.expenseToEdit == null
? 'Dépense ajoutée'
: 'Dépense modifiée',
isError: false,
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
);
ErrorService().showError(message: 'Erreur: $e');
}
} finally {
if (mounted) {

View File

@@ -15,6 +15,7 @@ import 'balances_tab.dart';
import 'expenses_tab.dart';
import '../../models/user_balance.dart';
import '../../models/expense.dart';
import '../../services/error_service.dart';
class GroupExpensesPage extends StatefulWidget {
final Account account;
@@ -93,20 +94,13 @@ class _GroupExpensesPageState extends State<GroupExpensesPage>
BlocListener<ExpenseBloc, ExpenseState>(
listener: (context, state) {
if (state is ExpenseOperationSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.green,
),
ErrorService().showSnackbar(
message: state.message,
isError: false,
);
_loadData(); // Recharger les données après une opération
} else if (state is ExpenseError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
ErrorService().showError(message: state.message);
} else if (state is ExpensesLoaded) {
// Rafraîchir les balances quand les dépenses changent (ex: via stream)
context.read<BalanceBloc>().add(
@@ -393,12 +387,7 @@ class _GroupExpensesPageState extends State<GroupExpensesPage>
AddExpenseDialog(group: widget.group, currentUser: userState.user),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erreur: utilisateur non connecté'),
backgroundColor: Colors.red,
),
);
ErrorService().showError(message: 'Erreur: utilisateur non connecté');
}
}

View File

@@ -1,31 +1,31 @@
import 'package:flutter/material.dart';
/// A reusable error display component.
///
///
/// This widget provides a consistent way to display error messages throughout
/// the application. It supports customizable titles, messages, icons, and
/// action buttons for retry and close operations.
class ErrorContent extends StatelessWidget {
/// The error title to display
final String title;
/// The error message to display
final String message;
/// Optional callback for retry action
final VoidCallback? onRetry;
/// Optional callback for close action
final VoidCallback? onClose;
/// Icon to display with the error
final IconData icon;
/// Color of the error icon
final Color? iconColor;
/// Creates a new [ErrorContent] widget.
///
///
/// [message] is required, other parameters are optional with sensible defaults.
const ErrorContent({
super.key,
@@ -79,11 +79,7 @@ class ErrorContent extends StatelessWidget {
color: defaultIconColor?.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 48,
color: defaultIconColor,
),
child: Icon(icon, size: 48, color: defaultIconColor),
),
const SizedBox(height: 24),
@@ -167,9 +163,7 @@ void showErrorDialog(
barrierDismissible: barrierDismissible,
builder: (BuildContext dialogContext) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: ErrorContent(
title: title,
message: message,
@@ -187,70 +181,3 @@ void showErrorDialog(
},
);
}
// Fonction helper pour afficher l'erreur en bottom sheet
void showErrorBottomSheet(
BuildContext context, {
String title = 'Une erreur est survenue',
required String message,
VoidCallback? onRetry,
IconData icon = Icons.error_outline,
Color? iconColor,
bool isDismissible = true,
}) {
showModalBottomSheet(
context: context,
isDismissible: isDismissible,
enableDrag: isDismissible,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (BuildContext sheetContext) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: ErrorContent(
title: title,
message: message,
icon: icon,
iconColor: iconColor,
onRetry: onRetry != null
? () {
Navigator.of(sheetContext).pop();
onRetry();
}
: null,
onClose: () => Navigator.of(sheetContext).pop(),
),
),
);
},
);
}
// Fonction helper pour afficher en SnackBar (pour erreurs mineures)
void showErrorSnackBar(
BuildContext context, {
required String message,
VoidCallback? onRetry,
Duration duration = const Duration(seconds: 4),
}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red[400],
duration: duration,
action: onRetry != null
? SnackBarAction(
label: 'Réessayer',
textColor: Colors.white,
onPressed: onRetry,
)
: null,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}

View File

@@ -10,6 +10,7 @@ import '../../models/group.dart';
import '../../models/group_member.dart';
import '../../models/message.dart';
import '../../repositories/group_repository.dart';
import '../../services/error_service.dart';
/// Chat group content widget for group messaging functionality.
///
@@ -220,12 +221,7 @@ class _ChatGroupContentState extends State<ChatGroupContent> {
child: BlocConsumer<MessageBloc, MessageState>(
listener: (context, state) {
if (state is MessageError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
ErrorService().showError(message: state.message);
}
},
builder: (context, state) {
@@ -871,20 +867,15 @@ class _ChatGroupContentState extends State<ChatGroupContent> {
// Le stream listener va automatiquement mettre à jour les membres
// Pas besoin de fermer le dialog ou de faire un refresh manuel
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Pseudo modifié en "$newPseudo"'),
backgroundColor: Colors.green,
),
ErrorService().showSnackbar(
message: 'Pseudo modifié en "$newPseudo"',
isError: false,
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la modification du pseudo: $e'),
backgroundColor: Colors.red,
),
ErrorService().showError(
message: 'Erreur lors de la modification du pseudo: $e',
);
}
}

View File

@@ -876,17 +876,16 @@ class _CreateTripContentState extends State<CreateTripContent> {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(email)) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Email invalide')));
_errorService.showError(message: 'Email invalide');
}
return;
}
if (_participants.contains(email)) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ce participant est déjà ajouté')),
_errorService.showSnackbar(
message: 'Ce participant est déjà ajouté',
isError: true,
);
}
return;
@@ -962,11 +961,9 @@ class _CreateTripContentState extends State<CreateTripContent> {
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Groupe et compte mis à jour avec succès !'),
backgroundColor: Colors.green,
),
_errorService.showSnackbar(
message: 'Groupe et compte mis à jour avec succès !',
isError: false,
);
setState(() {
_isLoading = false;
@@ -1048,11 +1045,9 @@ class _CreateTripContentState extends State<CreateTripContent> {
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Voyage, groupe et compte créés avec succès !'),
backgroundColor: Colors.green,
),
_errorService.showSnackbar(
message: 'Voyage, groupe et compte créés avec succès !',
isError: false,
);
setState(() {
_isLoading = false;
@@ -1066,9 +1061,7 @@ class _CreateTripContentState extends State<CreateTripContent> {
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
);
_errorService.showError(message: 'Erreur: $e');
setState(() {
_isLoading = false;
});
@@ -1083,8 +1076,9 @@ class _CreateTripContentState extends State<CreateTripContent> {
if (_startDate == null || _endDate == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Veuillez sélectionner les dates')),
_errorService.showSnackbar(
message: 'Veuillez sélectionner les dates',
isError: true,
);
}
return;
@@ -1129,14 +1123,10 @@ class _CreateTripContentState extends State<CreateTripContent> {
// Continuer sans coordonnées en cas d'erreur
tripWithCoordinates = trip;
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
_errorService.showSnackbar(
message:
'Voyage créé sans géolocalisation (pas d\'impact sur les fonctionnalités)',
),
backgroundColor: Colors.orange,
duration: Duration(seconds: 2),
),
isError: true, // Warning displayed as error for now
);
}
}
@@ -1167,9 +1157,7 @@ class _CreateTripContentState extends State<CreateTripContent> {
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
);
_errorService.showError(message: 'Erreur: $e');
setState(() {
_isLoading = false;
@@ -1200,11 +1188,9 @@ class _CreateTripContentState extends State<CreateTripContent> {
});
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Utilisateur non trouvé: $email'),
backgroundColor: Colors.orange,
),
_errorService.showSnackbar(
message: 'Utilisateur non trouvé: $email',
isError: true,
);
}
}

View File

@@ -11,6 +11,7 @@ import '../../blocs/trip/trip_bloc.dart';
import '../../blocs/trip/trip_state.dart';
import '../../blocs/trip/trip_event.dart';
import '../../models/trip.dart';
import '../../services/error_service.dart';
/// Home content widget for the main application dashboard.
///
@@ -79,26 +80,16 @@ class _HomeContentState extends State<HomeContent>
return BlocConsumer<TripBloc, TripState>(
listener: (context, tripState) {
if (tripState is TripOperationSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tripState.message),
backgroundColor: Colors.green,
),
ErrorService().showSnackbar(
message: tripState.message,
isError: false,
);
} else if (tripState is TripError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tripState.message),
backgroundColor: Colors.red,
),
);
ErrorService().showError(message: tripState.message);
} else if (tripState is TripCreated) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Voyage en cours de création...'),
backgroundColor: Colors.blue,
duration: Duration(seconds: 1),
),
ErrorService().showSnackbar(
message: 'Voyage en cours de création...',
isError: false,
);
}
},

View File

@@ -547,11 +547,9 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
}
void _showComingSoon(String feature) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$feature - Fonctionnalité à venir'),
backgroundColor: Colors.blue,
),
_errorService.showSnackbar(
message: '$feature - Fonctionnalité à venir',
isError: false,
);
}
@@ -868,11 +866,8 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
_addParticipantByEmail(emailController.text);
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez entrer un email valide'),
backgroundColor: Colors.red,
),
_errorService.showError(
message: 'Veuillez entrer un email valide',
);
}
},
@@ -940,11 +935,9 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
TripUpdateRequested(trip: updatedTrip),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${user.prenom} a été ajouté au voyage'),
backgroundColor: Colors.green,
),
_errorService.showSnackbar(
message: '${user.prenom} a été ajouté au voyage',
isError: false,
);
// Rafraîchir la page

View File

@@ -5,6 +5,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:ui' as ui;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../services/error_service.dart';
class MapContent extends StatefulWidget {
final String? initialSearchQuery;
@@ -416,13 +417,7 @@ class _MapContentState extends State<MapContent> {
void _showError(String message) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
ErrorService().showError(message: message);
}
}

View File

@@ -502,11 +502,9 @@ class ProfileContent extends StatelessWidget {
);
Navigator.of(dialogContext).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Profil mis à jour !'),
backgroundColor: Colors.green,
),
_errorService.showSnackbar(
message: 'Profil mis à jour !',
isError: false,
);
}
},
@@ -668,11 +666,9 @@ class ProfileContent extends StatelessWidget {
if (context.mounted) {
LoggerService.info('DEBUG: Affichage du succès');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Photo de profil mise à jour !'),
backgroundColor: Colors.green,
),
_errorService.showSnackbar(
message: 'Photo de profil mise à jour !',
isError: false,
);
}
} catch (e, stackTrace) {
@@ -736,22 +732,16 @@ class ProfileContent extends StatelessWidget {
if (currentPasswordController.text.isEmpty ||
newPasswordController.text.isEmpty ||
confirmPasswordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Tous les champs sont requis'),
backgroundColor: Colors.red,
),
_errorService.showError(
message: 'Tous les champs sont requis',
);
return;
}
if (newPasswordController.text !=
confirmPasswordController.text) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Les mots de passe ne correspondent pas'),
backgroundColor: Colors.red,
),
_errorService.showError(
message: 'Les mots de passe ne correspondent pas',
);
return;
}
@@ -765,11 +755,9 @@ class ProfileContent extends StatelessWidget {
if (context.mounted) {
Navigator.of(dialogContext).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Mot de passe changé !'),
backgroundColor: Colors.green,
),
_errorService.showSnackbar(
message: 'Mot de passe changé !',
isError: false,
);
}
} catch (e) {
@@ -823,11 +811,8 @@ class ProfileContent extends StatelessWidget {
TextButton(
onPressed: () async {
if (confirmationController.text != 'CONFIRMER') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Veuillez écrire CONFIRMER pour valider'),
backgroundColor: Colors.red,
),
_errorService.showError(
message: 'Veuillez écrire CONFIRMER pour valider',
);
return;
}
@@ -848,14 +833,10 @@ class ProfileContent extends StatelessWidget {
if (e.code == 'requires-recent-login') {
if (context.mounted) {
Navigator.of(dialogContext).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
_errorService.showSnackbar(
message:
'Par sécurité, veuillez vous reconnecter avant de supprimer votre compte',
),
backgroundColor: Colors.orange,
duration: Duration(seconds: 4),
),
isError: true, // It's a warning/error
);
}
} else {

View File

@@ -233,7 +233,7 @@ class AuthService {
],
// Configuration for Android/Web
webAuthenticationOptions: WebAuthenticationOptions(
clientId: 'be.devdayronvl.TravelMate.service',
clientId: 'be.devdayronvl.travel_mate.service',
redirectUri: Uri.parse(
'https://travelmate-a47f5.firebaseapp.com/__/auth/handler',
),