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

@@ -15,7 +15,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
namespace = "be.davdayronvl.travel_mate"
namespace = "be.devdayronvl.travel_mate"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -30,7 +30,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "be.davdayronvl.travel_mate"
applicationId = "be.devdayronvl.travel_mate"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion

View File

@@ -7,17 +7,69 @@
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:521527250907:android:be3db7fc84f053ec7da1fe",
"mobilesdk_app_id": "1:521527250907:android:56c632e98c7826347da1fe",
"android_client_info": {
"package_name": "be.davdayronvl.travel_mate"
"package_name": "be.devdayronvl.travel_mate"
}
},
"oauth_client": [
{
"client_id": "521527250907-j0kt1hc8hc7qc2kedp4akehau754cn5d.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAON_ol0Jr34tKbETvdDK9JCQdKNawxBeQ"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "521527250907-j0kt1hc8hc7qc2kedp4akehau754cn5d.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "521527250907-196i04qgm4talrosgi0ne0q8en90hkkh.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "be.devdayronvl.TravelMate"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:521527250907:android:be3db7fc84f053ec7da1fe",
"android_client_info": {
"package_name": "com.example.travel_mate"
}
},
"oauth_client": [
{
"client_id": "521527250907-19lrclc10eb0p8li1qutepctfqdohn0b.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.example.travel_mate",
"certificate_hash": "2374761dc92a30812608c072638510002041eca8"
}
},
{
"client_id": "521527250907-5v8l011nod30a6c52nkmk69d00h0ma0q.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.example.travel_mate",
"certificate_hash": "c98141ab89d42b16c273e611054e7c87aa773d83"
}
},
{
"client_id": "521527250907-lqgj1lmfcsjusm2be9r6kpuanq3jvjcd.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "be.davdayronvl.travel_mate",
"package_name": "com.example.travel_mate",
"certificate_hash": "9b7e3f14f0fcae0034ed977b5d40305b1812308d"
}
},
@@ -39,10 +91,10 @@
"client_type": 3
},
{
"client_id": "521527250907-3i1qe2656eojs8k9hjdi573j09i9p41m.apps.googleusercontent.com",
"client_id": "521527250907-196i04qgm4talrosgi0ne0q8en90hkkh.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.example.travelMate"
"bundle_id": "be.devdayronvl.TravelMate"
}
}
]

View File

@@ -1,4 +1,4 @@
package com.example.travel_mate
package be.devdayronvl.travel_mate
import io.flutter.embedding.android.FlutterActivity

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,11 +844,34 @@ class ProfileContent extends StatelessWidget {
(route) => false,
);
}
} catch (e) {
_errorService.showError(
message: 'Erreur: Mot de passe incorrect',
} 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) {
if (context.mounted) {
_errorService.showError(
message: 'Erreur inattendue: ${e.toString()}',
);
}
}
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text('Supprimer'),

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,15 +53,40 @@ class ForgotPasswordPage extends StatelessWidget {
backgroundColor: Colors.transparent,
elevation: 0,
),
body: SafeArea(
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(fontSize: 24, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
@@ -32,7 +94,10 @@ class ForgotPasswordPage extends StatelessWidget {
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),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 40),
@@ -40,33 +105,71 @@ class ForgotPasswordPage extends StatelessWidget {
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),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
const TextField(
decoration: InputDecoration(
labelText: 'example@travelmate.com',
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),
ElevatedButton(
onPressed: () {
// Logique de connexion
},
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: const Text('Envoyer', style: TextStyle(fontSize: 18)),
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),
@@ -84,7 +187,10 @@ class ForgotPasswordPage extends StatelessWidget {
GestureDetector(
onTap: () {
// Go to sign up page
Navigator.pushNamed(context, '/signup');
Navigator.pushReplacementNamed(
context,
'/signup',
);
},
child: const Text(
'Inscrivez-vous !',
@@ -101,6 +207,9 @@ class ForgotPasswordPage extends StatelessWidget {
),
),
),
),
),
),
);
}
}

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