feat: refactor account deletion to handle requires-recent-login and update Android package ID.

This commit is contained in:
Van Leemput Dayron
2025-11-28 19:01:01 +01:00
parent b1f86b1c6a
commit 0668fcad57
7 changed files with 336 additions and 124 deletions

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/components/widgets/user_state_widget.dart';
import 'package:travel_mate/services/error_service.dart';
@@ -789,7 +790,7 @@ class ProfileContent extends StatelessWidget {
BuildContext context,
user_state.UserModel user,
) {
final passwordController = TextEditingController();
final confirmationController = TextEditingController();
final authService = AuthService();
showDialog(
@@ -801,15 +802,15 @@ class ProfileContent extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est irréversible.',
'Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est irréversible.\n\nPour confirmer, veuillez écrire "CONFIRMER" ci-dessous.',
),
SizedBox(height: 16),
TextField(
controller: passwordController,
obscureText: true,
controller: confirmationController,
decoration: InputDecoration(
labelText: 'Confirmez votre mot de passe',
labelText: 'Écrivez CONFIRMER',
border: OutlineInputBorder(),
hintText: 'CONFIRMER',
),
),
],
@@ -821,11 +822,18 @@ class ProfileContent extends StatelessWidget {
),
TextButton(
onPressed: () async {
try {
await authService.deleteAccount(
password: passwordController.text,
email: user.email,
if (confirmationController.text != 'CONFIRMER') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Veuillez écrire CONFIRMER pour valider'),
backgroundColor: Colors.red,
),
);
return;
}
try {
await authService.deleteAccount();
if (context.mounted) {
Navigator.of(dialogContext).pop();
@@ -836,10 +844,33 @@ class ProfileContent extends StatelessWidget {
(route) => false,
);
}
} on FirebaseAuthException catch (e) {
if (e.code == 'requires-recent-login') {
if (context.mounted) {
Navigator.of(dialogContext).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Par sécurité, veuillez vous reconnecter avant de supprimer votre compte',
),
backgroundColor: Colors.orange,
duration: Duration(seconds: 4),
),
);
}
} else {
if (context.mounted) {
_errorService.showError(
message: 'Erreur lors de la suppression: ${e.message}',
);
}
}
} catch (e) {
_errorService.showError(
message: 'Erreur: Mot de passe incorrect',
);
if (context.mounted) {
_errorService.showError(
message: 'Erreur inattendue: ${e.toString()}',
);
}
}
},
style: TextButton.styleFrom(foregroundColor: Colors.red),

View File

@@ -1,8 +1,45 @@
import 'package:flutter/material.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 ForgotPasswordPage extends StatelessWidget {
class ForgotPasswordPage extends StatefulWidget {
const ForgotPasswordPage({super.key});
@override
State<ForgotPasswordPage> createState() => _ForgotPasswordPageState();
}
class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
final _emailController = TextEditingController();
final _formKey = GlobalKey<FormState>();
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
String? _validateEmail(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Email requis';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value.trim())) {
return 'Email invalide';
}
return null;
}
void _submit() {
if (_formKey.currentState!.validate()) {
context.read<AuthBloc>().add(
AuthPasswordResetRequested(email: _emailController.text.trim()),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -16,87 +53,159 @@ class ForgotPasswordPage extends StatelessWidget {
backgroundColor: Colors.transparent,
elevation: 0,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Column(
children: [
const Text(
"Travel Mate",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 24),
const Text(
"Vous avez oublié votre mot de passe ? \n Ne vous inquiétez pas vous pouvez le réinitaliser !",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
const SizedBox(height: 40),
const Text(
"Quel est votre email ? \n Si celui-ci existe dans note base de donées, nous vous enverrons un mail avec un mot de passe unique.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
const SizedBox(height: 24),
const TextField(
decoration: InputDecoration(
labelText: 'example@travelmate.com',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: () {
// Logique de connexion
},
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text('Envoyer', style: TextStyle(fontSize: 18)),
),
const SizedBox(height: 20),
Container(
width: double.infinity,
height: 1,
color: Colors.grey.shade300,
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Pas encore inscrit ? "),
GestureDetector(
onTap: () {
// Go to sign up page
Navigator.pushNamed(context, '/signup');
},
child: const Text(
'Inscrivez-vous !',
body: BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthPasswordResetSent) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Email de réinitialisation envoyé !'),
backgroundColor: Colors.green,
),
);
Navigator.pop(context);
} else if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
}
},
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
children: [
const Text(
"Travel Mate",
style: TextStyle(
color: Color.fromARGB(255, 37, 109, 167),
decoration: TextDecoration.underline,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
],
const SizedBox(height: 24),
const Text(
"Vous avez oublié votre mot de passe ? \n Ne vous inquiétez pas vous pouvez le réinitaliser !",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 40),
const Text(
"Quel est votre email ? \n Si celui-ci existe dans note base de donées, nous vous enverrons un mail avec un mot de passe unique.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
TextFormField(
controller: _emailController,
validator: _validateEmail,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'example@travelmate.com',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
prefixIcon: Icon(Icons.email_outlined),
),
),
const SizedBox(height: 40),
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
final isLoading = state is AuthLoading;
return ElevatedButton(
onPressed: isLoading ? null : _submit,
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
backgroundColor:
Theme.of(context).brightness ==
Brightness.dark
? Colors.white
: Colors.black,
foregroundColor:
Theme.of(context).brightness ==
Brightness.dark
? Colors.black
: Colors.white,
),
child: isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color:
Theme.of(context).brightness ==
Brightness.dark
? Colors.black
: Colors.white,
),
)
: const Text(
'Envoyer',
style: TextStyle(fontSize: 18),
),
);
},
),
const SizedBox(height: 20),
Container(
width: double.infinity,
height: 1,
color: Colors.grey.shade300,
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Pas encore inscrit ? "),
GestureDetector(
onTap: () {
// Go to sign up page
Navigator.pushReplacementNamed(
context,
'/signup',
);
},
child: const Text(
'Inscrivez-vous !',
style: TextStyle(
color: Color.fromARGB(255, 37, 109, 167),
decoration: TextDecoration.underline,
),
),
),
],
),
],
),
),
],
),
),
),
),

