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

View File

@@ -7,17 +7,69 @@
"client": [ "client": [
{ {
"client_info": { "client_info": {
"mobilesdk_app_id": "1:521527250907:android:be3db7fc84f053ec7da1fe", "mobilesdk_app_id": "1:521527250907:android:56c632e98c7826347da1fe",
"android_client_info": { "android_client_info": {
"package_name": "be.davdayronvl.travel_mate" "package_name": "be.devdayronvl.travel_mate"
} }
}, },
"oauth_client": [ "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_id": "521527250907-lqgj1lmfcsjusm2be9r6kpuanq3jvjcd.apps.googleusercontent.com",
"client_type": 1, "client_type": 1,
"android_info": { "android_info": {
"package_name": "be.davdayronvl.travel_mate", "package_name": "com.example.travel_mate",
"certificate_hash": "9b7e3f14f0fcae0034ed977b5d40305b1812308d" "certificate_hash": "9b7e3f14f0fcae0034ed977b5d40305b1812308d"
} }
}, },
@@ -39,10 +91,10 @@
"client_type": 3 "client_type": 3
}, },
{ {
"client_id": "521527250907-3i1qe2656eojs8k9hjdi573j09i9p41m.apps.googleusercontent.com", "client_id": "521527250907-196i04qgm4talrosgi0ne0q8en90hkkh.apps.googleusercontent.com",
"client_type": 2, "client_type": 2,
"ios_info": { "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 import io.flutter.embedding.android.FlutterActivity

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/components/widgets/user_state_widget.dart'; import 'package:travel_mate/components/widgets/user_state_widget.dart';
import 'package:travel_mate/services/error_service.dart'; import 'package:travel_mate/services/error_service.dart';
@@ -789,7 +790,7 @@ class ProfileContent extends StatelessWidget {
BuildContext context, BuildContext context,
user_state.UserModel user, user_state.UserModel user,
) { ) {
final passwordController = TextEditingController(); final confirmationController = TextEditingController();
final authService = AuthService(); final authService = AuthService();
showDialog( showDialog(
@@ -801,15 +802,15 @@ class ProfileContent extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( 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), SizedBox(height: 16),
TextField( TextField(
controller: passwordController, controller: confirmationController,
obscureText: true,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Confirmez votre mot de passe', labelText: 'Écrivez CONFIRMER',
border: OutlineInputBorder(), border: OutlineInputBorder(),
hintText: 'CONFIRMER',
), ),
), ),
], ],
@@ -821,11 +822,18 @@ class ProfileContent extends StatelessWidget {
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
try { if (confirmationController.text != 'CONFIRMER') {
await authService.deleteAccount( ScaffoldMessenger.of(context).showSnackBar(
password: passwordController.text, SnackBar(
email: user.email, content: Text('Veuillez écrire CONFIRMER pour valider'),
backgroundColor: Colors.red,
),
); );
return;
}
try {
await authService.deleteAccount();
if (context.mounted) { if (context.mounted) {
Navigator.of(dialogContext).pop(); Navigator.of(dialogContext).pop();
@@ -836,11 +844,34 @@ class ProfileContent extends StatelessWidget {
(route) => false, (route) => false,
); );
} }
} catch (e) { } on FirebaseAuthException catch (e) {
_errorService.showError( if (e.code == 'requires-recent-login') {
message: 'Erreur: Mot de passe incorrect', 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), style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text('Supprimer'), child: Text('Supprimer'),

View File

@@ -1,8 +1,45 @@
import 'package:flutter/material.dart'; 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}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -16,15 +53,40 @@ class ForgotPasswordPage extends StatelessWidget {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
elevation: 0, 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( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Center( child: Center(
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column( child: Column(
children: [ children: [
const Text( const Text(
"Travel Mate", "Travel Mate",
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -32,7 +94,10 @@ class ForgotPasswordPage extends StatelessWidget {
const Text( const Text(
"Vous avez oublié votre mot de passe ? \n Ne vous inquiétez pas vous pouvez le réinitaliser !", "Vous avez oublié votre mot de passe ? \n Ne vous inquiétez pas vous pouvez le réinitaliser !",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
), ),
const SizedBox(height: 40), const SizedBox(height: 40),
@@ -40,33 +105,71 @@ class ForgotPasswordPage extends StatelessWidget {
const Text( 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.", "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, textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
const TextField( TextFormField(
decoration: InputDecoration( controller: _emailController,
labelText: 'example@travelmate.com', validator: _validateEmail,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'example@travelmate.com',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
), ),
prefixIcon: Icon(Icons.email_outlined),
), ),
), ),
const SizedBox(height: 40), const SizedBox(height: 40),
ElevatedButton( BlocBuilder<AuthBloc, AuthState>(
onPressed: () { builder: (context, state) {
// Logique de connexion final isLoading = state is AuthLoading;
},
return ElevatedButton(
onPressed: isLoading ? null : _submit,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(50), minimumSize: const Size.fromHeight(50),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), 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), const SizedBox(height: 20),
@@ -84,7 +187,10 @@ class ForgotPasswordPage extends StatelessWidget {
GestureDetector( GestureDetector(
onTap: () { onTap: () {
// Go to sign up page // Go to sign up page
Navigator.pushNamed(context, '/signup'); Navigator.pushReplacementNamed(
context,
'/signup',
);
}, },
child: const Text( child: const Text(
'Inscrivez-vous !', 'Inscrivez-vous !',
@@ -101,6 +207,9 @@ class ForgotPasswordPage extends StatelessWidget {
), ),
), ),
), ),
),
),
),
); );
} }
} }

View File

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

View File

@@ -83,20 +83,30 @@ class AuthService {
/// ///
/// [password] - The user's current password for re-authentication /// [password] - The user's current password for re-authentication
/// [email] - The user's email address for re-authentication /// [email] - The user's email address for re-authentication
Future<void> deleteAccount({ Future<void> deleteAccount() async {
required String password, try {
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 currentUser!.delete();
await firebaseAuth.signOut(); 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. /// Resets the user's password after re-authentication.