Enhance model and service documentation with detailed comments and descriptions

- Updated Group, Trip, User, and other model classes to include comprehensive documentation for better understanding and maintainability.
- Improved error handling and logging in services, including AuthService, ErrorService, and StorageService.
- Added validation and business logic explanations in ExpenseService and TripService.
- Refactored method comments to follow a consistent format across the codebase.
- Translated error messages and comments from French to English for consistency.
This commit is contained in:
Dayron
2025-10-30 15:56:17 +01:00
parent 1eeea6997e
commit 2faf37f145
46 changed files with 2656 additions and 220 deletions

View File

@@ -2,14 +2,32 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
import 'package:travel_mate/services/error_service.dart';
/// Service for handling Firebase authentication operations.
///
/// This service provides methods for user authentication including email/password
/// sign-in, Google sign-in, Apple sign-in, password reset, and account management.
/// It acts as a wrapper around Firebase Auth functionality.
class AuthService {
/// Error service for logging authentication errors.
final _errorService = ErrorService();
/// Firebase Auth instance for authentication operations.
final FirebaseAuth firebaseAuth = FirebaseAuth.instance;
/// Gets the currently authenticated user.
///
/// Returns null if no user is currently signed in.
User? get currentUser => firebaseAuth.currentUser;
/// Stream that emits authentication state changes.
///
/// Emits the current user when authenticated, null when not authenticated.
Stream<User?> get authStateChanges => firebaseAuth.authStateChanges();
/// Signs in a user with email and password.
///
/// Returns a [UserCredential] containing the authenticated user's information.
/// Throws [FirebaseAuthException] if authentication fails.
Future<UserCredential> signInWithEmailAndPassword({
required String email,
required String password
@@ -18,6 +36,10 @@ class AuthService {
email: email, password: password);
}
/// Creates a new user account with email and password.
///
/// Returns a [UserCredential] containing the new user's information.
/// Throws [FirebaseAuthException] if account creation fails.
Future<UserCredential> signUpWithEmailAndPassword({
required String email,
required String password
@@ -26,52 +48,88 @@ class AuthService {
email: email, password: password);
}
/// Signs out the current user.
///
/// Clears the authentication state and signs out the user from Firebase.
Future<void> signOut() async {
await firebaseAuth.signOut();
}
/// Sends a password reset email to the specified email address.
///
/// The user will receive an email with instructions to reset their password.
/// Throws [FirebaseAuthException] if the email is invalid or other errors occur.
Future<void> resetPassword(String email) async {
await firebaseAuth.sendPasswordResetEmail(email: email);
}
/// Updates the display name of the current user.
///
/// Requires a user to be currently authenticated.
/// Throws if no user is signed in.
Future<void> updateDisplayName({
required String displayName,
}) async {
await currentUser!.updateDisplayName(displayName);
}
/// Deletes the current user's account permanently.
///
/// Requires re-authentication with the user's current password for security.
/// This operation cannot be undone.
///
/// [password] - The user's current password for re-authentication
/// [email] - The user's email address for re-authentication
Future<void> deleteAccount({
required String password,
required String email,
}) async {
// Re-authenticate the user
// Re-authenticate the user for security
AuthCredential credential =
EmailAuthProvider.credential(email: email, password: password);
await currentUser!.reauthenticateWithCredential(credential);
// Delete the user
// Delete the user account permanently
await currentUser!.delete();
await firebaseAuth.signOut();
}
/// Resets the user's password after re-authentication.
///
/// This method allows users to change their password by providing their
/// current password for security verification.
///
/// [currentPassword] - The user's current password for verification
/// [newPassword] - The new password to set
/// [email] - The user's email address for re-authentication
Future<void> resetPasswordFromCurrentPassword({
required String currentPassword,
required String newPassword,
required String email,
}) async {
// Re-authenticate the user
// Re-authenticate the user for security
AuthCredential credential =
EmailAuthProvider.credential(email: email, password: currentPassword);
await currentUser!.reauthenticateWithCredential(credential);
// Update the password
// Update to the new password
await currentUser!.updatePassword(newPassword);
}
/// Ensures Google Sign-In is properly initialized.
///
/// This method must be called before attempting Google authentication.
Future<void> ensureInitialized(){
return GoogleSignInPlatform.instance.init(const InitParameters());
}
/// Signs in a user using Google authentication.
///
/// Handles the complete Google Sign-In flow including platform initialization
/// and credential exchange with Firebase.
///
/// Returns a [UserCredential] containing the authenticated user's information.
/// Throws various exceptions if authentication fails.
Future<UserCredential> signInWithGoogle() async {
try {
await ensureInitialized();
@@ -86,24 +144,28 @@ class AuthService {
final OAuthCredential credential = GoogleAuthProvider.credential(idToken: idToken);
UserCredential userCredential = await firebaseAuth.signInWithCredential(credential);
// Retourner le UserCredential au lieu de void
// Return the UserCredential instead of void
return userCredential;
}
} on GoogleSignInException catch (e) {
_errorService.logError('Erreur Google Sign-In: $e', StackTrace.current);
_errorService.logError('Google Sign-In error: $e', StackTrace.current);
rethrow;
} on FirebaseAuthException catch (e) {
_errorService.logError('Erreur Firebase lors de l\'initialisation de Google Sign-In: $e', StackTrace.current);
_errorService.logError('Firebase error during Google Sign-In initialization: $e', StackTrace.current);
rethrow;
} catch (e) {
_errorService.logError('Erreur inconnue lors de l\'initialisation de Google Sign-In: $e', StackTrace.current);
_errorService.logError('Unknown error during Google Sign-In initialization: $e', StackTrace.current);
rethrow;
}
}
/// Signs in a user using Apple authentication.
///
/// TODO: Implement Apple Sign-In functionality
/// This method is currently a placeholder for future Apple authentication support.
Future signInWithApple() async {
// TODO: Implémenter la connexion avec Apple
// TODO: Implement Apple sign-in
}
}

View File

@@ -1,18 +1,40 @@
import 'package:flutter/material.dart';
import '../components/error/error_content.dart';
/// Service for handling application errors and user notifications.
///
/// This singleton service provides centralized error handling capabilities
/// including displaying error dialogs, snackbars, and logging errors for
/// debugging purposes. It uses a global navigator key to show notifications
/// from anywhere in the application.
class ErrorService {
static final ErrorService _instance = ErrorService._internal();
/// Factory constructor that returns the singleton instance.
factory ErrorService() => _instance;
/// Private constructor for singleton pattern.
ErrorService._internal();
// GlobalKey pour accéder au context depuis n'importe où
/// Global navigator key for accessing context from anywhere in the app.
///
/// This key should be assigned to the MaterialApp's navigatorKey property
/// to enable error notifications from any part of the application.
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// Afficher une erreur en dialog
/// Displays an error message in a dialog.
///
/// Shows a modal dialog with the error message and optional retry functionality.
/// The dialog appearance can be customized with different icons and colors.
///
/// [message] - The error message to display
/// [title] - The dialog title (defaults to 'Error')
/// [onRetry] - Optional callback for retry functionality
/// [icon] - Icon to display in the dialog
/// [iconColor] - Color of the icon
void showError({
required String message,
String title = 'Erreur',
String title = 'Error',
VoidCallback? onRetry,
IconData icon = Icons.error_outline,
Color? iconColor,
@@ -30,7 +52,14 @@ class ErrorService {
}
}
// Afficher une erreur en snackbar
/// Displays an error or success message in a snackbar.
///
/// Shows a floating snackbar at the bottom of the screen with the message.
/// The appearance changes based on whether it's an error or success message.
///
/// [message] - The message to display
/// [onRetry] - Optional callback for retry functionality
/// [isError] - Whether this is an error (true) or success (false) message
void showSnackbar({
required String message,
VoidCallback? onRetry,
@@ -45,7 +74,7 @@ class ErrorService {
duration: const Duration(seconds: 4),
action: onRetry != null
? SnackBarAction(
label: 'Réessayer',
label: 'Retry',
textColor: Colors.white,
onPressed: onRetry,
)
@@ -59,10 +88,17 @@ class ErrorService {
}
}
// Logger dans la console (développement)
/// Logs error messages to the console during development.
///
/// Formats and displays error information including source, error message,
/// and optional stack trace in a visually distinct format.
///
/// [source] - The source or location where the error occurred
/// [error] - The error object or message
/// [stackTrace] - Optional stack trace for debugging
void logError(String source, dynamic error, [StackTrace? stackTrace]) {
print('═══════════════════════════════════');
print('❌ ERREUR dans $source');
print('❌ ERROR in $source');
print('Message: $error');
if (stackTrace != null) {
print('StackTrace: $stackTrace');
@@ -70,12 +106,18 @@ class ErrorService {
print('═══════════════════════════════════');
}
// Logger une info (développement)
/// Logs informational messages to the console during development.
///
/// [source] - The source or location of the information
/// [message] - The informational message
void logInfo(String source, String message) {
print(' [$source] $message');
}
// Logger un succès
/// Logs success messages to the console during development.
///
/// [source] - The source or location of the success
/// [message] - The success message
void logSuccess(String source, String message) {
print('✅ [$source] $message');
}

View File

@@ -2,13 +2,27 @@ import 'dart:io';
import '../models/expense.dart';
import '../repositories/expense_repository.dart';
import 'error_service.dart';
import 'storage_service.dart'; // Pour upload des reçus
import 'storage_service.dart';
/// Service for managing expense operations with business logic validation.
///
/// This service provides high-level expense management functionality including
/// validation, receipt image uploading, and coordination with the expense repository.
/// It acts as a business logic layer between the UI and data persistence.
class ExpenseService {
/// Repository for expense data operations.
final ExpenseRepository _expenseRepository;
/// Service for error handling and logging.
final ErrorService _errorService;
/// Service for handling file uploads (receipts).
final StorageService _storageService;
/// Creates a new [ExpenseService] with required dependencies.
///
/// [expenseRepository] is required for data operations.
/// [errorService] and [storageService] have default implementations if not provided.
ExpenseService({
required ExpenseRepository expenseRepository,
ErrorService? errorService,
@@ -17,13 +31,22 @@ class ExpenseService {
_errorService = errorService ?? ErrorService(),
_storageService = storageService ?? StorageService();
// Création avec validation et upload d'image
/// Creates an expense with validation and optional receipt image upload.
///
/// Validates the expense data, uploads receipt image if provided, and
/// creates the expense record in the database.
///
/// [expense] - The expense data to create
/// [receiptImage] - Optional receipt image file to upload
///
/// Returns the ID of the created expense.
/// Throws exceptions if validation fails or creation errors occur.
Future<String> createExpenseWithValidation(Expense expense, File? receiptImage) async {
try {
// Validation métier
// Business logic validation
_validateExpenseData(expense);
// Upload du reçu si présent
// Upload receipt image if provided
String? receiptUrl;
if (receiptImage != null) {
receiptUrl = await _storageService.uploadReceiptImage(

View File

@@ -1,3 +1,29 @@
/// A service that handles file storage operations using Firebase Storage.
///
/// This service provides functionality for:
/// - Receipt image upload and compression
/// - Profile image management
/// - File validation and optimization
/// - Automatic image compression to reduce storage costs
/// - Metadata management for uploaded files
///
/// The service automatically compresses images to JPEG format with 85% quality
/// to balance file size and image quality. It also generates unique filenames
/// and handles error logging through the ErrorService.
///
/// Example usage:
/// ```dart
/// final storageService = StorageService();
///
/// // Upload a receipt image
/// final receiptUrl = await storageService.uploadReceiptImage(groupId, imageFile);
///
/// // Upload a profile image
/// final profileUrl = await storageService.uploadProfileImage(userId, imageFile);
///
/// // Delete a file
/// await storageService.deleteFile(fileUrl);
/// ```
import 'dart:io';
import 'dart:typed_data';
import 'package:firebase_storage/firebase_storage.dart';
@@ -5,32 +31,55 @@ import 'package:path/path.dart' as path;
import 'package:image/image.dart' as img;
import 'error_service.dart';
/// Service for managing file storage operations with Firebase Storage.
class StorageService {
/// Firebase Storage instance for file operations
final FirebaseStorage _storage;
/// Service for error handling and logging
final ErrorService _errorService;
/// Constructor for StorageService.
///
/// Args:
/// [storage]: Optional Firebase Storage instance (auto-created if null)
/// [errorService]: Optional error service instance (auto-created if null)
StorageService({
FirebaseStorage? storage,
ErrorService? errorService,
}) : _storage = storage ?? FirebaseStorage.instance,
_errorService = errorService ?? ErrorService();
/// Upload d'une image de reçu pour une dépense
/// Uploads a receipt image for an expense with automatic compression.
///
/// Validates the image file, compresses it to JPEG format with 85% quality,
/// generates a unique filename, and uploads it with appropriate metadata.
/// Monitors upload progress and logs it for debugging purposes.
///
/// Args:
/// [groupId]: ID of the group this receipt belongs to
/// [imageFile]: The image file to upload
///
/// Returns:
/// A Future<String> containing the download URL of the uploaded image
///
/// Throws:
/// Exception if file validation fails or upload encounters an error
Future<String> uploadReceiptImage(String groupId, File imageFile) async {
try {
// Validation du fichier
// File validation
_validateImageFile(imageFile);
// Compression de l'image
// Image compression
final compressedImage = await _compressImage(imageFile);
// Génération du nom de fichier unique
// Generate unique filename
final fileName = _generateReceiptFileName(groupId);
// Référence vers le storage
// Storage reference
final storageRef = _storage.ref().child('receipts/$groupId/$fileName');
// Métadonnées pour optimiser le cache et la compression
// Metadata for cache optimization and compression info
final metadata = SettableMetadata(
contentType: 'image/jpeg',
customMetadata: {
@@ -40,58 +89,77 @@ class StorageService {
},
);
// Upload du fichier
// File upload
final uploadTask = storageRef.putData(compressedImage, metadata);
// Monitoring du progrès (optionnel)
// Progress monitoring (optional)
uploadTask.snapshotEvents.listen((TaskSnapshot snapshot) {
final progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
_errorService.logInfo('StorageService', 'Upload progress: ${progress.toStringAsFixed(1)}%');
});
// Attendre la completion
// Wait for completion
final snapshot = await uploadTask;
// Récupérer l'URL de téléchargement
// Get download URL
final downloadUrl = await snapshot.ref.getDownloadURL();
_errorService.logSuccess('StorageService', 'Image uploadée avec succès: $fileName');
_errorService.logSuccess('StorageService', 'Image uploaded successfully: $fileName');
return downloadUrl;
} catch (e) {
_errorService.logError('StorageService', 'Erreur upload image: $e');
_errorService.logError('StorageService', 'Error uploading image: $e');
rethrow;
}
}
/// Supprimer une image de reçu
/// Deletes a receipt image from storage.
///
/// Extracts the storage reference from the provided URL and deletes the file.
/// Does not throw errors to avoid blocking expense deletion operations.
///
/// Args:
/// [imageUrl]: The download URL of the image to delete
Future<void> deleteReceiptImage(String imageUrl) async {
try {
if (imageUrl.isEmpty) return;
// Extraire la référence depuis l'URL
// Extract reference from URL
final ref = _storage.refFromURL(imageUrl);
await ref.delete();
_errorService.logSuccess('StorageService', 'Image supprimée avec succès');
_errorService.logSuccess('StorageService', 'Image deleted successfully');
} catch (e) {
_errorService.logError('StorageService', 'Erreur suppression image: $e');
// Ne pas rethrow pour éviter de bloquer la suppression de la dépense
_errorService.logError('StorageService', 'Error deleting image: $e');
// Don't rethrow to avoid blocking expense deletion
}
}
/// Compresser une image pour optimiser le stockage
/// Compresses an image to optimize storage space and upload speed.
///
/// Reads the image file, decodes it, resizes it if too large (max 1024x1024),
/// and encodes it as JPEG with 85% quality for optimal balance between
/// file size and image quality.
///
/// Args:
/// [imageFile]: The image file to compress
///
/// Returns:
/// A Future<Uint8List> containing the compressed image bytes
///
/// Throws:
/// Exception if the image cannot be decoded or processed
Future<Uint8List> _compressImage(File imageFile) async {
try {
// Lire l'image
// Read image
final bytes = await imageFile.readAsBytes();
img.Image? image = img.decodeImage(bytes);
if (image == null) {
throw Exception('Impossible de décoder l\'image');
throw Exception('Unable to decode image');
}
// Redimensionner si l'image est trop grande
// Resize if image is too large
const maxWidth = 1024;
const maxHeight = 1024;
@@ -104,50 +172,78 @@ class StorageService {
);
}
// Encoder en JPEG avec compression
// Encode as JPEG with compression
final compressedBytes = img.encodeJpg(image, quality: 85);
_errorService.logInfo('StorageService',
'Image compressée: ${bytes.length}${compressedBytes.length} bytes');
'Image compressed: ${bytes.length}${compressedBytes.length} bytes');
return Uint8List.fromList(compressedBytes);
} catch (e) {
_errorService.logError('StorageService', 'Erreur compression image: $e');
// Fallback: retourner l'image originale si la compression échoue
_errorService.logError('StorageService', 'Error compressing image: $e');
// Fallback: return original image if compression fails
return await imageFile.readAsBytes();
}
}
/// Valider le fichier image
/// Validates an image file before upload.
///
/// Checks file existence, size constraints (max 10MB), and file extension
/// to ensure only valid image files are processed for upload.
///
/// Args:
/// [imageFile]: The image file to validate
///
/// Throws:
/// Exception if validation fails (file doesn't exist, too large, or invalid extension)
void _validateImageFile(File imageFile) {
// Vérifier que le fichier existe
// Check if file exists
if (!imageFile.existsSync()) {
throw Exception('Le fichier image n\'existe pas');
throw Exception('Image file does not exist');
}
// Vérifier la taille du fichier (max 10MB)
// Check file size (max 10MB)
const maxSizeBytes = 10 * 1024 * 1024; // 10MB
final fileSize = imageFile.lengthSync();
if (fileSize > maxSizeBytes) {
throw Exception('La taille du fichier dépasse 10MB');
throw Exception('File size exceeds 10MB limit');
}
// Vérifier l'extension
// Check file extension
final extension = path.extension(imageFile.path).toLowerCase();
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.webp'];
if (!allowedExtensions.contains(extension)) {
throw Exception('Format d\'image non supporté. Utilisez JPG, PNG ou WebP');
throw Exception('Unsupported image format. Use JPG, PNG or WebP');
}
}
/// Générer un nom de fichier unique pour un reçu
/// Generates a unique filename for a receipt image.
///
/// Creates a filename using timestamp, microseconds, and group ID to ensure
/// uniqueness and prevent naming conflicts when multiple receipts are uploaded.
///
/// Args:
/// [groupId]: ID of the group this receipt belongs to
///
/// Returns:
/// A unique filename string for the receipt image
String _generateReceiptFileName(String groupId) {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final random = DateTime.now().microsecond;
return 'receipt_${groupId}_${timestamp}_$random.jpg';
}
/// Upload multiple d'images (pour futures fonctionnalités)
/// Uploads multiple images simultaneously (for future features).
///
/// Processes multiple image files in parallel for batch upload scenarios.
/// Each image is validated, compressed, and uploaded with unique filenames.
///
/// Args:
/// [groupId]: ID of the group these images belong to
/// [imageFiles]: List of image files to upload
///
/// Returns:
/// A Future<List<String>> containing download URLs of uploaded images
Future<List<String>> uploadMultipleImages(
String groupId,
List<File> imageFiles,

View File

@@ -1,10 +1,42 @@
/// A service that handles trip-related business logic and data operations.
///
/// This service provides functionality for:
/// - Trip creation, updating, and deletion
/// - Trip validation and business rule enforcement
/// - Location-based trip suggestions
/// - Trip statistics and analytics
/// - Integration with Firebase Firestore for data persistence
///
/// The service ensures data integrity by validating trip information
/// before database operations and provides comprehensive error handling
/// and logging through the ErrorService.
///
/// Example usage:
/// ```dart
/// final tripService = TripService();
///
/// // Create a new trip
/// final tripId = await tripService.createTrip(newTrip);
///
/// // Get trip suggestions for a location
/// final suggestions = await tripService.getTripSuggestions('Paris');
///
/// // Calculate trip statistics
/// final stats = await tripService.getTripStatistics(tripId);
/// ```
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:travel_mate/services/error_service.dart';
import '../models/trip.dart';
/// Service for managing trip-related operations and business logic.
class TripService {
/// Service for error handling and logging
final _errorService = ErrorService();
/// Firestore instance for database operations
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
/// Collection name for trips in Firestore
static const String _tripsCollection = 'trips';
// Charger tous les voyages

View File

@@ -3,29 +3,53 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'package:travel_mate/services/error_service.dart';
import '../blocs/user/user_state.dart';
/// Service for managing user operations with Firestore and Firebase Auth.
///
/// This service provides functionality for user management including creating,
/// retrieving, updating, and deleting user data in Firestore. It also handles
/// user authentication state and provides methods for user profile management.
class UserService {
/// Error service for logging user operation errors.
final _errorService = ErrorService();
/// Firestore instance for database operations.
final FirebaseFirestore _firestore;
/// Firebase Auth instance for user authentication.
final FirebaseAuth _auth;
/// Collection name for users in Firestore.
static const String _usersCollection = 'users';
/// Creates a new [UserService] with optional Firestore and Auth instances.
///
/// If [firestore] or [auth] are not provided, the default instances will be used.
UserService({
FirebaseFirestore? firestore,
FirebaseAuth? auth,
}) : _firestore = firestore ?? FirebaseFirestore.instance,
_auth = auth ?? FirebaseAuth.instance;
// Obtenir l'utilisateur connecté actuel
/// Gets the currently authenticated Firebase user.
///
/// Returns the [User] object if authenticated, null otherwise.
User? getCurrentFirebaseUser() {
return _auth.currentUser;
}
// Obtenir l'ID de l'utilisateur connecté
/// Gets the ID of the currently authenticated user.
///
/// Returns the user ID string if authenticated, null otherwise.
String? getCurrentUserId() {
return _auth.currentUser?.uid;
}
// Créer un nouvel utilisateur dans Firestore
/// Creates a new user document in Firestore.
///
/// Takes a [UserModel] object and stores it in the users collection.
/// Returns true if successful, false if an error occurs.
///
/// [user] - The user model to create in Firestore
Future<bool> createUser(UserModel user) async {
try {
await _firestore
@@ -34,12 +58,16 @@ class UserService {
.set(user.toJson());
return true;
} catch (e) {
_errorService.logError('Erreur lors de la création de l\'utilisateur: $e', StackTrace.current);
_errorService.logError('Error creating user: $e', StackTrace.current);
return false;
}
}
// Obtenir un utilisateur par son ID
/// Retrieves a user by their ID from Firestore.
///
/// Returns a [UserModel] if the user exists, null otherwise.
///
/// [userId] - The ID of the user to retrieve
Future<UserModel?> getUserById(String userId) async {
try {
final doc = await _firestore