View File

@@ -122,11 +122,16 @@ class AuthRepository {
String firstname,
) async {
try {
final firebaseUser = await _authService.signInWithGoogle();
firebase_auth.User? firebaseUser = _authService.currentUser;
if (firebaseUser.user != null) {
if (firebaseUser == null) {
final userCredential = await _authService.signInWithGoogle();
firebaseUser = userCredential.user;
}
if (firebaseUser != null) {
// Check if user already exists in Firestore
final existingUser = await getUserFromFirestore(firebaseUser.user!.uid);
final existingUser = await getUserFromFirestore(firebaseUser.uid);
if (existingUser != null) {
return existingUser;
@@ -134,12 +139,12 @@ class AuthRepository {
// Create new user document for first-time Google sign-in
final user = User(
id: firebaseUser.user!.uid,
email: firebaseUser.user!.email ?? '',
id: firebaseUser.uid,
email: firebaseUser.email ?? '',
nom: name,
prenom: firstname,
phoneNumber: phoneNumber,
profilePictureUrl: firebaseUser.user!.photoURL ?? 'Unknown',
profilePictureUrl: firebaseUser.photoURL ?? 'Unknown',
platform: 'google',
);
@@ -181,22 +186,27 @@ class AuthRepository {
String firstname,
) async {
try {
final firebaseUser = await _authService.signInWithApple();
firebase_auth.User? firebaseUser = _authService.currentUser;
if (firebaseUser.user != null) {
final existingUser = await getUserFromFirestore(firebaseUser.user!.uid);
if (firebaseUser == null) {
final userCredential = await _authService.signInWithApple();
firebaseUser = userCredential.user;
}
if (firebaseUser != null) {
final existingUser = await getUserFromFirestore(firebaseUser.uid);
if (existingUser != null) {
return existingUser;
}
final user = User(
id: firebaseUser.user!.uid,
email: firebaseUser.user!.email ?? '',
id: firebaseUser.uid,
email: firebaseUser.email ?? '',
nom: name,
prenom: firstname,
phoneNumber: phoneNumber,
profilePictureUrl: firebaseUser.user!.photoURL ?? 'Unknown',
profilePictureUrl: firebaseUser.photoURL ?? 'Unknown',
platform: 'apple',
);

View File

@@ -83,20 +83,30 @@ class AuthService {
///
/// [password] - The user's current password for re-authentication
/// [email] - The user's email address for re-authentication
Future<void> deleteAccount({
required String password,
required String email,
}) async {
// Re-authenticate the user for security
AuthCredential credential = EmailAuthProvider.credential(
email: email,
password: password,
);
await currentUser!.reauthenticateWithCredential(credential);
// Delete the user account permanently
await currentUser!.delete();
await firebaseAuth.signOut();
Future<void> deleteAccount() async {
try {
await currentUser!.delete();
await firebaseAuth.signOut();
} on FirebaseAuthException catch (e) {
if (e.code == 'requires-recent-login') {
_errorService.logError(
'Delete account requires recent login',
StackTrace.current,
);
rethrow;
}
_errorService.logError(
'Error deleting account: ${e.code} - ${e.message}',
StackTrace.current,
);
rethrow;
} catch (e) {
_errorService.logError(
'Unknown error deleting account: $e',
StackTrace.current,
);
rethrow;
}
}
/// Resets the user's password after re-authentication.