fAdd phone number support to user authentication events and methods

This commit is contained in:
Van Leemput Dayron
2025-11-05 13:09:12 +01:00
parent 5977f4d0da
commit fa7daca50a
7 changed files with 685 additions and 188 deletions

View File

@@ -20,6 +20,7 @@
/// - [AuthSignOutRequested]: Processes user sign-out /// - [AuthSignOutRequested]: Processes user sign-out
/// - [AuthPasswordResetRequested]: Processes password reset requests /// - [AuthPasswordResetRequested]: Processes password reset requests
library; library;
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repositories/auth_repository.dart'; import '../../repositories/auth_repository.dart';
import 'auth_event.dart'; import 'auth_event.dart';
@@ -41,7 +42,9 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
on<AuthSignInRequested>(_onSignInRequested); on<AuthSignInRequested>(_onSignInRequested);
on<AuthSignUpRequested>(_onSignUpRequested); on<AuthSignUpRequested>(_onSignUpRequested);
on<AuthGoogleSignInRequested>(_onGoogleSignInRequested); on<AuthGoogleSignInRequested>(_onGoogleSignInRequested);
on<AuthGoogleSignUpRequested>(_onGoogleSignUpRequested);
on<AuthAppleSignInRequested>(_onAppleSignInRequested); on<AuthAppleSignInRequested>(_onAppleSignInRequested);
on<AuthAppleSignUpRequested>(_onAppleSignUpRequested);
on<AuthSignOutRequested>(_onSignOutRequested); on<AuthSignOutRequested>(_onSignOutRequested);
on<AuthPasswordResetRequested>(_onPasswordResetRequested); on<AuthPasswordResetRequested>(_onPasswordResetRequested);
} }
@@ -61,7 +64,9 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
if (currentUser != null) { if (currentUser != null) {
// Fetch user data from Firestore // Fetch user data from Firestore
final user = await _authRepository.getUserFromFirestore(currentUser.uid); final user = await _authRepository.getUserFromFirestore(
currentUser.uid,
);
if (user != null) { if (user != null) {
emit(AuthAuthenticated(user: user)); emit(AuthAuthenticated(user: user));
@@ -118,12 +123,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
password: event.password, password: event.password,
nom: event.nom, nom: event.nom,
prenom: event.prenom, prenom: event.prenom,
phoneNumber: event.phoneNumber,
); );
if (user != null) { if (user != null) {
emit(AuthAuthenticated(user: user)); emit(AuthAuthenticated(user: user));
} else { } else {
emit(const AuthError(message: 'Registration failed')); emit(const AuthError(message: 'Failed to create account'));
} }
} catch (e) { } catch (e) {
emit(AuthError(message: e.toString())); emit(AuthError(message: e.toString()));
@@ -146,7 +152,50 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
if (user != null) { if (user != null) {
emit(AuthAuthenticated(user: user)); emit(AuthAuthenticated(user: user));
} else { } 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<void> _onGoogleSignUpRequested(
AuthGoogleSignUpRequested event,
Emitter<AuthState> 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<void> _onAppleSignUpRequested(
AuthAppleSignUpRequested event,
Emitter<AuthState> 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) { } catch (e) {
emit(AuthError(message: e.toString())); emit(AuthError(message: e.toString()));
@@ -169,7 +218,12 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
if (user != null) { if (user != null) {
emit(AuthAuthenticated(user: user)); emit(AuthAuthenticated(user: user));
} else { } 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) { } catch (e) {
emit(AuthError(message: e.toString())); emit(AuthError(message: e.toString()));

View File

@@ -32,10 +32,7 @@ class AuthSignInRequested extends AuthEvent {
/// Creates a new [AuthSignInRequested] event. /// Creates a new [AuthSignInRequested] event.
/// ///
/// Both [email] and [password] are required parameters. /// Both [email] and [password] are required parameters.
const AuthSignInRequested({ const AuthSignInRequested({required this.email, required this.password});
required this.email,
required this.password,
});
@override @override
List<Object?> get props => [email, password]; List<Object?> get props => [email, password];
@@ -58,6 +55,9 @@ class AuthSignUpRequested extends AuthEvent {
/// The user's first name. /// The user's first name.
final String prenom; final String prenom;
/// The user's phone number.
final String phoneNumber;
/// Creates a new [AuthSignUpRequested] event. /// Creates a new [AuthSignUpRequested] event.
/// ///
/// All parameters are required for user registration. /// All parameters are required for user registration.
@@ -66,6 +66,7 @@ class AuthSignUpRequested extends AuthEvent {
required this.password, required this.password,
required this.nom, required this.nom,
required this.prenom, required this.prenom,
required this.phoneNumber,
}); });
@override @override
@@ -77,11 +78,37 @@ class AuthSignUpRequested extends AuthEvent {
/// This event triggers the Google sign-in flow when dispatched to the [AuthBloc]. /// This event triggers the Google sign-in flow when dispatched to the [AuthBloc].
class AuthGoogleSignInRequested extends AuthEvent {} 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<Object?> get props => [phoneNumber];
}
/// Event to request user sign-in using Apple authentication. /// Event to request user sign-in using Apple authentication.
/// ///
/// This event triggers the Apple sign-in flow when dispatched to the [AuthBloc]. /// This event triggers the Apple sign-in flow when dispatched to the [AuthBloc].
class AuthAppleSignInRequested extends AuthEvent {} 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<Object?> get props => [phoneNumber];
}
/// Event to request user sign-out. /// Event to request user sign-out.
/// ///
/// This event triggers the sign-out process and clears the user session /// This event triggers the sign-out process and clears the user session

View File

@@ -126,7 +126,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
} }
if (state is ActivityLoaded) { if (state is ActivityLoaded) {
print('✅ Activités chargées: ${state.activities.length}');
// Stocker les activités localement // Stocker les activités localement
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() { setState(() {
@@ -137,28 +136,18 @@ class _ActivitiesPageState extends State<ActivitiesPage>
_isLoadingTripActivities = false; _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 // Vérifier si on a besoin de charger plus d'activités dans les suggestions
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
print(
'🚀 [ActivityLoaded] Déclenchement de la vérification auto-reload',
);
_checkAndLoadMoreActivitiesIfNeeded(); _checkAndLoadMoreActivitiesIfNeeded();
}); });
}); });
} }
if (state is ActivitySearchResults) { if (state is ActivitySearchResults) {
print('🔍 Résultats Google: ${state.searchResults.length}');
// Déclencher l'auto-reload uniquement pour la recherche initiale (6 résultats) // Déclencher l'auto-reload uniquement pour la recherche initiale (6 résultats)
// et pas pour les rechargements automatiques // et pas pour les rechargements automatiques
if (state.searchResults.length <= 6 && !_autoReloadInProgress) { if (state.searchResults.length <= 6 && !_autoReloadInProgress) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
print(
'🎯 [ActivitySearchResults] Première recherche avec peu de résultats, vérification auto-reload',
);
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
_checkAndLoadMoreActivitiesIfNeeded(); _checkAndLoadMoreActivitiesIfNeeded();
}); });
@@ -167,13 +156,11 @@ class _ActivitiesPageState extends State<ActivitiesPage>
} }
if (state is ActivityVoteRecorded) { if (state is ActivityVoteRecorded) {
print('<EFBFBD> Vote enregistré pour activité: ${state.activityId}');
// Recharger les activités du voyage pour mettre à jour les votes // Recharger les activités du voyage pour mettre à jour les votes
_loadActivities(); _loadActivities();
} }
if (state is ActivityAdded) { if (state is ActivityAdded) {
print('✅ Activité ajoutée avec succès: ${state.activity.name}');
// Recharger automatiquement les activités du voyage // Recharger automatiquement les activités du voyage
_loadActivities(); _loadActivities();
} }
@@ -654,10 +641,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
); );
}).toList(); }).toList();
print(
'🔍 [Google Search] ${googleActivities.length} résultats trouvés, ${filteredActivities.length} après filtrage',
);
if (filteredActivities.isEmpty && googleActivities.isNotEmpty) { if (filteredActivities.isEmpty && googleActivities.isNotEmpty) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -764,18 +747,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: () { onPressed: () {
print(
'🧪 [DEBUG] Force auto-reload check - État actuel:',
);
print(
'🧪 [DEBUG] _tripActivities: ${_tripActivities.length}',
);
print(
'🧪 [DEBUG] _autoReloadInProgress: $_autoReloadInProgress',
);
print(
'🧪 [DEBUG] _lastAutoReloadTriggerCount: $_lastAutoReloadTriggerCount',
);
_checkAndLoadMoreActivitiesIfNeeded(); _checkAndLoadMoreActivitiesIfNeeded();
}, },
icon: const Icon(Icons.bug_report, size: 16), icon: const Icon(Icons.bug_report, size: 16),
@@ -1251,8 +1222,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
} }
void _voteForActivity(String activityId, int vote) { void _voteForActivity(String activityId, int vote) {
print('🗳️ Vote pour activité $activityId: $vote');
// TODO: Récupérer l'ID utilisateur actuel // TODO: Récupérer l'ID utilisateur actuel
// Pour l'instant, on utilise un ID temporaire // Pour l'instant, on utilise un ID temporaire
final userId = 'current_user_id'; final userId = 'current_user_id';
@@ -1275,8 +1244,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
} }
void _addGoogleActivityToTrip(Activity activity) { void _addGoogleActivityToTrip(Activity activity) {
print(' [Add Activity] Adding ${activity.name} to trip');
// Créer une nouvelle activité avec l'ID du voyage // Créer une nouvelle activité avec l'ID du voyage
final newActivity = activity.copyWith( final newActivity = activity.copyWith(
tripId: widget.trip.id, tripId: widget.trip.id,
@@ -1307,7 +1274,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
void _checkAndLoadMoreActivitiesIfNeeded() { void _checkAndLoadMoreActivitiesIfNeeded() {
// Protection contre les rechargements en boucle // Protection contre les rechargements en boucle
if (_autoReloadInProgress) { if (_autoReloadInProgress) {
print('⏸️ [Auto-reload] Auto-reload déjà en cours, skip');
return; return;
} }
@@ -1315,13 +1281,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
if (currentState is ActivitySearchResults) { if (currentState is ActivitySearchResults) {
final googleActivities = currentState.searchResults; 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 // Filtrer les activités déjà présentes dans le voyage
final filteredActivities = googleActivities.where((googleActivity) { final filteredActivities = googleActivities.where((googleActivity) {
final isDuplicate = _tripActivities.any( final isDuplicate = _tripActivities.any(
@@ -1329,21 +1288,12 @@ class _ActivitiesPageState extends State<ActivitiesPage>
tripActivity.name.toLowerCase().trim() == tripActivity.name.toLowerCase().trim() ==
googleActivity.name.toLowerCase().trim(), googleActivity.name.toLowerCase().trim(),
); );
if (isDuplicate) { if (isDuplicate) {}
print('🔍 [Auto-reload] Activité filtrée: ${googleActivity.name}');
}
return !isDuplicate; return !isDuplicate;
}).toList(); }).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 // Protection: ne pas redéclencher pour le même nombre d'activités Google
if (googleActivities.length == _lastAutoReloadTriggerCount) { if (googleActivities.length == _lastAutoReloadTriggerCount) {
print(
'🔒 [Auto-reload] Même nombre qu\'avant (${googleActivities.length}), skip pour éviter la boucle',
);
return; return;
} }
@@ -1360,13 +1310,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
activitiesNeeded + activitiesNeeded +
6; // Activités actuelles + ce qui manque + buffer de 6 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 // Mettre à jour le compteur et recharger avec le nouveau total
_totalGoogleActivitiesRequested = newTotalToRequest; _totalGoogleActivitiesRequested = newTotalToRequest;
_loadMoreGoogleActivitiesWithTotal(newTotalToRequest); _loadMoreGoogleActivitiesWithTotal(newTotalToRequest);
@@ -1374,35 +1317,19 @@ class _ActivitiesPageState extends State<ActivitiesPage>
// Libérer le verrou après un délai // Libérer le verrou après un délai
Future.delayed(const Duration(seconds: 3), () { Future.delayed(const Duration(seconds: 3), () {
_autoReloadInProgress = false; _autoReloadInProgress = false;
print('🔓 [Auto-reload] Verrou libéré');
}); });
} else if (filteredActivities.length >= 4) { } else if (filteredActivities.length >= 4) {
print( } else {}
'✅ [Auto-reload] Suffisamment d\'activités visibles (${filteredActivities.length} >= 4)', } else {}
);
} 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}',
);
}
} }
void _searchGoogleActivities() { void _searchGoogleActivities() {
print('🔍 [Google Search] Initializing first search with 6 results');
_totalGoogleActivitiesRequested = 6; // Reset du compteur _totalGoogleActivitiesRequested = 6; // Reset du compteur
_autoReloadInProgress = false; // Reset des protections _autoReloadInProgress = false; // Reset des protections
_lastAutoReloadTriggerCount = 0; _lastAutoReloadTriggerCount = 0;
// Utiliser les coordonnées pré-géolocalisées du voyage si disponibles // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles
if (widget.trip.hasCoordinates) { if (widget.trip.hasCoordinates) {
print(
'🌍 [Google Search] Using pre-geocoded coordinates: ${widget.trip.latitude}, ${widget.trip.longitude}',
);
context.read<ActivityBloc>().add( context.read<ActivityBloc>().add(
SearchActivitiesWithCoordinates( SearchActivitiesWithCoordinates(
tripId: widget.trip.id!, tripId: widget.trip.id!,
@@ -1414,9 +1341,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
), ),
); );
} else { } else {
print(
'⚠️ [Google Search] No coordinates available, falling back to destination geocoding',
);
context.read<ActivityBloc>().add( context.read<ActivityBloc>().add(
SearchActivities( SearchActivities(
tripId: widget.trip.id!, tripId: widget.trip.id!,
@@ -1431,16 +1355,12 @@ class _ActivitiesPageState extends State<ActivitiesPage>
} }
void _resetAndSearchGoogleActivities() { void _resetAndSearchGoogleActivities() {
print('🔄 [Google Search] Resetting and starting fresh search');
_totalGoogleActivitiesRequested = 6; // Reset du compteur _totalGoogleActivitiesRequested = 6; // Reset du compteur
_autoReloadInProgress = false; // Reset des protections _autoReloadInProgress = false; // Reset des protections
_lastAutoReloadTriggerCount = 0; _lastAutoReloadTriggerCount = 0;
// Utiliser les coordonnées pré-géolocalisées du voyage si disponibles // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles
if (widget.trip.hasCoordinates) { if (widget.trip.hasCoordinates) {
print(
'🌍 [Google Search] Using pre-geocoded coordinates: ${widget.trip.latitude}, ${widget.trip.longitude}',
);
context.read<ActivityBloc>().add( context.read<ActivityBloc>().add(
SearchActivitiesWithCoordinates( SearchActivitiesWithCoordinates(
tripId: widget.trip.id!, tripId: widget.trip.id!,
@@ -1452,9 +1372,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
), ),
); );
} else { } else {
print(
'⚠️ [Google Search] No coordinates available, falling back to destination geocoding',
);
context.read<ActivityBloc>().add( context.read<ActivityBloc>().add(
SearchActivities( SearchActivities(
tripId: widget.trip.id!, tripId: widget.trip.id!,
@@ -1469,23 +1386,16 @@ class _ActivitiesPageState extends State<ActivitiesPage>
} }
void _loadMoreGoogleActivities() { void _loadMoreGoogleActivities() {
print('📄 [Google Search] Loading more activities (next 6 results)');
final currentState = context.read<ActivityBloc>().state; final currentState = context.read<ActivityBloc>().state;
if (currentState is ActivitySearchResults) { if (currentState is ActivitySearchResults) {
final currentCount = currentState.searchResults.length; final currentCount = currentState.searchResults.length;
final newTotal = currentCount + 6; final newTotal = currentCount + 6;
print(
'📊 [Google Search] Current results count: $currentCount, requesting total: $newTotal',
);
_totalGoogleActivitiesRequested = newTotal; _totalGoogleActivitiesRequested = newTotal;
// Utiliser les coordonnées pré-géolocalisées du voyage si disponibles // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles
if (widget.trip.hasCoordinates) { if (widget.trip.hasCoordinates) {
print(
'🌍 [Google Search] Using pre-geocoded coordinates for more results',
);
context.read<ActivityBloc>().add( context.read<ActivityBloc>().add(
SearchActivitiesWithCoordinates( SearchActivitiesWithCoordinates(
tripId: widget.trip.id!, tripId: widget.trip.id!,
@@ -1497,9 +1407,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
), ),
); );
} else { } else {
print(
'⚠️ [Google Search] No coordinates available, falling back to destination geocoding',
);
context.read<ActivityBloc>().add( context.read<ActivityBloc>().add(
SearchActivities( SearchActivities(
tripId: widget.trip.id!, tripId: widget.trip.id!,
@@ -1514,26 +1421,15 @@ class _ActivitiesPageState extends State<ActivitiesPage>
} }
void _loadMoreGoogleActivitiesWithTotal(int totalToRequest) { 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 // Au lieu de reset, on utilise l'offset et append pour forcer plus de résultats
final currentState = context.read<ActivityBloc>().state; final currentState = context.read<ActivityBloc>().state;
if (currentState is ActivitySearchResults) { if (currentState is ActivitySearchResults) {
final currentCount = currentState.searchResults.length; final currentCount = currentState.searchResults.length;
final additionalNeeded = totalToRequest - currentCount; final additionalNeeded = totalToRequest - currentCount;
print(
'📊 [Google Search] Current: $currentCount, Total demandé: $totalToRequest, Additional: $additionalNeeded',
);
if (additionalNeeded > 0) { if (additionalNeeded > 0) {
// Utiliser les coordonnées pré-géolocalisées du voyage si disponibles // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles
if (widget.trip.hasCoordinates) { if (widget.trip.hasCoordinates) {
print(
'🌍 [Google Search] Using pre-geocoded coordinates for additional results',
);
context.read<ActivityBloc>().add( context.read<ActivityBloc>().add(
SearchActivitiesWithCoordinates( SearchActivitiesWithCoordinates(
tripId: widget.trip.id!, tripId: widget.trip.id!,
@@ -1546,9 +1442,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
), ),
); );
} else { } else {
print(
'⚠️ [Google Search] No coordinates available, falling back to destination geocoding',
);
context.read<ActivityBloc>().add( context.read<ActivityBloc>().add(
SearchActivities( SearchActivities(
tripId: widget.trip.id!, tripId: widget.trip.id!,
@@ -1560,15 +1453,10 @@ class _ActivitiesPageState extends State<ActivitiesPage>
), ),
); );
} }
} else { } 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 // Si pas de résultats existants, faire une recherche complète
if (widget.trip.hasCoordinates) { if (widget.trip.hasCoordinates) {
print(
'🌍 [Google Search] Using pre-geocoded coordinates for fresh search',
);
context.read<ActivityBloc>().add( context.read<ActivityBloc>().add(
SearchActivitiesWithCoordinates( SearchActivitiesWithCoordinates(
tripId: widget.trip.id!, tripId: widget.trip.id!,
@@ -1580,9 +1468,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
), ),
); );
} else { } else {
print(
'⚠️ [Google Search] No coordinates available, falling back to destination geocoding',
);
context.read<ActivityBloc>().add( context.read<ActivityBloc>().add(
SearchActivities( SearchActivities(
tripId: widget.trip.id!, tripId: widget.trip.id!,

View File

@@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
class LoadingContent extends StatefulWidget {
final Future<void> 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<LoadingContent> createState() => _LoadingContentState();
}
class _LoadingContentState extends State<LoadingContent>
with TickerProviderStateMixin {
late AnimationController _rotationController;
late AnimationController _pulseController;
late Animation<double> _rotationAnimation;
late Animation<double> _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<double>(
begin: 0,
end: 1,
).animate(_rotationController);
_pulseAnimation = Tween<double>(begin: 0.8, end: 1.2).animate(
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
);
_executeBackgroundTask();
}
Future<void> _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<Color>(Colors.blue.shade400),
),
),
],
),
),
);
}
}

View File

@@ -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<SignUpPlatformContent> createState() => _SignUpPlatformContentState();
}
class _SignUpPlatformContentState extends State<SignUpPlatformContent> {
final _formKey = GlobalKey<FormState>();
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),
),
),
],
),
),
),
),
);
}
}

View File

@@ -1,8 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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_bloc.dart';
import '../blocs/auth/auth_event.dart'; import '../blocs/auth/auth_event.dart';
import '../blocs/auth/auth_state.dart'; import '../blocs/auth/auth_state.dart';
import '../services/error_service.dart';
class SignUpPage extends StatefulWidget { class SignUpPage extends StatefulWidget {
const SignUpPage({super.key}); const SignUpPage({super.key});
@@ -18,10 +22,13 @@ class _SignUpPageState extends State<SignUpPage> {
final _emailController = TextEditingController(); final _emailController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController(); final _confirmPasswordController = TextEditingController();
final _phoneNumberController = TextEditingController();
bool _obscurePassword = true; bool _obscurePassword = true;
bool _obscureConfirmPassword = true; bool _obscureConfirmPassword = true;
final _errorService = ErrorService();
@override @override
void dispose() { void dispose() {
_nomController.dispose(); _nomController.dispose();
@@ -81,6 +88,7 @@ class _SignUpPageState extends State<SignUpPage> {
password: _passwordController.text, password: _passwordController.text,
nom: _nomController.text.trim(), nom: _nomController.text.trim(),
prenom: _prenomController.text.trim(), prenom: _prenomController.text.trim(),
phoneNumber: _phoneNumberController.text.trim(),
), ),
); );
} }
@@ -107,11 +115,8 @@ class _SignUpPageState extends State<SignUpPage> {
); );
Navigator.pushReplacementNamed(context, '/home'); Navigator.pushReplacementNamed(context, '/home');
} else if (state is AuthError) { } else if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar( _errorService.showError(
SnackBar( message: 'Erreur lors de la création du compte',
content: Text(state.message),
backgroundColor: Colors.red,
),
); );
} }
}, },
@@ -129,7 +134,10 @@ class _SignUpPageState extends State<SignUpPage> {
const SizedBox(height: 40), const SizedBox(height: 40),
const Text( const Text(
'Créer un compte', 'Créer un compte',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
const Text( const Text(
@@ -166,6 +174,21 @@ class _SignUpPageState extends State<SignUpPage> {
), ),
const SizedBox(height: 20), 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 // Champ Email
TextFormField( TextFormField(
controller: _emailController, controller: _emailController,
@@ -227,7 +250,8 @@ class _SignUpPageState extends State<SignUpPage> {
), ),
onPressed: () { onPressed: () {
setState(() { setState(() {
_obscureConfirmPassword = !_obscureConfirmPassword; _obscureConfirmPassword =
!_obscureConfirmPassword;
}); });
}, },
), ),
@@ -247,8 +271,13 @@ class _SignUpPageState extends State<SignUpPage> {
), ),
), ),
child: isLoading child: isLoading
? const CircularProgressIndicator(color: Colors.white) ? const CircularProgressIndicator(
: const Text('S\'inscrire', style: TextStyle(fontSize: 18)), color: Colors.white,
)
: const Text(
'S\'inscrire',
style: TextStyle(fontSize: 18),
),
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -270,6 +299,86 @@ class _SignUpPageState extends State<SignUpPage> {
), ),
], ],
), ),
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'),
),
);
},
),
),
);
},
),
], ],
), ),
), ),

