diff --git a/lib/blocs/auth/auth_bloc.dart b/lib/blocs/auth/auth_bloc.dart new file mode 100644 index 0000000..ef78469 --- /dev/null +++ b/lib/blocs/auth/auth_bloc.dart @@ -0,0 +1,150 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repositories/auth_repository.dart'; +import 'auth_event.dart'; +import 'auth_state.dart'; + +class AuthBloc extends Bloc { + final AuthRepository _authRepository; + + AuthBloc({required AuthRepository authRepository}) + : _authRepository = authRepository, + super(AuthInitial()) { + on(_onAuthCheckRequested); + on(_onSignInRequested); + on(_onSignUpRequested); + on(_onGoogleSignInRequested); + on(_onAppleSignInRequested); + on(_onSignOutRequested); + on(_onPasswordResetRequested); + } + + Future _onAuthCheckRequested( + AuthCheckRequested event, + Emitter emit, + ) async { + emit(AuthLoading()); + + try { + final currentUser = _authRepository.currentUser; + + if (currentUser != null) { + // Récupérer les données utilisateur depuis Firestore + final user = await _authRepository.getUserFromFirestore(currentUser.uid); + + if (user != null) { + emit(AuthAuthenticated(user: user)); + } else { + emit(AuthUnauthenticated()); + } + } else { + emit(AuthUnauthenticated()); + } + } catch (e) { + emit(AuthError(message: e.toString())); + } + } + + Future _onSignInRequested( + AuthSignInRequested event, + Emitter emit, + ) async { + emit(AuthLoading()); + + try { + final user = await _authRepository.signInWithEmailAndPassword( + email: event.email, + password: event.password, + ); + + if (user != null) { + emit(AuthAuthenticated(user: user)); + } else { + emit(const AuthError(message: 'Email ou mot de passe incorrect')); + } + } catch (e) { + emit(AuthError(message: e.toString())); + } + } + + Future _onSignUpRequested( + AuthSignUpRequested event, + Emitter emit, + ) async { + emit(AuthLoading()); + + try { + final user = await _authRepository.signUpWithEmailAndPassword( + email: event.email, + password: event.password, + nom: event.nom, + prenom: event.prenom, + ); + + if (user != null) { + emit(AuthAuthenticated(user: user)); + } else { + emit(const AuthError(message: 'Erreur lors de l\'inscription')); + } + } catch (e) { + emit(AuthError(message: e.toString())); + } + } + + Future _onGoogleSignInRequested( + AuthGoogleSignInRequested event, + Emitter emit, + ) async { + emit(AuthLoading()); + + try { + final user = await _authRepository.signInWithGoogle(); + + if (user != null) { + emit(AuthAuthenticated(user: user)); + } else { + emit(const AuthError(message: 'Connexion Google annulée')); + } + } catch (e) { + emit(AuthError(message: e.toString())); + } + } + + Future _onAppleSignInRequested( + AuthAppleSignInRequested event, + Emitter emit, + ) async { + emit(AuthLoading()); + + try { + final user = await _authRepository.signInWithApple(); + + if (user != null) { + emit(AuthAuthenticated(user: user)); + } else { + emit(const AuthError(message: 'Connexion Apple annulée')); + } + } catch (e) { + emit(AuthError(message: e.toString())); + } + } + + Future _onSignOutRequested( + AuthSignOutRequested event, + Emitter emit, + ) async { + await _authRepository.signOut(); + emit(AuthUnauthenticated()); + } + + Future _onPasswordResetRequested( + AuthPasswordResetRequested event, + Emitter emit, + ) async { + try { + await _authRepository.resetPassword(event.email); + emit(AuthPasswordResetSent(email: event.email)); + } catch (e) { + emit(AuthError(message: e.toString())); + } + } +} \ No newline at end of file diff --git a/lib/blocs/auth/auth_event.dart b/lib/blocs/auth/auth_event.dart new file mode 100644 index 0000000..c1e12aa --- /dev/null +++ b/lib/blocs/auth/auth_event.dart @@ -0,0 +1,55 @@ +import 'package:equatable/equatable.dart'; + +abstract class AuthEvent extends Equatable { + const AuthEvent(); + + @override + List get props => []; +} + +class AuthCheckRequested extends AuthEvent {} + +class AuthSignInRequested extends AuthEvent { + final String email; + final String password; + + const AuthSignInRequested({ + required this.email, + required this.password, + }); + + @override + List get props => [email, password]; +} + +class AuthSignUpRequested extends AuthEvent { + final String email; + final String password; + final String nom; + final String prenom; + + const AuthSignUpRequested({ + required this.email, + required this.password, + required this.nom, + required this.prenom, + }); + + @override + List get props => [email, password, nom, prenom]; +} + +class AuthGoogleSignInRequested extends AuthEvent {} + +class AuthAppleSignInRequested extends AuthEvent {} + +class AuthSignOutRequested extends AuthEvent {} + +class AuthPasswordResetRequested extends AuthEvent { + final String email; + + const AuthPasswordResetRequested({required this.email}); + + @override + List get props => [email]; +} \ No newline at end of file diff --git a/lib/blocs/auth/auth_state.dart b/lib/blocs/auth/auth_state.dart new file mode 100644 index 0000000..8806273 --- /dev/null +++ b/lib/blocs/auth/auth_state.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; +import '../../data/models/user.dart'; + +abstract class AuthState extends Equatable { + const AuthState(); + + @override + List get props => []; +} + +class AuthInitial extends AuthState {} + +class AuthLoading extends AuthState {} + +class AuthAuthenticated extends AuthState { + final User user; + + const AuthAuthenticated({required this.user}); + + @override + List get props => [user]; +} + +class AuthUnauthenticated extends AuthState {} + +class AuthError extends AuthState { + final String message; + + const AuthError({required this.message}); + + @override + List get props => [message]; +} + +class AuthPasswordResetSent extends AuthState { + final String email; + + const AuthPasswordResetSent({required this.email}); + + @override + List get props => [email]; +} \ No newline at end of file diff --git a/lib/blocs/theme/theme_bloc.dart b/lib/blocs/theme/theme_bloc.dart new file mode 100644 index 0000000..7b46526 --- /dev/null +++ b/lib/blocs/theme/theme_bloc.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'theme_event.dart'; +import 'theme_state.dart'; + +class ThemeBloc extends Bloc { + ThemeBloc() : super(const ThemeState()) { + on(_onThemeChanged); + on(_onThemeLoadRequested); + } + + Future _onThemeChanged( + ThemeChanged event, + Emitter emit, + ) async { + emit(state.copyWith(themeMode: event.themeMode)); + + // Sauvegarder la préférence + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('themeMode', event.themeMode.toString()); + } + + Future _onThemeLoadRequested( + ThemeLoadRequested event, + Emitter emit, + ) async { + final prefs = await SharedPreferences.getInstance(); + final themeModeString = prefs.getString('themeMode'); + + if (themeModeString != null) { + ThemeMode themeMode; + switch (themeModeString) { + case 'ThemeMode.light': + themeMode = ThemeMode.light; + break; + case 'ThemeMode.dark': + themeMode = ThemeMode.dark; + break; + default: + themeMode = ThemeMode.system; + } + emit(state.copyWith(themeMode: themeMode)); + } + } +} \ No newline at end of file diff --git a/lib/blocs/theme/theme_event.dart b/lib/blocs/theme/theme_event.dart new file mode 100644 index 0000000..da9c2be --- /dev/null +++ b/lib/blocs/theme/theme_event.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +abstract class ThemeEvent extends Equatable { + const ThemeEvent(); + + @override + List get props => []; +} + +class ThemeChanged extends ThemeEvent { + final ThemeMode themeMode; + + const ThemeChanged({required this.themeMode}); + + @override + List get props => [themeMode]; +} + +class ThemeLoadRequested extends ThemeEvent {} \ No newline at end of file diff --git a/lib/blocs/theme/theme_state.dart b/lib/blocs/theme/theme_state.dart new file mode 100644 index 0000000..a50806e --- /dev/null +++ b/lib/blocs/theme/theme_state.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +class ThemeState extends Equatable { + final ThemeMode themeMode; + + const ThemeState({this.themeMode = ThemeMode.system}); + + bool get isDarkMode { + return themeMode == ThemeMode.dark; + } + + ThemeState copyWith({ThemeMode? themeMode}) { + return ThemeState( + themeMode: themeMode ?? this.themeMode, + ); + } + + @override + List get props => [themeMode]; +} \ No newline at end of file diff --git a/lib/blocs/trip/trip_bloc.dart b/lib/blocs/trip/trip_bloc.dart new file mode 100644 index 0000000..37f1a4b --- /dev/null +++ b/lib/blocs/trip/trip_bloc.dart @@ -0,0 +1,118 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repositories/trip_repository.dart'; +import 'trip_event.dart'; +import 'trip_state.dart'; +import '../../data/models/trip.dart'; + +class TripBloc extends Bloc { + final TripRepository _tripRepository; + StreamSubscription? _tripsSubscription; + + TripBloc({required TripRepository tripRepository}) + : _tripRepository = tripRepository, + super(TripInitial()) { + on(_onLoadRequested); + on(_onCreateRequested); + on(_onUpdateRequested); + on(_onDeleteRequested); + on(_onParticipantAddRequested); + on(_onParticipantRemoveRequested); + } + + Future _onLoadRequested( + TripLoadRequested event, + Emitter emit, + ) async { + emit(TripLoading()); + + await _tripsSubscription?.cancel(); + + _tripsSubscription = _tripRepository.getUserTrips(event.userId).listen( + (trips) => add(const _TripUpdated(trips: [])), // Sera géré par un événement interne + onError: (error) => emit(TripError(message: error.toString())), + ); + } + + Future _onCreateRequested( + TripCreateRequested event, + Emitter emit, + ) async { + try { + await _tripRepository.createTrip(event.trip); + emit(const TripOperationSuccess(message: 'Voyage créé avec succès')); + } catch (e) { + emit(TripError(message: e.toString())); + } + } + + Future _onUpdateRequested( + TripUpdateRequested event, + Emitter emit, + ) async { + try { + await _tripRepository.updateTrip(event.trip); + emit(const TripOperationSuccess(message: 'Voyage mis à jour')); + } catch (e) { + emit(TripError(message: e.toString())); + } + } + + Future _onDeleteRequested( + TripDeleteRequested event, + Emitter emit, + ) async { + try { + await _tripRepository.deleteTrip(event.tripId); + emit(const TripOperationSuccess(message: 'Voyage supprimé')); + } catch (e) { + emit(TripError(message: e.toString())); + } + } + + Future _onParticipantAddRequested( + TripParticipantAddRequested event, + Emitter emit, + ) async { + try { + await _tripRepository.addParticipant( + event.tripId, + event.participantEmail, + ); + emit(const TripOperationSuccess(message: 'Participant ajouté')); + } catch (e) { + emit(TripError(message: e.toString())); + } + } + + Future _onParticipantRemoveRequested( + TripParticipantRemoveRequested event, + Emitter emit, + ) async { + try { + await _tripRepository.removeParticipant( + event.tripId, + event.participantEmail, + ); + emit(const TripOperationSuccess(message: 'Participant retiré')); + } catch (e) { + emit(TripError(message: e.toString())); + } + } + + @override + Future close() { + _tripsSubscription?.cancel(); + return super.close(); + } +} + +// Événement interne pour les mises à jour du stream +class _TripUpdated extends TripEvent { + final List trips; + + const _TripUpdated({required this.trips}); + + @override + List get props => [trips]; +} \ No newline at end of file diff --git a/lib/blocs/trip/trip_event.dart b/lib/blocs/trip/trip_event.dart new file mode 100644 index 0000000..d387e6f --- /dev/null +++ b/lib/blocs/trip/trip_event.dart @@ -0,0 +1,71 @@ +import 'package:equatable/equatable.dart'; +import '../../data/models/trip.dart'; + +abstract class TripEvent extends Equatable { + const TripEvent(); + + @override + List get props => []; +} + +class TripLoadRequested extends TripEvent { + final String userId; + + const TripLoadRequested({required this.userId}); + + @override + List get props => [userId]; +} + +class TripCreateRequested extends TripEvent { + final Trip trip; + + const TripCreateRequested({required this.trip}); + + @override + List get props => [trip]; +} + +class TripUpdateRequested extends TripEvent { + final Trip trip; + + const TripUpdateRequested({required this.trip}); + + @override + List get props => [trip]; +} + +class TripDeleteRequested extends TripEvent { + final String tripId; + + const TripDeleteRequested({required this.tripId}); + + @override + List get props => [tripId]; +} + +class TripParticipantAddRequested extends TripEvent { + final String tripId; + final String participantEmail; + + const TripParticipantAddRequested({ + required this.tripId, + required this.participantEmail, + }); + + @override + List get props => [tripId, participantEmail]; +} + +class TripParticipantRemoveRequested extends TripEvent { + final String tripId; + final String participantEmail; + + const TripParticipantRemoveRequested({ + required this.tripId, + required this.participantEmail, + }); + + @override + List get props => [tripId, participantEmail]; +} \ No newline at end of file diff --git a/lib/blocs/trip/trip_state.dart b/lib/blocs/trip/trip_state.dart new file mode 100644 index 0000000..1652e2d --- /dev/null +++ b/lib/blocs/trip/trip_state.dart @@ -0,0 +1,40 @@ +import 'package:equatable/equatable.dart'; +import '../../data/models/trip.dart'; + +abstract class TripState extends Equatable { + const TripState(); + + @override + List get props => []; +} + +class TripInitial extends TripState {} + +class TripLoading extends TripState {} + +class TripLoaded extends TripState { + final List trips; + + const TripLoaded({required this.trips}); + + @override + List get props => [trips]; +} + +class TripOperationSuccess extends TripState { + final String message; + + const TripOperationSuccess({required this.message}); + + @override + List get props => [message]; +} + +class TripError extends TripState { + final String message; + + const TripError({required this.message}); + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/lib/blocs/user/user_bloc.dart b/lib/blocs/user/user_bloc.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/blocs/user/user_event.dart b/lib/blocs/user/user_event.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/blocs/user/user_state.dart b/lib/blocs/user/user_state.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/components/home/create_trip_content.dart b/lib/components/home/create_trip_content.dart index fe0b1a8..6b5db29 100644 --- a/lib/components/home/create_trip_content.dart +++ b/lib/components/home/create_trip_content.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:travel_mate/models/trip.dart'; +import 'package:travel_mate/data/models/trip.dart'; import 'package:travel_mate/providers/user_provider.dart'; import 'package:travel_mate/services/trip_service.dart'; import 'package:travel_mate/services/group_service.dart'; -import 'package:travel_mate/models/group.dart'; +import 'package:travel_mate/data/models/group.dart'; class CreateTripContent extends StatefulWidget { diff --git a/lib/components/home/home_content.dart b/lib/components/home/home_content.dart index 566abf2..2099fa5 100644 --- a/lib/components/home/home_content.dart +++ b/lib/components/home/home_content.dart @@ -3,7 +3,7 @@ import 'package:provider/provider.dart'; import 'package:travel_mate/components/home/create_trip_content.dart'; import '../../providers/user_provider.dart'; import '../../services/trip_service.dart'; -import '../../models/trip.dart'; +import '../../data/models/trip.dart'; import '../home/show_trip_details_content.dart'; class HomeContent extends StatefulWidget { diff --git a/lib/components/home/show_trip_details_content.dart b/lib/components/home/show_trip_details_content.dart index 0a83e92..03fd1d8 100644 --- a/lib/components/home/show_trip_details_content.dart +++ b/lib/components/home/show_trip_details_content.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:travel_mate/models/trip.dart'; +import 'package:travel_mate/data/models/trip.dart'; class ShowTripDetailsContent extends StatefulWidget { final Trip trip; diff --git a/lib/data/data_sources/firestore_data_source.dart b/lib/data/data_sources/firestore_data_source.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/data/data_sources/local_data_source.dart b/lib/data/data_sources/local_data_source.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/models/group.dart b/lib/data/models/group.dart similarity index 100% rename from lib/models/group.dart rename to lib/data/models/group.dart diff --git a/lib/models/message.dart b/lib/data/models/message.dart similarity index 100% rename from lib/models/message.dart rename to lib/data/models/message.dart diff --git a/lib/models/trip.dart b/lib/data/models/trip.dart similarity index 100% rename from lib/models/trip.dart rename to lib/data/models/trip.dart diff --git a/lib/models/user.dart b/lib/data/models/user.dart similarity index 100% rename from lib/models/user.dart rename to lib/data/models/user.dart diff --git a/lib/main.dart b/lib/main.dart index 65e5e85..d7e52a7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,30 +1,25 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:travel_mate/pages/resetpswd.dart'; -import 'package:travel_mate/pages/signup.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'blocs/auth/auth_bloc.dart'; +import 'blocs/auth/auth_event.dart'; +import 'blocs/auth/auth_state.dart'; +import 'blocs/theme/theme_bloc.dart'; +import 'blocs/theme/theme_event.dart'; +import 'blocs/theme/theme_state.dart'; +import 'repositories/auth_repository.dart'; +import 'repositories/trip_repository.dart'; +import 'repositories/user_repository.dart'; import 'pages/login.dart'; import 'pages/home.dart'; -import 'providers/theme_provider.dart'; -import 'providers/user_provider.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'firebase_options.dart'; +import 'pages/signup.dart'; +import 'pages/resetpswd.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, -); - runApp( - MultiProvider( - providers: [ - ChangeNotifierProvider(create: (context) => ThemeProvider()), - ChangeNotifierProvider( - create: (context) => UserProvider(), - ), // Ajoutez cette ligne - ], - child: const MyApp(), - ), - ); + await Firebase.initializeApp(); + + runApp(const MyApp()); } class MyApp extends StatelessWidget { @@ -32,40 +27,67 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, themeProvider, child) { - return MaterialApp( - title: 'Travel Mate', - themeMode: themeProvider.themeMode, - - // Thème clair - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: const Color.fromARGB(255, 180, 180, 180), - brightness: Brightness.light, - ), - useMaterial3: true, + return MultiRepositoryProvider( + providers: [ + RepositoryProvider( + create: (context) => AuthRepository(), + ), + RepositoryProvider( + create: (context) => UserRepository(), + ), + RepositoryProvider( + create: (context) => TripRepository(), + ), + ], + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => ThemeBloc()..add(ThemeLoadRequested()), ), - - // Thème sombre - darkTheme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: const Color.fromARGB(255, 43, 43, 43), - brightness: Brightness.dark, - ), - useMaterial3: true, + BlocProvider( + create: (context) => AuthBloc( + authRepository: context.read(), + )..add(AuthCheckRequested()), ), - - initialRoute: '/login', - routes: { - '/login': (context) => const LoginPage(), - '/signup': (context) => const SignUpPage(), - '/home': (context) => const HomePage(), - '/forgot': (context) => const ForgotPasswordPage(), + ], + child: BlocBuilder( + builder: (context, themeState) { + return MaterialApp( + title: 'Travel Mate', + themeMode: themeState.themeMode, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color.fromARGB(255, 180, 180, 180), + brightness: Brightness.light, + ), + useMaterial3: true, + ), + darkTheme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color.fromARGB(255, 43, 43, 43), + brightness: Brightness.dark, + ), + useMaterial3: true, + ), + home: BlocBuilder( + builder: (context, authState) { + if (authState is AuthAuthenticated) { + return const HomePage(); + } + return const LoginPage(); + }, + ), + routes: { + '/login': (context) => const LoginPage(), + '/signup': (context) => const SignUpPage(), + '/home': (context) => const HomePage(), + '/forgot': (context) => const ForgotPasswordPage(), + }, + debugShowCheckedModeBanner: false, + ); }, - debugShowCheckedModeBanner: false, - ); - }, + ), + ), ); } } diff --git a/lib/pages/login.dart b/lib/pages/login.dart index d26e67d..d2f8fe7 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import '../services/auth_service.dart'; -import 'package:provider/provider.dart'; -import '../providers/user_provider.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../blocs/auth/auth_bloc.dart'; +import '../blocs/auth/auth_event.dart'; +import '../blocs/auth/auth_state.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -14,9 +15,6 @@ class _LoginPageState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); - final _authService = AuthService(); - - bool _isLoading = false; bool _obscurePassword = true; @override @@ -26,339 +24,6 @@ class _LoginPageState extends State { super.dispose(); } - // Méthode de connexion - Future _login() async { - if (!_formKey.currentState!.validate()) { - return; - } - - setState(() { - _isLoading = true; - }); - - try { - final userCredential = await _authService.signIn( - email: _emailController.text.trim(), - password: _passwordController.text, - ); - - if (mounted && userCredential.user != null) { - // Récupérer les données utilisateur depuis Firestore - final userProvider = Provider.of(context, listen: false); - final userData = await userProvider.getUserData(userCredential.user!.uid); - - if (userData != null) { - userProvider.setCurrentUser(userData); - Navigator.pushReplacementNamed(context, '/home'); - } else { - _showErrorMessage('Données utilisateur non trouvées'); - } - } - } catch (e) { - if (mounted) { - _showErrorMessage('Email ou mot de passe incorrect'); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - Future _signInWithGoogle() async { - setState(() { - _isLoading = true; - }); - - try { - final userCredential = await _authService.signInWithGoogle(); - - if (mounted && userCredential.user != null) { - final user = userCredential.user!; - final userProvider = Provider.of(context, listen: false); - - // Récupérer les données utilisateur depuis Firestore - final userData = await userProvider.getUserData(user.uid); - - if (userData != null) { - // L'utilisateur existe déjà - userProvider.setCurrentUser(userData); - Navigator.pushReplacementNamed(context, '/home'); - } else { - // L'utilisateur n'existe pas, créer son profil - final newUserData = { - 'uid': user.uid, - 'email': user.email ?? '', - 'name': user.displayName ?? 'Utilisateur', - }; - - // Créer le profil utilisateur dans Firestore - final createdUser = await userProvider.createUser(newUserData); - - if (createdUser != null) { - userProvider.setCurrentUser(createdUser); - Navigator.pushReplacementNamed(context, '/home'); - } else { - _showErrorMessage('Erreur lors de la création du profil utilisateur'); - } - } - } - } catch (e) { - if (mounted) { - _showErrorMessage('Erreur lors de la connexion avec Google: ${e.toString()}'); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Form( - key: _formKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 40), - - // Titre - Text( - 'Travel Mate', - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), - ), - - const SizedBox(height: 12), - - // Sous-titre - Text( - 'Connectez-vous pour continuer', - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - - const SizedBox(height: 32), - - // Champ email - TextFormField( - controller: _emailController, - validator: _validateEmail, - keyboardType: TextInputType.emailAddress, - decoration: InputDecoration( - labelText: 'Email', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - prefixIcon: Icon(Icons.email), - ), - ), - - const SizedBox(height: 16), - - // Champ mot de passe - TextFormField( - controller: _passwordController, - validator: _validatePassword, - obscureText: _obscurePassword, - decoration: InputDecoration( - labelText: 'Mot de passe', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - prefixIcon: Icon(Icons.lock), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword - ? Icons.visibility - : Icons.visibility_off, - ), - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - ), - ), - ), - - const SizedBox(height: 8), - - // Lien "Mot de passe oublié" - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () { - Navigator.pushNamed(context, '/forgot'); - }, - child: Text('Mot de passe oublié?'), - ), - ), - - const SizedBox(height: 24), - - // Bouton de connexion - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: _isLoading ? null : _login, - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: _isLoading - ? CircularProgressIndicator(color: Colors.white) - : Text( - 'Se connecter', - style: TextStyle(fontSize: 16), - ), - ), - ), - - const SizedBox(height: 24), - - // Lien d'inscription - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("Vous n'avez pas de compte?"), - TextButton( - onPressed: () { - Navigator.pushNamed(context, '/signup'); - }, - child: Text('S\'inscrire'), - ), - ], - ), - - const SizedBox(height: 40), - - // Séparateur - Container( - width: double.infinity, - height: 1, - color: Colors.grey.shade300, - ), - - const SizedBox(height: 40), - - Text( - 'Ou connectez-vous avec', - style: TextStyle(color: Colors.grey.shade600), - ), - - const SizedBox(height: 20), - - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - // GOOGLE - GestureDetector( - onTap: _isLoading ? null : _signInWithGoogle, - child: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.black, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.3), - spreadRadius: 1, - blurRadius: 3, - offset: Offset(0, 1), - ), - ], - border: Border.all( - color: Colors.grey.shade300, - width: 1, - ), - ), - child: Center( - child: Image.asset( - 'assets/icons/google.png', - width: 24, - height: 24, - ), - ), - ), - ), - const SizedBox(height: 8), - const Text('Google'), - ], - ), - - const SizedBox(width: 40), - - Column( - children: [ - // APPLE - GestureDetector( - onTap: () { - // TODO: Implémenter la connexion Apple - _showErrorMessage( - 'Connexion Apple non implémentée', - ); - }, - child: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.black, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.3), - spreadRadius: 1, - blurRadius: 3, - offset: Offset(0, 1), - ), - ], - border: Border.all( - color: Colors.grey.shade300, - width: 1, - ), - ), - child: Center( - child: Image.asset( - 'assets/icons/apple_white.png', - width: 24, - height: 24, - color: Colors.white, - ), - ), - ), - ), - const SizedBox(height: 8), - const Text('Apple'), - ], - ), - ], - ), - - const SizedBox(height: 40), - ], - ), - ), - ), - ), - ), - ); - } - - // Validation de l'email String? _validateEmail(String? value) { if (value == null || value.trim().isEmpty) { return 'Email requis'; @@ -370,7 +35,6 @@ class _LoginPageState extends State { return null; } - // Validation du mot de passe String? _validatePassword(String? value) { if (value == null || value.isEmpty) { return 'Mot de passe requis'; @@ -378,15 +42,281 @@ class _LoginPageState extends State { return null; } - void _showErrorMessage(String message) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - backgroundColor: Colors.red, - duration: const Duration(seconds: 3), - ), - ); + void _login(BuildContext context) { + if (!_formKey.currentState!.validate()) { + return; } + + context.read().add( + AuthSignInRequested( + email: _emailController.text.trim(), + password: _passwordController.text, + ), + ); } -} + + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocConsumer( + listener: (context, state) { + if (state is AuthAuthenticated) { + Navigator.pushReplacementNamed(context, '/home'); + } else if (state is AuthError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.red, + ), + ); + } + }, + builder: (context, state) { + final isLoading = state is AuthLoading; + + return SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 40), + const Text( + 'Travel Mate', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + const Text( + 'Connectez-vous pour continuer', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 32), + + // Email + TextFormField( + controller: _emailController, + validator: _validateEmail, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + labelText: 'Email', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.email), + ), + ), + + const SizedBox(height: 16), + + // Password + TextFormField( + controller: _passwordController, + validator: _validatePassword, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: 'Mot de passe', + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: const Icon(Icons.lock), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility + : Icons.visibility_off, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + ), + + const SizedBox(height: 8), + + // Forgot password + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () { + Navigator.pushNamed(context, '/forgot'); + }, + child: const Text('Mot de passe oublié?'), + ), + ), + + const SizedBox(height: 24), + + // Login button + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: isLoading ? null : () => _login(context), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: isLoading + ? const CircularProgressIndicator( + color: Colors.white, + ) + : const Text( + 'Se connecter', + style: TextStyle(fontSize: 16), + ), + ), + ), + + const SizedBox(height: 24), + + // Sign up link + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Vous n'avez pas de compte?"), + TextButton( + onPressed: () { + Navigator.pushNamed(context, '/signup'); + }, + child: const Text('S\'inscrire'), + ), + ], + ), + + const SizedBox(height: 40), + + // Divider + Container( + width: double.infinity, + height: 1, + color: Colors.grey.shade300, + ), + + const SizedBox(height: 40), + + const Text( + 'Ou connectez-vous avec', + style: TextStyle(color: Colors.grey), + ), + + const SizedBox(height: 20), + + // Social login buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Google + Column( + children: [ + GestureDetector( + onTap: isLoading + ? null + : () { + context + .read() + .add(AuthGoogleSignInRequested()); + }, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.black, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.3), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + border: Border.all( + color: Colors.grey.shade300, + width: 1, + ), + ), + child: Center( + child: Image.asset( + 'assets/icons/google.png', + width: 24, + height: 24, + ), + ), + ), + ), + const SizedBox(height: 8), + const Text('Google'), + ], + ), + + const SizedBox(width: 40), + + // Apple + Column( + children: [ + GestureDetector( + onTap: isLoading + ? null + : () { + context + .read() + .add(AuthAppleSignInRequested()); + }, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.black, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.3), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + border: Border.all( + color: Colors.grey.shade300, + width: 1, + ), + ), + child: Center( + child: Image.asset( + 'assets/icons/apple_white.png', + width: 24, + height: 24, + color: Colors.white, + ), + ), + ), + ), + const SizedBox(height: 8), + const Text('Apple'), + ], + ), + ], + ), + + const SizedBox(height: 40), + ], + ), + ), + ), + ), + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/signup.dart b/lib/pages/signup.dart index bc2e19f..0096322 100644 --- a/lib/pages/signup.dart +++ b/lib/pages/signup.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import '../models/user.dart'; -import '../services/auth_service.dart'; -import '../providers/user_provider.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../blocs/auth/auth_bloc.dart'; +import '../blocs/auth/auth_event.dart'; +import '../blocs/auth/auth_state.dart'; class SignUpPage extends StatefulWidget { const SignUpPage({super.key}); @@ -18,9 +18,7 @@ class _SignUpPageState extends State { final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); - final _authService = AuthService(); - bool _isLoading = false; bool _obscurePassword = true; bool _obscureConfirmPassword = true; @@ -34,7 +32,6 @@ class _SignUpPageState extends State { super.dispose(); } - // Méthode de validation String? _validateField(String? value, String fieldName) { if (value == null || value.trim().isEmpty) { return '$fieldName est requis'; @@ -46,7 +43,6 @@ class _SignUpPageState extends State { if (value == null || value.trim().isEmpty) { return 'Email est requis'; } - final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!emailRegex.hasMatch(value.trim())) { return 'Email invalide'; @@ -74,86 +70,19 @@ class _SignUpPageState extends State { return null; } - // Méthode d'enregistrement - Future _signUp() async { + void _signUp(BuildContext context) { if (!_formKey.currentState!.validate()) { return; } - setState(() { - _isLoading = true; - }); - - try { - // Créer le compte avec Firebase Auth - final userCredential = await _authService.createAccount( - email: _emailController.text.trim(), - password: _passwordController.text, - ); - - // Créer l'objet User - User newUser = User( - id: userCredential.user!.uid, - nom: _nomController.text.trim(), - prenom: _prenomController.text.trim(), - email: _emailController.text.trim(), - ); - - // Sauvegarder les données utilisateur dans Firestore - await Provider.of(context, listen: false).saveUserData(newUser); - - // Mettre à jour le displayName - await _authService.updateDisplayName(displayName: newUser.fullName); - - _showSuccessDialog(); - } catch (e) { - _showErrorDialog('Erreur lors de la création du compte: ${e.toString()}'); - } finally { - setState(() { - _isLoading = false; - }); - } - } - - void _showSuccessDialog() { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Succès'), - content: Text('Votre compte a été créé avec succès !'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); // Fermer la dialog - Navigator.of(context).pop(); // Retourner à la page de login - }, - child: Text('OK'), - ), - ], + context.read().add( + AuthSignUpRequested( + email: _emailController.text.trim(), + password: _passwordController.text, + nom: _nomController.text.trim(), + prenom: _prenomController.text.trim(), + ), ); - }, - ); - } - - void _showErrorDialog(String message) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Erreur'), - content: Text(message), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('OK'), - ), - ], - ); - }, - ); } @override @@ -161,185 +90,194 @@ class _SignUpPageState extends State { return Scaffold( appBar: AppBar( leading: IconButton( - icon: Icon(Icons.arrow_back), - onPressed: () { - Navigator.pop(context); - }, + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), ), backgroundColor: Colors.transparent, elevation: 0, ), - body: SafeArea( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( - children: [ - const SizedBox(height: 40), + body: BlocConsumer( + listener: (context, state) { + if (state is AuthAuthenticated) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Compte créé avec succès !'), + backgroundColor: Colors.green, + ), + ); + Navigator.pushReplacementNamed(context, '/home'); + } else if (state is AuthError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.red, + ), + ); + } + }, + builder: (context, state) { + final isLoading = state is AuthLoading; - const Text( - 'Créer un compte', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), - - const SizedBox(height: 12), - - const Text( - 'Rejoignez Travel Mate', - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - - const SizedBox(height: 40), - - // Champ Prénom - TextFormField( - controller: _prenomController, - validator: (value) => _validateField(value, 'Prénom'), - decoration: InputDecoration( - labelText: 'Prénom', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - prefixIcon: Icon(Icons.person_outline), - ), - ), - - const SizedBox(height: 20), - - // Champ Nom - TextFormField( - controller: _nomController, - validator: (value) => _validateField(value, 'Nom'), - decoration: InputDecoration( - labelText: 'Nom', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - prefixIcon: Icon(Icons.person), - ), - ), - - const SizedBox(height: 20), - - // Champ Email - TextFormField( - controller: _emailController, - validator: _validateEmail, - keyboardType: TextInputType.emailAddress, - decoration: InputDecoration( - labelText: 'Email', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - prefixIcon: Icon(Icons.email), - ), - ), - - const SizedBox(height: 20), - - // Champ Mot de passe - TextFormField( - controller: _passwordController, - validator: _validatePassword, - obscureText: _obscurePassword, - decoration: InputDecoration( - labelText: 'Mot de passe', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - prefixIcon: Icon(Icons.lock), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword - ? Icons.visibility - : Icons.visibility_off, - ), - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - ), - ), - ), - - const SizedBox(height: 20), - - // Champ Confirmation mot de passe - TextFormField( - controller: _confirmPasswordController, - validator: _validateConfirmPassword, - obscureText: _obscureConfirmPassword, - decoration: InputDecoration( - labelText: 'Confirmez le mot de passe', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - prefixIcon: Icon(Icons.lock_outline), - suffixIcon: IconButton( - icon: Icon( - _obscureConfirmPassword - ? Icons.visibility - : Icons.visibility_off, - ), - onPressed: () { - setState(() { - _obscureConfirmPassword = !_obscureConfirmPassword; - }); - }, - ), - ), - ), - - const SizedBox(height: 30), - - // Bouton d'inscription - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: _isLoading ? null : _signUp, - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: _isLoading - ? CircularProgressIndicator(color: Colors.white) - : Text('S\'inscrire', style: TextStyle(fontSize: 18)), - ), - ), - - const SizedBox(height: 20), - - // Lien vers login - Row( - mainAxisAlignment: MainAxisAlignment.center, + return SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( children: [ - const Text("Déjà un compte ? "), - GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: const Text( - 'Connectez-vous !', - style: TextStyle( - color: Colors.blue, - fontWeight: FontWeight.bold, + const SizedBox(height: 40), + const Text( + 'Créer un compte', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + const Text( + 'Rejoignez Travel Mate', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 40), + + // Champ Prénom + TextFormField( + controller: _prenomController, + validator: (value) => _validateField(value, 'Prénom'), + decoration: const InputDecoration( + labelText: 'Prénom', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.person_outline), + ), + ), + const SizedBox(height: 20), + + // Champ Nom + TextFormField( + controller: _nomController, + validator: (value) => _validateField(value, 'Nom'), + decoration: const InputDecoration( + labelText: 'Nom', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.person), + ), + ), + const SizedBox(height: 20), + + // Champ Email + TextFormField( + controller: _emailController, + validator: _validateEmail, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + labelText: 'Email', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.email), + ), + ), + const SizedBox(height: 20), + + // Champ Mot de passe + TextFormField( + controller: _passwordController, + validator: _validatePassword, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: 'Mot de passe', + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: const Icon(Icons.lock), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility + : Icons.visibility_off, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, ), ), ), + const SizedBox(height: 20), + + // Champ Confirmation mot de passe + TextFormField( + controller: _confirmPasswordController, + validator: _validateConfirmPassword, + obscureText: _obscureConfirmPassword, + decoration: InputDecoration( + labelText: 'Confirmez le mot de passe', + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: const Icon(Icons.lock_outline), + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword + ? Icons.visibility + : Icons.visibility_off, + ), + onPressed: () { + setState(() { + _obscureConfirmPassword = !_obscureConfirmPassword; + }); + }, + ), + ), + ), + const SizedBox(height: 30), + + // Bouton d'inscription + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: isLoading ? null : () => _signUp(context), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: isLoading + ? const CircularProgressIndicator(color: Colors.white) + : const Text('S\'inscrire', style: TextStyle(fontSize: 18)), + ), + ), + const SizedBox(height: 20), + + // Lien vers login + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Déjà un compte ? "), + GestureDetector( + onTap: () => Navigator.pop(context), + child: const Text( + 'Connectez-vous !', + style: TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), ], ), - ], + ), ), ), - ), - ), + ); + }, ), ); } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/lib/repositories/auth_repository.dart b/lib/repositories/auth_repository.dart new file mode 100644 index 0000000..fe27641 --- /dev/null +++ b/lib/repositories/auth_repository.dart @@ -0,0 +1,148 @@ +import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; +import 'package:cloud_firestore/cloud_firestore.dart'; +import '../data/models/user.dart'; +import '../services/auth_service.dart'; + +class AuthRepository { + final AuthService _authService; + final FirebaseFirestore _firestore; + + AuthRepository({ + AuthService? authService, + FirebaseFirestore? firestore, + }) : _authService = authService ?? AuthService(), + _firestore = firestore ?? FirebaseFirestore.instance; + + // Vérifier l'état de connexion actuel + Stream get authStateChanges => + _authService.authStateChanges; + + firebase_auth.User? get currentUser => _authService.currentUser; + + // Connexion avec email/mot de passe + Future signInWithEmailAndPassword({ + required String email, + required String password, + }) async { + try { + final firebaseUser = await _authService.signInWithEmailAndPassword( + email: email, + password: password, + ); + return await getUserFromFirestore(firebaseUser.user!.uid); + } catch (e) { + throw Exception('Erreur de connexion: $e'); + } + } + + // Inscription avec email/mot de passe + Future signUpWithEmailAndPassword({ + required String email, + required String password, + required String nom, + required String prenom, + }) async { + try { + final firebaseUser = await _authService.signUpWithEmailAndPassword( + email: email, + password: password, + ); + + // Créer le document utilisateur dans Firestore + final user = User( + id: firebaseUser.user!.uid, + email: email, + nom: nom, + prenom: prenom, + ); + + await _firestore.collection('users').doc(user.id).set(user.toMap()); + return user; + + } catch (e) { + throw Exception('Erreur d\'inscription: $e'); + } + } + + // Connexion avec Google + Future signInWithGoogle() async { + try { + final firebaseUser = await _authService.signInWithGoogle(); + + if (firebaseUser.user != null) { + // Vérifier si l'utilisateur existe déjà + final existingUser = await getUserFromFirestore(firebaseUser.user!.uid); + + if (existingUser != null) { + return existingUser; + } + + // Créer un nouvel utilisateur + final user = User( + id: firebaseUser.user!.uid, + email: firebaseUser.user!.email ?? '', + nom: '', + prenom: firebaseUser.user!.displayName ?? 'Utilisateur', + ); + + await _firestore.collection('users').doc(user.id).set(user.toMap()); + return user; + } + return null; + } catch (e) { + throw Exception('Erreur de connexion Google: $e'); + } + } + + // Connexion avec Apple + Future signInWithApple() async { + try { + final firebaseUser = await _authService.signInWithApple(); + + if (firebaseUser?.user != null) { + final existingUser = await getUserFromFirestore(firebaseUser!.user!.uid); + + if (existingUser != null) { + return existingUser; + } + + final user = User( + id: firebaseUser.user!.uid, + email: firebaseUser.user!.email ?? '', + nom: '', + prenom: firebaseUser.user!.displayName ?? 'Utilisateur', + ); + + await _firestore.collection('users').doc(user.id).set(user.toMap()); + return user; + } + return null; + } catch (e) { + throw Exception('Erreur de connexion Apple: $e'); + } + } + + // Déconnexion + Future signOut() async { + await _authService.signOut(); + } + + // Réinitialisation du mot de passe + Future resetPassword(String email) async { + await _authService.resetPassword(email); + } + + // Récupérer les données utilisateur depuis Firestore + Future getUserFromFirestore(String uid) async { + try { + final doc = await _firestore.collection('users').doc(uid).get(); + if (doc.exists) { + final data = doc.data() as Map; + return User.fromMap({...data, 'id': uid}); + } + return null; + } catch (e) { + return null; + } + } +} \ No newline at end of file diff --git a/lib/repositories/trip_repository.dart b/lib/repositories/trip_repository.dart new file mode 100644 index 0000000..f72279e --- /dev/null +++ b/lib/repositories/trip_repository.dart @@ -0,0 +1,113 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import '../data/models/trip.dart'; + +class TripRepository { + final FirebaseFirestore _firestore; + + TripRepository({FirebaseFirestore? firestore}) + : _firestore = firestore ?? FirebaseFirestore.instance; + + // Créer un voyage + Future createTrip(Trip trip) async { + try { + final docRef = await _firestore.collection('trips').add(trip.toMap()); + final createdTrip = trip.copyWith(id: docRef.id); + + // Mettre à jour avec l'ID généré + await docRef.update({'id': docRef.id}); + + return createdTrip; + } catch (e) { + throw Exception('Erreur lors de la création du voyage: $e'); + } + } + + // Récupérer les voyages d'un utilisateur + Stream> getUserTrips(String userId) { + return _firestore + .collection('trips') + .where('createdBy', isEqualTo: userId) + .snapshots() + .map((snapshot) { + return snapshot.docs.map((doc) { + final data = doc.data(); + return Trip.fromMap({...data, 'id': doc.id}); + }).toList(); + }); + } + + // Récupérer les voyages où l'utilisateur est participant + Stream> getSharedTrips(String userId) { + return _firestore + .collection('trips') + .where('participants', arrayContains: userId) + .snapshots() + .map((snapshot) { + return snapshot.docs.map((doc) { + final data = doc.data(); + return Trip.fromMap({...data, 'id': doc.id}); + }).toList(); + }); + } + + // Récupérer un voyage par ID + Future getTripById(String tripId) async { + try { + final doc = await _firestore.collection('trips').doc(tripId).get(); + if (doc.exists) { + final data = doc.data() as Map; + return Trip.fromMap({...data, 'id': doc.id}); + } + return null; + } catch (e) { + throw Exception('Erreur lors de la récupération du voyage: $e'); + } + } + + // Mettre à jour un voyage + Future updateTrip(Trip trip) async { + try { + await _firestore + .collection('trips') + .doc(trip.id) + .update(trip.toMap()); + return true; + } catch (e) { + throw Exception('Erreur lors de la mise à jour du voyage: $e'); + } + } + + // Supprimer un voyage + Future deleteTrip(String tripId) async { + try { + await _firestore.collection('trips').doc(tripId).delete(); + return true; + } catch (e) { + throw Exception('Erreur lors de la suppression du voyage: $e'); + } + } + + // Ajouter un participant + Future addParticipant(String tripId, String participantEmail) async { + try { + await _firestore.collection('trips').doc(tripId).update({ + 'participants': FieldValue.arrayUnion([participantEmail]) + }); + return true; + } catch (e) { + throw Exception('Erreur lors de l\'ajout du participant: $e'); + } + } + + // Retirer un participant + Future removeParticipant(String tripId, String participantEmail) async { + try { + await _firestore.collection('trips').doc(tripId).update({ + 'participants': FieldValue.arrayRemove([participantEmail]) + }); + return true; + } catch (e) { + throw Exception('Erreur lors du retrait du participant: $e'); + } + } +} \ No newline at end of file diff --git a/lib/repositories/user_repository.dart b/lib/repositories/user_repository.dart new file mode 100644 index 0000000..f1b591f --- /dev/null +++ b/lib/repositories/user_repository.dart @@ -0,0 +1,95 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import '../data/models/user.dart'; +import '../services/auth_service.dart'; + +class UserRepository { + final FirebaseFirestore _firestore; + final AuthService _authService; + + UserRepository({ + FirebaseFirestore? firestore, + AuthService? authService, + }) : _firestore = firestore ?? FirebaseFirestore.instance, + _authService = authService ?? AuthService(); + + // Récupérer un utilisateur par ID + Future getUserById(String uid) async { + try { + final doc = await _firestore.collection('users').doc(uid).get(); + if (doc.exists) { + final data = doc.data() as Map; + return User.fromMap({...data, 'id': uid}); + } + return null; + } catch (e) { + throw Exception('Erreur lors de la récupération de l\'utilisateur: $e'); + } + } + + // Récupérer un utilisateur par email + Future getUserByEmail(String email) async { + try { + final querySnapshot = await _firestore + .collection('users') + .where('email', isEqualTo: email.trim()) + .limit(1) + .get(); + + if (querySnapshot.docs.isNotEmpty) { + final doc = querySnapshot.docs.first; + final data = doc.data(); + return User.fromMap({...data, 'id': doc.id}); + } + return null; + } catch (e) { + throw Exception('Erreur lors de la recherche de l\'utilisateur: $e'); + } + } + + // Mettre à jour un utilisateur + Future updateUser(User user) async { + try { + await _firestore.collection('users').doc(user.id).update(user.toMap()); + + // Mettre à jour le displayName dans Firebase Auth + await _authService.updateDisplayName(displayName: user.fullName); + + return true; + } catch (e) { + throw Exception('Erreur lors de la mise à jour: $e'); + } + } + + // Supprimer un utilisateur + Future deleteUser(String uid) async { + try { + await _firestore.collection('users').doc(uid).delete(); + // Note: Vous devrez également supprimer le compte Firebase Auth + return true; + } catch (e) { + throw Exception('Erreur lors de la suppression: $e'); + } + } + + // Changer le mot de passe + Future changePassword({ + required String currentPassword, + required String newPassword, + }) async { + try { + final currentUser = _authService.currentUser; + if (currentUser?.email == null) { + throw Exception('Utilisateur non connecté ou email non disponible'); + } + + await _authService.resetPasswordFromCurrentPassword( + email: currentUser!.email!, + currentPassword: currentPassword, + newPassword: newPassword, + ); + return true; + } catch (e) { + throw Exception('Erreur lors du changement de mot de passe: $e'); + } + } +} \ No newline at end of file diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 21083ae..d0f72b8 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -9,7 +9,7 @@ class AuthService { Stream get authStateChanges => firebaseAuth.authStateChanges(); - Future signIn({ + Future signInWithEmailAndPassword({ required String email, required String password }) async { @@ -17,7 +17,7 @@ class AuthService { email: email, password: password); } - Future createAccount({ + Future signUpWithEmailAndPassword({ required String email, required String password }) async { @@ -100,5 +100,9 @@ class AuthService { rethrow; } } + + Future signInWithApple() async { + // TODO: Implémenter la connexion avec Apple + } } \ No newline at end of file diff --git a/lib/services/trip_service.dart b/lib/services/trip_service.dart index ad4b89b..7e91e5d 100644 --- a/lib/services/trip_service.dart +++ b/lib/services/trip_service.dart @@ -1,5 +1,5 @@ import 'package:cloud_firestore/cloud_firestore.dart'; -import '../models/trip.dart'; +import '../data/models/trip.dart'; class TripService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; @@ -53,26 +53,19 @@ class TripService { .collection(_tripsCollection) .snapshots() .map((snapshot) { - print('=== NOUVEAU SNAPSHOT ==='); - print('Nombre de documents: ${snapshot.docs.length}'); final List trips = []; for (int i = 0; i < snapshot.docs.length; i++) { var doc = snapshot.docs[i]; - print('\n--- Document $i (${doc.id}) ---'); try { - final data = doc.data() as Map; + final data = doc.data(); // Vérifier si l'utilisateur est impliqué dans ce voyage final String createdBy = data['createdBy']?.toString() ?? ''; final List participants = data['participants'] ?? []; - print('CreatedBy: "$createdBy"'); - print('UserId: "$userId"'); - print('Participants: $participants'); - bool userIsInvolved = false; String reason = ''; @@ -88,20 +81,11 @@ class TripService { reason = reason.isEmpty ? 'Participant par ID' : '$reason + Participant par ID'; } - print('Utilisateur impliqué: $userIsInvolved'); - print('Raison: $reason'); - if (userIsInvolved) { - print('Tentative de conversion du trip...'); final trip = _convertDocumentToTrip(doc.id, data); if (trip != null) { trips.add(trip); - print('Trip ajouté: ${trip.title}'); - } else { - print('Échec de la conversion du trip'); } - } else { - print('Utilisateur non impliqué dans ce voyage'); } } catch (e, stackTrace) { print('Erreur lors du traitement du document ${doc.id}: $e'); @@ -109,14 +93,6 @@ class TripService { } } - print('\n=== RÉSUMÉ ==='); - print('Trips trouvés: ${trips.length}'); - if (trips.isNotEmpty) { - for (int i = 0; i < trips.length; i++) { - print(' ${i+1}. ${trips[i].title} (${trips[i].id})'); - } - } - // Trier par date de création (les plus récents en premier) trips.sort((a, b) { try { @@ -138,8 +114,6 @@ class TripService { // Obtenir les voyages d'un utilisateur (version simplifiée) Future> getTripsByUser(String userId) async { try { - print('Récupération des voyages pour userId: $userId'); - // Récupérer d'abord les voyages créés par l'utilisateur final QuerySnapshot createdTrips = await _firestore .collection(_tripsCollection) @@ -176,9 +150,7 @@ class TripService { } // Méthode helper pour convertir un document Firestore en Trip - Trip? _convertDocumentToTrip(String docId, Map data) { - print('\n=== CONVERSION TRIP $docId ==='); - + Trip? _convertDocumentToTrip(String docId, Map data) { try { // Créer une copie des données pour ne pas modifier l'original Map processedData = Map.from(data); @@ -211,7 +183,7 @@ class TripService { return trip; } catch (e, stackTrace) { - print('❌ Erreur lors de la conversion du document $docId: $e'); + print('Erreur lors de la conversion du document $docId: $e'); print('StackTrace: $stackTrace'); return null; } diff --git a/pubspec.lock b/pubspec.lock index 4a08512..13b4fbc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.3" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" boolean_selector: dependency: transitive description: @@ -97,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -174,6 +190,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" flutter_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0da8f9b..40259da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,8 @@ dependencies: path_provider: ^2.1.1 bcrypt: ^1.1.3 location: ^5.0.0 + flutter_bloc : ^8.1.3 + equatable: ^2.0.5 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.