Refactor signup page to use BLoC pattern and implement authentication repository

- Updated signup.dart to replace Provider with BLoC for state management.
- Created AuthRepository to handle authentication logic and Firestore user management.
- Added TripRepository and UserRepository for trip and user data management.
- Implemented methods for user sign-in, sign-up, and data retrieval in repositories.
- Enhanced trip management with create, update, delete, and participant management functionalities.
- Updated AuthService to include new methods for sign-in and sign-up.
- Removed unnecessary print statements from TripService for cleaner code.
- Added dependencies for flutter_bloc and equatable in pubspec.yaml.

Not tested yet
This commit is contained in:
Dayron
2025-10-14 10:53:28 +02:00
parent a467b92979
commit c4588a65c0
31 changed files with 1500 additions and 689 deletions

View File

@@ -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<AuthEvent, AuthState> {
final AuthRepository _authRepository;
AuthBloc({required AuthRepository authRepository})
: _authRepository = authRepository,
super(AuthInitial()) {
on<AuthCheckRequested>(_onAuthCheckRequested);
on<AuthSignInRequested>(_onSignInRequested);
on<AuthSignUpRequested>(_onSignUpRequested);
on<AuthGoogleSignInRequested>(_onGoogleSignInRequested);
on<AuthAppleSignInRequested>(_onAppleSignInRequested);
on<AuthSignOutRequested>(_onSignOutRequested);
on<AuthPasswordResetRequested>(_onPasswordResetRequested);
}
Future<void> _onAuthCheckRequested(
AuthCheckRequested event,
Emitter<AuthState> 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<void> _onSignInRequested(
AuthSignInRequested event,
Emitter<AuthState> 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<void> _onSignUpRequested(
AuthSignUpRequested event,
Emitter<AuthState> 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<void> _onGoogleSignInRequested(
AuthGoogleSignInRequested event,
Emitter<AuthState> 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<void> _onAppleSignInRequested(
AuthAppleSignInRequested event,
Emitter<AuthState> 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<void> _onSignOutRequested(
AuthSignOutRequested event,
Emitter<AuthState> emit,
) async {
await _authRepository.signOut();
emit(AuthUnauthenticated());
}
Future<void> _onPasswordResetRequested(
AuthPasswordResetRequested event,
Emitter<AuthState> emit,
) async {
try {
await _authRepository.resetPassword(event.email);
emit(AuthPasswordResetSent(email: event.email));
} catch (e) {
emit(AuthError(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,55 @@
import 'package:equatable/equatable.dart';
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object?> 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<Object?> 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<Object?> 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<Object?> get props => [email];
}

View File

@@ -0,0 +1,42 @@
import 'package:equatable/equatable.dart';
import '../../data/models/user.dart';
abstract class AuthState extends Equatable {
const AuthState();
@override
List<Object?> get props => [];
}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
final User user;
const AuthAuthenticated({required this.user});
@override
List<Object?> get props => [user];
}
class AuthUnauthenticated extends AuthState {}
class AuthError extends AuthState {
final String message;
const AuthError({required this.message});
@override
List<Object?> get props => [message];
}
class AuthPasswordResetSent extends AuthState {
final String email;
const AuthPasswordResetSent({required this.email});
@override
List<Object?> get props => [email];
}

View File

@@ -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<ThemeEvent, ThemeState> {
ThemeBloc() : super(const ThemeState()) {
on<ThemeChanged>(_onThemeChanged);
on<ThemeLoadRequested>(_onThemeLoadRequested);
}
Future<void> _onThemeChanged(
ThemeChanged event,
Emitter<ThemeState> 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<void> _onThemeLoadRequested(
ThemeLoadRequested event,
Emitter<ThemeState> 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));
}
}
}

View File

@@ -0,0 +1,20 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
abstract class ThemeEvent extends Equatable {
const ThemeEvent();
@override
List<Object?> get props => [];
}
class ThemeChanged extends ThemeEvent {
final ThemeMode themeMode;
const ThemeChanged({required this.themeMode});
@override
List<Object?> get props => [themeMode];
}
class ThemeLoadRequested extends ThemeEvent {}

View File

@@ -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<Object?> get props => [themeMode];
}

View File

@@ -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<TripEvent, TripState> {
final TripRepository _tripRepository;
StreamSubscription? _tripsSubscription;
TripBloc({required TripRepository tripRepository})
: _tripRepository = tripRepository,
super(TripInitial()) {
on<TripLoadRequested>(_onLoadRequested);
on<TripCreateRequested>(_onCreateRequested);
on<TripUpdateRequested>(_onUpdateRequested);
on<TripDeleteRequested>(_onDeleteRequested);
on<TripParticipantAddRequested>(_onParticipantAddRequested);
on<TripParticipantRemoveRequested>(_onParticipantRemoveRequested);
}
Future<void> _onLoadRequested(
TripLoadRequested event,
Emitter<TripState> 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<void> _onCreateRequested(
TripCreateRequested event,
Emitter<TripState> 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<void> _onUpdateRequested(
TripUpdateRequested event,
Emitter<TripState> emit,
) async {
try {
await _tripRepository.updateTrip(event.trip);
emit(const TripOperationSuccess(message: 'Voyage mis à jour'));
} catch (e) {
emit(TripError(message: e.toString()));
}
}
Future<void> _onDeleteRequested(
TripDeleteRequested event,
Emitter<TripState> emit,
) async {
try {
await _tripRepository.deleteTrip(event.tripId);
emit(const TripOperationSuccess(message: 'Voyage supprimé'));
} catch (e) {
emit(TripError(message: e.toString()));
}
}
Future<void> _onParticipantAddRequested(
TripParticipantAddRequested event,
Emitter<TripState> emit,
) async {
try {
await _tripRepository.addParticipant(
event.tripId,
event.participantEmail,
);
emit(const TripOperationSuccess(message: 'Participant ajouté'));
} catch (e) {
emit(TripError(message: e.toString()));
}
}
Future<void> _onParticipantRemoveRequested(
TripParticipantRemoveRequested event,
Emitter<TripState> 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<void> close() {
_tripsSubscription?.cancel();
return super.close();
}
}
// Événement interne pour les mises à jour du stream
class _TripUpdated extends TripEvent {
final List<Trip> trips;
const _TripUpdated({required this.trips});
@override
List<Object?> get props => [trips];
}

View File

@@ -0,0 +1,71 @@
import 'package:equatable/equatable.dart';
import '../../data/models/trip.dart';
abstract class TripEvent extends Equatable {
const TripEvent();
@override
List<Object?> get props => [];
}
class TripLoadRequested extends TripEvent {
final String userId;
const TripLoadRequested({required this.userId});
@override
List<Object?> get props => [userId];
}
class TripCreateRequested extends TripEvent {
final Trip trip;
const TripCreateRequested({required this.trip});
@override
List<Object?> get props => [trip];
}
class TripUpdateRequested extends TripEvent {
final Trip trip;
const TripUpdateRequested({required this.trip});
@override
List<Object?> get props => [trip];
}
class TripDeleteRequested extends TripEvent {
final String tripId;
const TripDeleteRequested({required this.tripId});
@override
List<Object?> get props => [tripId];
}
class TripParticipantAddRequested extends TripEvent {
final String tripId;
final String participantEmail;
const TripParticipantAddRequested({
required this.tripId,
required this.participantEmail,
});
@override
List<Object?> get props => [tripId, participantEmail];
}
class TripParticipantRemoveRequested extends TripEvent {
final String tripId;
final String participantEmail;
const TripParticipantRemoveRequested({
required this.tripId,
required this.participantEmail,
});
@override
List<Object?> get props => [tripId, participantEmail];
}

View File

@@ -0,0 +1,40 @@
import 'package:equatable/equatable.dart';
import '../../data/models/trip.dart';
abstract class TripState extends Equatable {
const TripState();
@override
List<Object?> get props => [];
}
class TripInitial extends TripState {}
class TripLoading extends TripState {}
class TripLoaded extends TripState {
final List<Trip> trips;
const TripLoaded({required this.trips});
@override
List<Object?> get props => [trips];
}
class TripOperationSuccess extends TripState {
final String message;
const TripOperationSuccess({required this.message});
@override
List<Object?> get props => [message];
}
class TripError extends TripState {
final String message;
const TripError({required this.message});
@override
List<Object?> get props => [message];
}

View File

View File

View File

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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<ThemeProvider>(
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<AuthRepository>(
create: (context) => AuthRepository(),
),
RepositoryProvider<UserRepository>(
create: (context) => UserRepository(),
),
RepositoryProvider<TripRepository>(
create: (context) => TripRepository(),
),
],
child: MultiBlocProvider(
providers: [
BlocProvider<ThemeBloc>(
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<AuthBloc>(
create: (context) => AuthBloc(
authRepository: context.read<AuthRepository>(),
)..add(AuthCheckRequested()),
),
initialRoute: '/login',
routes: {
'/login': (context) => const LoginPage(),
'/signup': (context) => const SignUpPage(),
'/home': (context) => const HomePage(),
'/forgot': (context) => const ForgotPasswordPage(),
],
child: BlocBuilder<ThemeBloc, ThemeState>(
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<AuthBloc, AuthState>(
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,
);
},
),
),
);
}
}

View File

@@ -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<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _authService = AuthService();
bool _isLoading = false;
bool _obscurePassword = true;
@override
@@ -26,339 +24,6 @@ class _LoginPageState extends State<LoginPage> {
super.dispose();
}
// Méthode de connexion
Future<void> _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<UserProvider>(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<void> _signInWithGoogle() async {
setState(() {
_isLoading = true;
});
try {
final userCredential = await _authService.signInWithGoogle();
if (mounted && userCredential.user != null) {
final user = userCredential.user!;
final userProvider = Provider.of<UserProvider>(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<LoginPage> {
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<LoginPage> {
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<AuthBloc>().add(
AuthSignInRequested(
email: _emailController.text.trim(),
password: _passwordController.text,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocConsumer<AuthBloc, AuthState>(
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<AuthBloc>()
.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<AuthBloc>()
.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),
],
),
),
),
),
);
},
),
);
}
}

View File

@@ -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<SignUpPage> {
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<SignUpPage> {
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<SignUpPage> {
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<SignUpPage> {
return null;
}
// Méthode d'enregistrement
Future<void> _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<UserProvider>(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<AuthBloc>().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<SignUpPage> {
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<AuthBloc, AuthState>(
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,
),
),
),
],
),
],
),
],
),
),
),
),
),
);
},
),
);
}
}
}

