feat: refactor account deletion to handle requires-recent-login and update Android package ID.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.travel_mate
|
||||
package be.devdayronvl.travel_mate
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user