diff --git a/lib/blocs/auth/auth_bloc.dart b/lib/blocs/auth/auth_bloc.dart index baa3dfa..fa6b714 100644 --- a/lib/blocs/auth/auth_bloc.dart +++ b/lib/blocs/auth/auth_bloc.dart @@ -1,16 +1,16 @@ /// Business Logic Component for managing authentication state. -/// +/// /// The [AuthBloc] handles authentication-related events and manages the /// authentication state throughout the application. It coordinates with /// the [AuthRepository] to perform authentication operations and emits /// appropriate states based on the results. -/// +/// /// Supported authentication methods: /// - Email and password authentication /// - Google Sign-In /// - Apple Sign-In /// - Password reset functionality -/// +/// /// This bloc handles the following events: /// - [AuthCheckRequested]: Verifies current authentication status /// - [AuthSignInRequested]: Processes email/password sign-in @@ -20,6 +20,7 @@ /// - [AuthSignOutRequested]: Processes user sign-out /// - [AuthPasswordResetRequested]: Processes password reset requests library; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../repositories/auth_repository.dart'; import 'auth_event.dart'; @@ -31,23 +32,25 @@ class AuthBloc extends Bloc { final AuthRepository _authRepository; /// Creates an [AuthBloc] with the provided [authRepository]. - /// + /// /// The bloc starts in the [AuthInitial] state and registers event handlers /// for all supported authentication events. AuthBloc({required AuthRepository authRepository}) - : _authRepository = authRepository, - super(AuthInitial()) { + : _authRepository = authRepository, + super(AuthInitial()) { on(_onAuthCheckRequested); on(_onSignInRequested); on(_onSignUpRequested); on(_onGoogleSignInRequested); + on(_onGoogleSignUpRequested); on(_onAppleSignInRequested); + on(_onAppleSignUpRequested); on(_onSignOutRequested); on(_onPasswordResetRequested); } /// Handles [AuthCheckRequested] events. - /// + /// /// Checks if a user is currently authenticated and emits the appropriate state. /// If a user is found, attempts to fetch user data from Firestore. Future _onAuthCheckRequested( @@ -55,14 +58,16 @@ class AuthBloc extends Bloc { Emitter emit, ) async { emit(AuthLoading()); - + try { final currentUser = _authRepository.currentUser; - + if (currentUser != null) { // Fetch user data from Firestore - final user = await _authRepository.getUserFromFirestore(currentUser.uid); - + final user = await _authRepository.getUserFromFirestore( + currentUser.uid, + ); + if (user != null) { emit(AuthAuthenticated(user: user)); } else { @@ -77,7 +82,7 @@ class AuthBloc extends Bloc { } /// Handles [AuthSignInRequested] events. - /// + /// /// Attempts to sign in a user with the provided email and password. /// Emits [AuthAuthenticated] on success or [AuthError] on failure. Future _onSignInRequested( @@ -85,7 +90,7 @@ class AuthBloc extends Bloc { Emitter emit, ) async { emit(AuthLoading()); - + try { final user = await _authRepository.signInWithEmailAndPassword( email: event.email, @@ -103,7 +108,7 @@ class AuthBloc extends Bloc { } /// Handles [AuthSignUpRequested] events. - /// + /// /// Attempts to create a new user account with the provided information. /// Emits [AuthAuthenticated] on success or [AuthError] on failure. Future _onSignUpRequested( @@ -111,19 +116,20 @@ class AuthBloc extends Bloc { Emitter emit, ) async { emit(AuthLoading()); - + try { final user = await _authRepository.signUpWithEmailAndPassword( email: event.email, password: event.password, nom: event.nom, prenom: event.prenom, + phoneNumber: event.phoneNumber, ); if (user != null) { emit(AuthAuthenticated(user: user)); } else { - emit(const AuthError(message: 'Registration failed')); + emit(const AuthError(message: 'Failed to create account')); } } catch (e) { emit(AuthError(message: e.toString())); @@ -131,7 +137,7 @@ class AuthBloc extends Bloc { } /// Handles [AuthGoogleSignInRequested] events. - /// + /// /// Attempts to sign in the user using Google authentication. /// Emits [AuthAuthenticated] on success or [AuthError] on failure. Future _onGoogleSignInRequested( @@ -139,14 +145,57 @@ class AuthBloc extends Bloc { Emitter emit, ) async { emit(AuthLoading()); - + try { final user = await _authRepository.signInWithGoogle(); if (user != null) { emit(AuthAuthenticated(user: user)); } else { - emit(const AuthError(message: 'Google sign-in cancelled')); + emit( + const AuthError( + message: + 'Utilisateur n\'est pas encore inscrit avec Google, veuillez créer un compte avec Google', + ), + ); + } + } catch (e) { + emit(AuthError(message: e.toString())); + } + } + + Future _onGoogleSignUpRequested( + AuthGoogleSignUpRequested event, + Emitter emit, + ) async { + // This method can be implemented if needed for Google sign-up specific logic. + emit(AuthLoading()); + try { + final user = await _authRepository.signUpWithGoogle(event.phoneNumber); + + if (user != null) { + emit(AuthAuthenticated(user: user)); + } else { + emit(const AuthError(message: 'Failed to create account with Google')); + } + } catch (e) { + emit(AuthError(message: e.toString())); + } + } + + Future _onAppleSignUpRequested( + AuthAppleSignUpRequested event, + Emitter emit, + ) async { + // This method can be implemented if needed for Apple sign-up specific logic. + emit(AuthLoading()); + try { + final user = await _authRepository.signUpWithApple(event.phoneNumber); + + if (user != null) { + emit(AuthAuthenticated(user: user)); + } else { + emit(const AuthError(message: 'Failed to create account with Apple')); } } catch (e) { emit(AuthError(message: e.toString())); @@ -154,7 +203,7 @@ class AuthBloc extends Bloc { } /// Handles [AuthAppleSignInRequested] events. - /// + /// /// Attempts to sign in the user using Apple authentication. /// Emits [AuthAuthenticated] on success or [AuthError] on failure. Future _onAppleSignInRequested( @@ -162,14 +211,19 @@ class AuthBloc extends Bloc { Emitter emit, ) async { emit(AuthLoading()); - + try { final user = await _authRepository.signInWithApple(); if (user != null) { emit(AuthAuthenticated(user: user)); } else { - emit(const AuthError(message: 'Apple sign-in cancelled')); + emit( + const AuthError( + message: + 'Utilisateur n\'est pas encore inscrit avec Apple, veuillez créer un compte avec Apple', + ), + ); } } catch (e) { emit(AuthError(message: e.toString())); @@ -177,7 +231,7 @@ class AuthBloc extends Bloc { } /// Handles [AuthSignOutRequested] events. - /// + /// /// Signs out the current user and emits [AuthUnauthenticated]. Future _onSignOutRequested( AuthSignOutRequested event, @@ -188,7 +242,7 @@ class AuthBloc extends Bloc { } /// Handles [AuthPasswordResetRequested] events. - /// + /// /// Sends a password reset email to the specified email address. /// Emits [AuthPasswordResetSent] on success or [AuthError] on failure. Future _onPasswordResetRequested( @@ -202,4 +256,4 @@ class AuthBloc extends Bloc { 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 index e15fd91..1c04713 100644 --- a/lib/blocs/auth/auth_event.dart +++ b/lib/blocs/auth/auth_event.dart @@ -1,7 +1,7 @@ import 'package:equatable/equatable.dart'; /// Abstract base class for all authentication-related events. -/// +/// /// This class extends [Equatable] to enable value equality for event comparison. /// All authentication events in the application should inherit from this class. abstract class AuthEvent extends Equatable { @@ -13,59 +13,60 @@ abstract class AuthEvent extends Equatable { } /// Event to check the current authentication status. -/// +/// /// This event is typically dispatched when the app starts to determine /// if a user is already authenticated. class AuthCheckRequested extends AuthEvent {} /// Event to request user sign-in with email and password. -/// +/// /// This event contains the user's credentials and triggers the authentication /// process when dispatched to the [AuthBloc]. class AuthSignInRequested extends AuthEvent { /// The user's email address. final String email; - + /// The user's password. final String password; /// Creates a new [AuthSignInRequested] event. - /// + /// /// Both [email] and [password] are required parameters. - const AuthSignInRequested({ - required this.email, - required this.password, - }); + const AuthSignInRequested({required this.email, required this.password}); @override List get props => [email, password]; } /// Event to request user registration with email, password, and personal information. -/// +/// /// This event contains all necessary information to create a new user account /// and triggers the registration process when dispatched to the [AuthBloc]. class AuthSignUpRequested extends AuthEvent { /// The user's email address. final String email; - + /// The user's password. final String password; - + /// The user's last name. final String nom; - + /// The user's first name. final String prenom; + /// The user's phone number. + final String phoneNumber; + /// Creates a new [AuthSignUpRequested] event. - /// + /// /// All parameters are required for user registration. const AuthSignUpRequested({ required this.email, required this.password, required this.nom, required this.prenom, + required this.phoneNumber, }); @override @@ -73,23 +74,49 @@ class AuthSignUpRequested extends AuthEvent { } /// Event to request user sign-in using Google authentication. -/// +/// /// This event triggers the Google sign-in flow when dispatched to the [AuthBloc]. class AuthGoogleSignInRequested extends AuthEvent {} +class AuthGoogleSignUpRequested extends AuthEvent { + /// The user's phone number. + final String phoneNumber; + + /// Creates a new [AuthGoogleSignUpRequested] event. + /// + /// The [phoneNumber] parameter is required. + const AuthGoogleSignUpRequested({required this.phoneNumber}); + + @override + List get props => [phoneNumber]; +} + /// Event to request user sign-in using Apple authentication. -/// +/// /// This event triggers the Apple sign-in flow when dispatched to the [AuthBloc]. class AuthAppleSignInRequested extends AuthEvent {} +class AuthAppleSignUpRequested extends AuthEvent { + /// The user's phone number. + final String phoneNumber; + + /// Creates a new [AuthAppleSignUpRequested] event. + /// + /// The [phoneNumber] parameter is required. + const AuthAppleSignUpRequested({required this.phoneNumber}); + + @override + List get props => [phoneNumber]; +} + /// Event to request user sign-out. -/// +/// /// This event triggers the sign-out process and clears the user session /// when dispatched to the [AuthBloc]. class AuthSignOutRequested extends AuthEvent {} /// Event to request a password reset for a user account. -/// +/// /// This event triggers the password reset process by sending a reset email /// to the specified email address. class AuthPasswordResetRequested extends AuthEvent { @@ -97,10 +124,10 @@ class AuthPasswordResetRequested extends AuthEvent { final String email; /// Creates a new [AuthPasswordResetRequested] event. - /// + /// /// The [email] parameter is required. const AuthPasswordResetRequested({required this.email}); @override List get props => [email]; -} \ No newline at end of file +} diff --git a/lib/components/activities/activities_page.dart b/lib/components/activities/activities_page.dart index 3c24f6a..68f736f 100644 --- a/lib/components/activities/activities_page.dart +++ b/lib/components/activities/activities_page.dart @@ -126,7 +126,6 @@ class _ActivitiesPageState extends State } if (state is ActivityLoaded) { - print('✅ Activités chargées: ${state.activities.length}'); // Stocker les activités localement WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { @@ -137,28 +136,18 @@ class _ActivitiesPageState extends State _isLoadingTripActivities = false; }); - print( - '🔄 [ActivityLoaded] Activités du voyage mises à jour: ${_tripActivities.length}', - ); // Vérifier si on a besoin de charger plus d'activités dans les suggestions Future.delayed(const Duration(milliseconds: 500), () { - print( - '🚀 [ActivityLoaded] Déclenchement de la vérification auto-reload', - ); _checkAndLoadMoreActivitiesIfNeeded(); }); }); } if (state is ActivitySearchResults) { - print('🔍 Résultats Google: ${state.searchResults.length}'); // Déclencher l'auto-reload uniquement pour la recherche initiale (6 résultats) // et pas pour les rechargements automatiques if (state.searchResults.length <= 6 && !_autoReloadInProgress) { WidgetsBinding.instance.addPostFrameCallback((_) { - print( - '🎯 [ActivitySearchResults] Première recherche avec peu de résultats, vérification auto-reload', - ); Future.delayed(const Duration(milliseconds: 500), () { _checkAndLoadMoreActivitiesIfNeeded(); }); @@ -167,13 +156,11 @@ class _ActivitiesPageState extends State } if (state is ActivityVoteRecorded) { - print('�️ Vote enregistré pour activité: ${state.activityId}'); // Recharger les activités du voyage pour mettre à jour les votes _loadActivities(); } if (state is ActivityAdded) { - print('✅ Activité ajoutée avec succès: ${state.activity.name}'); // Recharger automatiquement les activités du voyage _loadActivities(); } @@ -654,10 +641,6 @@ class _ActivitiesPageState extends State ); }).toList(); - print( - '🔍 [Google Search] ${googleActivities.length} résultats trouvés, ${filteredActivities.length} après filtrage', - ); - if (filteredActivities.isEmpty && googleActivities.isNotEmpty) { return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -764,18 +747,6 @@ class _ActivitiesPageState extends State padding: const EdgeInsets.symmetric(horizontal: 16), child: OutlinedButton.icon( onPressed: () { - print( - '🧪 [DEBUG] Force auto-reload check - État actuel:', - ); - print( - '🧪 [DEBUG] _tripActivities: ${_tripActivities.length}', - ); - print( - '🧪 [DEBUG] _autoReloadInProgress: $_autoReloadInProgress', - ); - print( - '🧪 [DEBUG] _lastAutoReloadTriggerCount: $_lastAutoReloadTriggerCount', - ); _checkAndLoadMoreActivitiesIfNeeded(); }, icon: const Icon(Icons.bug_report, size: 16), @@ -1251,8 +1222,6 @@ class _ActivitiesPageState extends State } void _voteForActivity(String activityId, int vote) { - print('🗳️ Vote pour activité $activityId: $vote'); - // TODO: Récupérer l'ID utilisateur actuel // Pour l'instant, on utilise un ID temporaire final userId = 'current_user_id'; @@ -1275,8 +1244,6 @@ class _ActivitiesPageState extends State } void _addGoogleActivityToTrip(Activity activity) { - print('➕ [Add Activity] Adding ${activity.name} to trip'); - // Créer une nouvelle activité avec l'ID du voyage final newActivity = activity.copyWith( tripId: widget.trip.id, @@ -1307,7 +1274,6 @@ class _ActivitiesPageState extends State void _checkAndLoadMoreActivitiesIfNeeded() { // Protection contre les rechargements en boucle if (_autoReloadInProgress) { - print('⏸️ [Auto-reload] Auto-reload déjà en cours, skip'); return; } @@ -1315,13 +1281,6 @@ class _ActivitiesPageState extends State if (currentState is ActivitySearchResults) { final googleActivities = currentState.searchResults; - print( - '🔍 [Auto-reload] Activités du voyage en mémoire: ${_tripActivities.length}', - ); - print( - '🔍 [Auto-reload] Activités Google total: ${googleActivities.length}', - ); - // Filtrer les activités déjà présentes dans le voyage final filteredActivities = googleActivities.where((googleActivity) { final isDuplicate = _tripActivities.any( @@ -1329,21 +1288,12 @@ class _ActivitiesPageState extends State tripActivity.name.toLowerCase().trim() == googleActivity.name.toLowerCase().trim(), ); - if (isDuplicate) { - print('🔍 [Auto-reload] Activité filtrée: ${googleActivity.name}'); - } + if (isDuplicate) {} return !isDuplicate; }).toList(); - print( - '🔍 [Auto-reload] ${filteredActivities.length} activités visibles après filtrage sur ${googleActivities.length} total', - ); - // Protection: ne pas redéclencher pour le même nombre d'activités Google if (googleActivities.length == _lastAutoReloadTriggerCount) { - print( - '🔒 [Auto-reload] Même nombre qu\'avant (${googleActivities.length}), skip pour éviter la boucle', - ); return; } @@ -1360,13 +1310,6 @@ class _ActivitiesPageState extends State activitiesNeeded + 6; // Activités actuelles + ce qui manque + buffer de 6 - print( - '🔄 [Auto-reload] DÉCLENCHEMENT: Besoin de $activitiesNeeded activités supplémentaires', - ); - print( - '📊 [Auto-reload] Demande totale: $newTotalToRequest activités (actuellement: ${googleActivities.length})', - ); - // Mettre à jour le compteur et recharger avec le nouveau total _totalGoogleActivitiesRequested = newTotalToRequest; _loadMoreGoogleActivitiesWithTotal(newTotalToRequest); @@ -1374,35 +1317,19 @@ class _ActivitiesPageState extends State // Libérer le verrou après un délai Future.delayed(const Duration(seconds: 3), () { _autoReloadInProgress = false; - print('🔓 [Auto-reload] Verrou libéré'); }); } else if (filteredActivities.length >= 4) { - print( - '✅ [Auto-reload] Suffisamment d\'activités visibles (${filteredActivities.length} >= 4)', - ); - } else { - print( - '🚫 [Auto-reload] Trop d\'activités Google déjà chargées (${googleActivities.length} >= 20), arrêt auto-reload', - ); - } - } else { - print( - '⚠️ [Auto-reload] État pas prêt pour auto-chargement: ${currentState.runtimeType}', - ); - } + } else {} + } else {} } void _searchGoogleActivities() { - print('🔍 [Google Search] Initializing first search with 6 results'); _totalGoogleActivitiesRequested = 6; // Reset du compteur _autoReloadInProgress = false; // Reset des protections _lastAutoReloadTriggerCount = 0; // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles if (widget.trip.hasCoordinates) { - print( - '🌍 [Google Search] Using pre-geocoded coordinates: ${widget.trip.latitude}, ${widget.trip.longitude}', - ); context.read().add( SearchActivitiesWithCoordinates( tripId: widget.trip.id!, @@ -1414,9 +1341,6 @@ class _ActivitiesPageState extends State ), ); } else { - print( - '⚠️ [Google Search] No coordinates available, falling back to destination geocoding', - ); context.read().add( SearchActivities( tripId: widget.trip.id!, @@ -1431,16 +1355,12 @@ class _ActivitiesPageState extends State } void _resetAndSearchGoogleActivities() { - print('🔄 [Google Search] Resetting and starting fresh search'); _totalGoogleActivitiesRequested = 6; // Reset du compteur _autoReloadInProgress = false; // Reset des protections _lastAutoReloadTriggerCount = 0; // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles if (widget.trip.hasCoordinates) { - print( - '🌍 [Google Search] Using pre-geocoded coordinates: ${widget.trip.latitude}, ${widget.trip.longitude}', - ); context.read().add( SearchActivitiesWithCoordinates( tripId: widget.trip.id!, @@ -1452,9 +1372,6 @@ class _ActivitiesPageState extends State ), ); } else { - print( - '⚠️ [Google Search] No coordinates available, falling back to destination geocoding', - ); context.read().add( SearchActivities( tripId: widget.trip.id!, @@ -1469,23 +1386,16 @@ class _ActivitiesPageState extends State } void _loadMoreGoogleActivities() { - print('📄 [Google Search] Loading more activities (next 6 results)'); final currentState = context.read().state; if (currentState is ActivitySearchResults) { final currentCount = currentState.searchResults.length; final newTotal = currentCount + 6; - print( - '📊 [Google Search] Current results count: $currentCount, requesting total: $newTotal', - ); _totalGoogleActivitiesRequested = newTotal; // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles if (widget.trip.hasCoordinates) { - print( - '🌍 [Google Search] Using pre-geocoded coordinates for more results', - ); context.read().add( SearchActivitiesWithCoordinates( tripId: widget.trip.id!, @@ -1497,9 +1407,6 @@ class _ActivitiesPageState extends State ), ); } else { - print( - '⚠️ [Google Search] No coordinates available, falling back to destination geocoding', - ); context.read().add( SearchActivities( tripId: widget.trip.id!, @@ -1514,26 +1421,15 @@ class _ActivitiesPageState extends State } void _loadMoreGoogleActivitiesWithTotal(int totalToRequest) { - print( - '📈 [Google Search] Loading activities with specific total: $totalToRequest', - ); - // Au lieu de reset, on utilise l'offset et append pour forcer plus de résultats final currentState = context.read().state; if (currentState is ActivitySearchResults) { final currentCount = currentState.searchResults.length; final additionalNeeded = totalToRequest - currentCount; - print( - '📊 [Google Search] Current: $currentCount, Total demandé: $totalToRequest, Additional: $additionalNeeded', - ); - if (additionalNeeded > 0) { // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles if (widget.trip.hasCoordinates) { - print( - '🌍 [Google Search] Using pre-geocoded coordinates for additional results', - ); context.read().add( SearchActivitiesWithCoordinates( tripId: widget.trip.id!, @@ -1546,9 +1442,6 @@ class _ActivitiesPageState extends State ), ); } else { - print( - '⚠️ [Google Search] No coordinates available, falling back to destination geocoding', - ); context.read().add( SearchActivities( tripId: widget.trip.id!, @@ -1560,15 +1453,10 @@ class _ActivitiesPageState extends State ), ); } - } else { - print('⚠️ [Google Search] Pas besoin de charger plus (déjà suffisant)'); - } + } else {} } else { // Si pas de résultats existants, faire une recherche complète if (widget.trip.hasCoordinates) { - print( - '🌍 [Google Search] Using pre-geocoded coordinates for fresh search', - ); context.read().add( SearchActivitiesWithCoordinates( tripId: widget.trip.id!, @@ -1580,9 +1468,6 @@ class _ActivitiesPageState extends State ), ); } else { - print( - '⚠️ [Google Search] No coordinates available, falling back to destination geocoding', - ); context.read().add( SearchActivities( tripId: widget.trip.id!, diff --git a/lib/components/loading/laoding_content.dart b/lib/components/loading/laoding_content.dart new file mode 100644 index 0000000..a3ca6f9 --- /dev/null +++ b/lib/components/loading/laoding_content.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; + +class LoadingContent extends StatefulWidget { + final Future Function()? onBackgroundTask; + final String? loadingText; + final VoidCallback? onComplete; + + const LoadingContent({ + Key? key, + this.onBackgroundTask, + this.loadingText = "Chargement en cours...", + this.onComplete, + }) : super(key: key); + + @override + State createState() => _LoadingContentState(); +} + +class _LoadingContentState extends State + with TickerProviderStateMixin { + late AnimationController _rotationController; + late AnimationController _pulseController; + late Animation _rotationAnimation; + late Animation _pulseAnimation; + + @override + void initState() { + super.initState(); + + _rotationController = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(); + + _pulseController = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + )..repeat(reverse: true); + + _rotationAnimation = Tween( + begin: 0, + end: 1, + ).animate(_rotationController); + + _pulseAnimation = Tween(begin: 0.8, end: 1.2).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); + + _executeBackgroundTask(); + } + + Future _executeBackgroundTask() async { + if (widget.onBackgroundTask != null) { + try { + await widget.onBackgroundTask!(); + if (widget.onComplete != null) { + widget.onComplete!(); + } + } catch (e) { + // Gérer les erreurs si nécessaire + print('Erreur lors de la tâche en arrière-plan: $e'); + } + } + } + + @override + void dispose() { + _rotationController.dispose(); + _pulseController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Animation de rotation et pulsation + AnimatedBuilder( + animation: Listenable.merge([ + _rotationAnimation, + _pulseAnimation, + ]), + builder: (context, child) { + return Transform.scale( + scale: _pulseAnimation.value, + child: RotationTransition( + turns: _rotationAnimation, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + Colors.blue.shade400, + Colors.purple.shade400, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.3), + blurRadius: 20, + spreadRadius: 5, + ), + ], + ), + child: const Icon( + Icons.travel_explore, + color: Colors.white, + size: 40, + ), + ), + ), + ); + }, + ), + const SizedBox(height: 40), + + // Texte de chargement avec animation + AnimatedBuilder( + animation: _pulseController, + builder: (context, child) { + return Opacity( + opacity: _pulseAnimation.value - 0.3, + child: Text( + widget.loadingText ?? "Chargement en cours...", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Colors.grey.shade700, + ), + textAlign: TextAlign.center, + ), + ); + }, + ), + const SizedBox(height: 20), + + // Indicateur de progression linéaire + SizedBox( + width: 200, + child: LinearProgressIndicator( + backgroundColor: Colors.grey.shade300, + valueColor: AlwaysStoppedAnimation(Colors.blue.shade400), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/signup/sign_up_platform_content.dart b/lib/components/signup/sign_up_platform_content.dart new file mode 100644 index 0000000..97c8874 --- /dev/null +++ b/lib/components/signup/sign_up_platform_content.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; + +class SignUpPlatformContent extends StatefulWidget { + final String platform; // 'google' ou 'apple' + final String? email; + final String? phoneNumber; + final String? name; + final String? firstName; + + const SignUpPlatformContent({ + Key? key, + required this.platform, + this.email, + this.phoneNumber, + this.name, + this.firstName, + }) : super(key: key); + + @override + State createState() => _SignUpPlatformContentState(); +} + +class _SignUpPlatformContentState extends State { + final _formKey = GlobalKey(); + late TextEditingController _nameController; + late TextEditingController _firstNameController; + late TextEditingController _emailController; + late TextEditingController _phoneController; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.name ?? ''); + _firstNameController = TextEditingController(text: widget.firstName ?? ''); + _emailController = TextEditingController(text: widget.email ?? ''); + _phoneController = TextEditingController(text: widget.phoneNumber ?? ''); + } + + @override + void dispose() { + _nameController.dispose(); + _firstNameController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + super.dispose(); + } + + String? _validateField(String? value, String fieldName) { + if (value == null || value.isEmpty) { + return '$fieldName est requis'; + } + return null; + } + + void _submitForm() { + if (_formKey.currentState!.validate()) { + // Traitement de l'inscription + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Profil complété avec succès!')), + ); + } + } + + Widget _buildPlatformIndicator() { + Color platformColor = widget.platform == 'google' + ? Colors.red + : Colors.black; + IconData platformIcon = widget.platform == 'google' + ? Icons.g_mobiledata + : Icons.apple; + String platformName = widget.platform == 'google' ? 'Google' : 'Apple'; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: widget.platform == 'google' + ? Colors.red.shade50 + : Colors.black.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: platformColor, width: 1), + ), + child: Row( + children: [ + Icon(platformIcon, color: platformColor, size: 24), + const SizedBox(width: 8), + Text( + 'Connecté avec $platformName', + style: TextStyle( + color: platformColor, + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => Navigator.pop(context), + ), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Indicateur de plateforme + _buildPlatformIndicator(), + const SizedBox(height: 32), + + // Titre + const Text( + 'Complétez votre profil', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + + Text( + 'Vérifiez et complétez vos informations', + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + + // Champ Nom + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Nom', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.person_outline), + ), + validator: (value) => _validateField(value, 'Nom'), + ), + const SizedBox(height: 16), + + // Champ Prénom + TextFormField( + controller: _firstNameController, + decoration: const InputDecoration( + labelText: 'Prénom', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.person_outline), + ), + validator: (value) => _validateField(value, 'Prénom'), + ), + const SizedBox(height: 16), + + // Champ Email (non modifiable) + TextFormField( + controller: _emailController, + decoration: InputDecoration( + labelText: 'Email', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.email_outlined), + suffixIcon: Icon( + Icons.lock_outline, + color: Colors.grey[400], + ), + ), + enabled: false, + style: TextStyle(color: Colors.grey[600]), + ), + const SizedBox(height: 8), + Text( + 'Email fourni par ${widget.platform == 'google' ? 'Google' : 'Apple'}', + style: TextStyle(fontSize: 12, color: Colors.grey[500]), + ), + const SizedBox(height: 16), + + // Champ Téléphone + TextFormField( + controller: _phoneController, + decoration: const InputDecoration( + labelText: 'Numéro de téléphone', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.phone_outlined), + hintText: '+33 6 12 34 56 78', + ), + keyboardType: TextInputType.phone, + validator: (value) => + _validateField(value, 'Numéro de téléphone'), + ), + const SizedBox(height: 32), + + // Bouton de confirmation + ElevatedButton( + onPressed: _submitForm, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Confirmer mon inscription', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + const SizedBox(height: 16), + + // Bouton secondaire + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text( + 'Modifier la méthode de connexion', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/signup.dart b/lib/pages/signup.dart index 0096322..ac140db 100644 --- a/lib/pages/signup.dart +++ b/lib/pages/signup.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sign_in_button/sign_in_button.dart'; +import 'package:travel_mate/components/loading/laoding_content.dart'; +import 'package:travel_mate/components/signup/sign_up_platform_content.dart'; import '../blocs/auth/auth_bloc.dart'; import '../blocs/auth/auth_event.dart'; import '../blocs/auth/auth_state.dart'; +import '../services/error_service.dart'; class SignUpPage extends StatefulWidget { const SignUpPage({super.key}); @@ -18,10 +22,13 @@ class _SignUpPageState extends State { final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); + final _phoneNumberController = TextEditingController(); bool _obscurePassword = true; bool _obscureConfirmPassword = true; + final _errorService = ErrorService(); + @override void dispose() { _nomController.dispose(); @@ -76,13 +83,14 @@ class _SignUpPageState extends State { } context.read().add( - AuthSignUpRequested( - email: _emailController.text.trim(), - password: _passwordController.text, - nom: _nomController.text.trim(), - prenom: _prenomController.text.trim(), - ), - ); + AuthSignUpRequested( + email: _emailController.text.trim(), + password: _passwordController.text, + nom: _nomController.text.trim(), + prenom: _prenomController.text.trim(), + phoneNumber: _phoneNumberController.text.trim(), + ), + ); } @override @@ -107,11 +115,8 @@ class _SignUpPageState extends State { ); Navigator.pushReplacementNamed(context, '/home'); } else if (state is AuthError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message), - backgroundColor: Colors.red, - ), + _errorService.showError( + message: 'Erreur lors de la création du compte', ); } }, @@ -129,7 +134,10 @@ class _SignUpPageState extends State { const SizedBox(height: 40), const Text( 'Créer un compte', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: 12), const Text( @@ -166,6 +174,21 @@ class _SignUpPageState extends State { ), const SizedBox(height: 20), + // Champ Numéro de téléphone + TextFormField( + controller: _phoneNumberController, + validator: (value) => + _validateField(value, 'Numéro de téléphone'), + decoration: const InputDecoration( + labelText: 'Numéro de téléphone', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.phone), + ), + ), + const SizedBox(height: 20), + // Champ Email TextFormField( controller: _emailController, @@ -227,7 +250,8 @@ class _SignUpPageState extends State { ), onPressed: () { setState(() { - _obscureConfirmPassword = !_obscureConfirmPassword; + _obscureConfirmPassword = + !_obscureConfirmPassword; }); }, ), @@ -247,8 +271,13 @@ class _SignUpPageState extends State { ), ), child: isLoading - ? const CircularProgressIndicator(color: Colors.white) - : const Text('S\'inscrire', style: TextStyle(fontSize: 18)), + ? const CircularProgressIndicator( + color: Colors.white, + ) + : const Text( + 'S\'inscrire', + style: TextStyle(fontSize: 18), + ), ), ), const SizedBox(height: 20), @@ -270,6 +299,86 @@ class _SignUpPageState extends State { ), ], ), + + const SizedBox(height: 20), + + // Divider + Container( + width: double.infinity, + height: 1, + color: Colors.grey.shade300, + ), + + const SizedBox(height: 20), + + const Text( + 'Ou inscrivez-vous avec', + style: TextStyle(color: Colors.grey), + ), + + const SizedBox(height: 20), + + SignInButton( + Buttons.google, + onPressed: () async { + // Afficher la page de loading + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LoadingContent( + onBackgroundTask: () async { + // Effectuer la requête vers Google + final platformData = await _fetchGoogleSignInData(); + return platformData; + }, + onComplete: () { + // Fermer le loading et naviguer vers SignUpPlatformContent + Navigator.pop(context); // Fermer LoadingContent + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + SignUpPlatformContent(platform: 'google'), + ), + ); + }, + ), + ), + ); + }, + ), + + const SizedBox(height: 16), + + SignInButton( + Buttons.apple, + onPressed: () async { + // Afficher la page de loading + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LoadingContent( + onBackgroundTask: () async { + // Effectuer la requête vers Google + final platformData = await + return platformData; + }, + onComplete: () { + // Fermer le loading et naviguer vers SignUpPlatformContent + Navigator.pop(context); // Fermer LoadingContent + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + SignUpPlatformContent(platform: 'apple'), + ), + ); + }, + ), + ), + ); + }, + ), ], ), ), @@ -280,4 +389,4 @@ class _SignUpPageState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/repositories/auth_repository.dart b/lib/repositories/auth_repository.dart index 6afb966..6237580 100644 --- a/lib/repositories/auth_repository.dart +++ b/lib/repositories/auth_repository.dart @@ -73,6 +73,7 @@ class AuthRepository { /// [password] - User's password /// [nom] - User's last name /// [prenom] - User's first name + /// [phoneNumber] - User's phone number /// /// Returns the created [User] model if successful. /// Throws an exception if account creation fails. @@ -81,6 +82,7 @@ class AuthRepository { required String password, required String nom, required String prenom, + required String phoneNumber, }) async { try { final firebaseUser = await _authService.signUpWithEmailAndPassword( @@ -94,7 +96,7 @@ class AuthRepository { email: email, nom: nom, prenom: prenom, - phoneNumber: 'Uknown', + phoneNumber: phoneNumber, platform: 'email', profilePictureUrl: '', ); @@ -114,7 +116,7 @@ class AuthRepository { /// /// Returns the [User] model if successful, null if Google sign-in was cancelled. /// Throws an exception if authentication fails. - Future signInWithGoogle() async { + Future signUpWithGoogle(String phoneNumber) async { try { final firebaseUser = await _authService.signInWithGoogle(); @@ -132,8 +134,8 @@ class AuthRepository { email: firebaseUser.user!.email ?? '', nom: '', prenom: firebaseUser.user!.displayName ?? 'User', - phoneNumber: firebaseUser.user!.phoneNumber ?? 'Uknown', - profilePictureUrl: firebaseUser.user!.photoURL, + phoneNumber: phoneNumber, + profilePictureUrl: firebaseUser.user!.photoURL ?? 'Unknown', platform: 'google', ); @@ -147,6 +149,21 @@ class AuthRepository { return null; } + Future signInWithGoogle() async { + try { + final firebaseUser = await _authService.signInWithGoogle(); + final user = await getUserFromFirestore(firebaseUser.user!.uid); + if (user != null) { + return user; + } else { + throw Exception('Utilisateur non trouvé'); + } + } catch (e) { + _errorService.showError(message: 'Erreur lors de la connexion Google'); + } + return null; + } + /// Signs in a user using Apple authentication. /// /// Handles Apple sign-in flow and creates/retrieves user data from Firestore. @@ -154,7 +171,7 @@ class AuthRepository { /// /// Returns the [User] model if successful, null if Apple sign-in was cancelled. /// Throws an exception if authentication fails. - Future signInWithApple() async { + Future signUpWithApple(String phoneNumber) async { try { final firebaseUser = await _authService.signInWithApple(); @@ -170,8 +187,8 @@ class AuthRepository { email: firebaseUser.user!.email ?? '', nom: '', prenom: firebaseUser.user!.displayName ?? 'User', - phoneNumber: firebaseUser.user!.phoneNumber ?? 'Uknown', - profilePictureUrl: firebaseUser.user!.photoURL, + phoneNumber: phoneNumber, + profilePictureUrl: firebaseUser.user!.photoURL ?? 'Unknown', platform: 'apple', ); @@ -185,6 +202,21 @@ class AuthRepository { return null; } + Future signInWithApple() async { + try { + final firebaseUser = await _authService.signInWithApple(); + final user = await getUserFromFirestore(firebaseUser.user!.uid); + if (user != null) { + return user; + } else { + throw Exception('Utilisateur non trouvé'); + } + } catch (e) { + _errorService.showError(message: 'Erreur lors de la connexion Apple'); + } + return null; + } + /// Signs out the current user. /// /// Clears the authentication state and signs out from Firebase Auth.