View File

@@ -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<firebase_auth.User?> get authStateChanges =>
_authService.authStateChanges;
firebase_auth.User? get currentUser => _authService.currentUser;
// Connexion avec email/mot de passe
Future<User?> 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<User?> 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<User?> 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<User?> 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<void> signOut() async {
await _authService.signOut();
}
// Réinitialisation du mot de passe
Future<void> resetPassword(String email) async {
await _authService.resetPassword(email);
}
// Récupérer les données utilisateur depuis Firestore
Future<User?> getUserFromFirestore(String uid) async {
try {
final doc = await _firestore.collection('users').doc(uid).get();
if (doc.exists) {
final data = doc.data() as Map<String, dynamic>;
return User.fromMap({...data, 'id': uid});
}
return null;
} catch (e) {
return null;
}
}
}

View File

@@ -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<Trip> 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<List<Trip>> 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<List<Trip>> 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<Trip?> getTripById(String tripId) async {
try {
final doc = await _firestore.collection('trips').doc(tripId).get();
if (doc.exists) {
final data = doc.data() as Map<String, dynamic>;
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<bool> 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<bool> 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<bool> 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<bool> 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');
}
}
}

View File

@@ -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<User?> getUserById(String uid) async {
try {
final doc = await _firestore.collection('users').doc(uid).get();
if (doc.exists) {
final data = doc.data() as Map<String, dynamic>;
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<User?> 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<bool> 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<bool> 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<bool> 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');
}
}
}

View File

@@ -9,7 +9,7 @@ class AuthService {
Stream<User?> get authStateChanges => firebaseAuth.authStateChanges();
Future<UserCredential> signIn({
Future<UserCredential> signInWithEmailAndPassword({
required String email,
required String password
}) async {
@@ -17,7 +17,7 @@ class AuthService {
email: email, password: password);
}
Future<UserCredential> createAccount({
Future<UserCredential> 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
}
}

View File

@@ -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<Trip> 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<String, dynamic>;
final data = doc.data();
// Vérifier si l'utilisateur est impliqué dans ce voyage
final String createdBy = data['createdBy']?.toString() ?? '';
final List<dynamic> 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<List<Trip>> 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<String, dynamic> data) {
print('\n=== CONVERSION TRIP $docId ===');
Trip? _convertDocumentToTrip(String docId, Map<String, dynamic> data) {
try {
// Créer une copie des données pour ne pas modifier l'original
Map<String, dynamic> processedData = Map<String, dynamic>.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;
}

View File

@@ -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:

View File

@@ -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.