View File

@@ -73,6 +73,7 @@ class AuthRepository {
/// [password] - User's password /// [password] - User's password
/// [nom] - User's last name /// [nom] - User's last name
/// [prenom] - User's first name /// [prenom] - User's first name
/// [phoneNumber] - User's phone number
/// ///
/// Returns the created [User] model if successful. /// Returns the created [User] model if successful.
/// Throws an exception if account creation fails. /// Throws an exception if account creation fails.
@@ -81,6 +82,7 @@ class AuthRepository {
required String password, required String password,
required String nom, required String nom,
required String prenom, required String prenom,
required String phoneNumber,
}) async { }) async {
try { try {
final firebaseUser = await _authService.signUpWithEmailAndPassword( final firebaseUser = await _authService.signUpWithEmailAndPassword(
@@ -94,7 +96,7 @@ class AuthRepository {
email: email, email: email,
nom: nom, nom: nom,
prenom: prenom, prenom: prenom,
phoneNumber: 'Uknown', phoneNumber: phoneNumber,
platform: 'email', platform: 'email',
profilePictureUrl: '', profilePictureUrl: '',
); );
@@ -114,7 +116,7 @@ class AuthRepository {
/// ///
/// Returns the [User] model if successful, null if Google sign-in was cancelled. /// Returns the [User] model if successful, null if Google sign-in was cancelled.
/// Throws an exception if authentication fails. /// Throws an exception if authentication fails.
Future<User?> signInWithGoogle() async { Future<User?> signUpWithGoogle(String phoneNumber) async {
try { try {
final firebaseUser = await _authService.signInWithGoogle(); final firebaseUser = await _authService.signInWithGoogle();
@@ -132,8 +134,8 @@ class AuthRepository {
email: firebaseUser.user!.email ?? '', email: firebaseUser.user!.email ?? '',
nom: '', nom: '',
prenom: firebaseUser.user!.displayName ?? 'User', prenom: firebaseUser.user!.displayName ?? 'User',
phoneNumber: firebaseUser.user!.phoneNumber ?? 'Uknown', phoneNumber: phoneNumber,
profilePictureUrl: firebaseUser.user!.photoURL, profilePictureUrl: firebaseUser.user!.photoURL ?? 'Unknown',
platform: 'google', platform: 'google',
); );
@@ -147,6 +149,21 @@ class AuthRepository {
return null; return null;
} }
Future<User?> 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. /// Signs in a user using Apple authentication.
/// ///
/// Handles Apple sign-in flow and creates/retrieves user data from Firestore. /// 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. /// Returns the [User] model if successful, null if Apple sign-in was cancelled.
/// Throws an exception if authentication fails. /// Throws an exception if authentication fails.
Future<User?> signInWithApple() async { Future<User?> signUpWithApple(String phoneNumber) async {
try { try {
final firebaseUser = await _authService.signInWithApple(); final firebaseUser = await _authService.signInWithApple();
@@ -170,8 +187,8 @@ class AuthRepository {
email: firebaseUser.user!.email ?? '', email: firebaseUser.user!.email ?? '',
nom: '', nom: '',
prenom: firebaseUser.user!.displayName ?? 'User', prenom: firebaseUser.user!.displayName ?? 'User',
phoneNumber: firebaseUser.user!.phoneNumber ?? 'Uknown', phoneNumber: phoneNumber,
profilePictureUrl: firebaseUser.user!.photoURL, profilePictureUrl: firebaseUser.user!.photoURL ?? 'Unknown',
platform: 'apple', platform: 'apple',
); );
@@ -185,6 +202,21 @@ class AuthRepository {
return null; return null;
} }
Future<User?> 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. /// Signs out the current user.
/// ///
/// Clears the authentication state and signs out from Firebase Auth. /// Clears the authentication state and signs out from Firebase Auth.