From 0668fcad57d7679a0fdfb40e2755ae99e46e7b8d Mon Sep 17 00:00:00 2001 From: Van Leemput Dayron Date: Fri, 28 Nov 2025 19:01:01 +0100 Subject: [PATCH] feat: refactor account deletion to handle `requires-recent-login` and update Android package ID. --- android/app/build.gradle.kts | 4 +- android/app/google-services.json | 62 +++- .../davdayronvl}/travel_mate/MainActivity.kt | 2 +- .../settings/profile/profile_content.dart | 55 +++- lib/pages/resetpswd.dart | 265 ++++++++++++------ lib/repositories/auth_repository.dart | 34 ++- lib/services/auth_service.dart | 38 ++- 7 files changed, 336 insertions(+), 124 deletions(-) rename android/app/src/main/kotlin/{com/example => be/davdayronvl}/travel_mate/MainActivity.kt (72%) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 63450ba..29d9dbc 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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 diff --git a/android/app/google-services.json b/android/app/google-services.json index e4c4270..3f35d05 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -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" } } ] diff --git a/android/app/src/main/kotlin/com/example/travel_mate/MainActivity.kt b/android/app/src/main/kotlin/be/davdayronvl/travel_mate/MainActivity.kt similarity index 72% rename from android/app/src/main/kotlin/com/example/travel_mate/MainActivity.kt rename to android/app/src/main/kotlin/be/davdayronvl/travel_mate/MainActivity.kt index 5dea0e5..62bff85 100644 --- a/android/app/src/main/kotlin/com/example/travel_mate/MainActivity.kt +++ b/android/app/src/main/kotlin/be/davdayronvl/travel_mate/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.travel_mate +package be.devdayronvl.travel_mate import io.flutter.embedding.android.FlutterActivity diff --git a/lib/components/settings/profile/profile_content.dart b/lib/components/settings/profile/profile_content.dart index bb7ab21..94e923c 100644 --- a/lib/components/settings/profile/profile_content.dart +++ b/lib/components/settings/profile/profile_content.dart @@ -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), diff --git a/lib/pages/resetpswd.dart b/lib/pages/resetpswd.dart index 63dc3ce..cb52076 100644 --- a/lib/pages/resetpswd.dart +++ b/lib/pages/resetpswd.dart @@ -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 createState() => _ForgotPasswordPageState(); +} + +class _ForgotPasswordPageState extends State { + final _emailController = TextEditingController(); + final _formKey = GlobalKey(); + + @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().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( + 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( + 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, + ), + ), + ), + ], + ), + ], + ), ), - ], + ), ), ), ), diff --git a/lib/repositories/auth_repository.dart b/lib/repositories/auth_repository.dart index 4acf693..dcadd26 100644 --- a/lib/repositories/auth_repository.dart +++ b/lib/repositories/auth_repository.dart @@ -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', ); diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index dbff737..a52c86f 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -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 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 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.