From 745f2597d938dfd41014962af544f59e69ae5740 Mon Sep 17 00:00:00 2001 From: Van Leemput Dayron Date: Mon, 3 Nov 2025 01:06:19 +0100 Subject: [PATCH] Add Apple Login => Works only on iPhone for now, will be adding rest later. --- ios/Podfile.lock | 6 ++ ios/Runner.xcodeproj/project.pbxproj | 8 --- ios/Runner/Info.plist | 11 ++++ lib/repositories/auth_repository.dart | 75 ++++++++++++------------ lib/services/auth_service.dart | 82 ++++++++++++++++++++++++++- pubspec.lock | 24 ++++++++ pubspec.yaml | 1 + 7 files changed, 160 insertions(+), 47 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5e428d8..beef90f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1444,6 +1444,8 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - sign_in_with_apple (0.0.1): + - Flutter - url_launcher_ios (0.0.1): - Flutter @@ -1461,6 +1463,7 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -1520,6 +1523,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sign_in_with_apple: + :path: ".symlinks/plugins/sign_in_with_apple/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" @@ -1564,6 +1569,7 @@ SPEC CHECKSUMS: PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b PODFILE CHECKSUM: 53a6aebc29ccee84c41f92f409fc20cd4ca011f1 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 903ef39..9267a2a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -340,14 +340,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -376,14 +372,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 1b1e3c5..630903a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -62,5 +62,16 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + CFBundleURLTypes + + + CFBundleURLName + Apple Sign-In + CFBundleURLSchemes + + $(PRODUCT_BUNDLE_IDENTIFIER) + + + diff --git a/lib/repositories/auth_repository.dart b/lib/repositories/auth_repository.dart index 83a34c1..2c334c5 100644 --- a/lib/repositories/auth_repository.dart +++ b/lib/repositories/auth_repository.dart @@ -2,9 +2,10 @@ import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:cloud_firestore/cloud_firestore.dart'; import '../models/user.dart'; import '../services/auth_service.dart'; +import '../services/error_service.dart'; /// Repository for authentication operations and user data management. -/// +/// /// This repository handles authentication logic and user data persistence /// by coordinating between Firebase Auth and Firestore. It provides a /// clean interface for authentication operations while managing the @@ -12,38 +13,39 @@ import '../services/auth_service.dart'; class AuthRepository { /// Authentication service for Firebase Auth operations. final AuthService _authService; - + /// Firestore instance for user data operations. final FirebaseFirestore _firestore; + /// Error service for logging authentication errors. + final _errorService = ErrorService(); + /// Creates a new [AuthRepository] with optional service dependencies. - /// + /// /// If [authService] or [firestore] are not provided, default instances will be used. - AuthRepository({ - AuthService? authService, - FirebaseFirestore? firestore, - }) : _authService = authService ?? AuthService(), - _firestore = firestore ?? FirebaseFirestore.instance; + AuthRepository({AuthService? authService, FirebaseFirestore? firestore}) + : _authService = authService ?? AuthService(), + _firestore = firestore ?? FirebaseFirestore.instance; /// Stream of authentication state changes. - /// + /// /// Emits Firebase user objects when authentication state changes. Stream get authStateChanges => _authService.authStateChanges; /// Gets the currently authenticated Firebase user. - /// + /// /// Returns null if no user is currently authenticated. firebase_auth.User? get currentUser => _authService.currentUser; /// Signs in a user with email and password. - /// + /// /// Authenticates with Firebase Auth and retrieves the corresponding /// user data from Firestore. - /// + /// /// [email] - User's email address /// [password] - User's password - /// + /// /// Returns the [User] model if successful, null if user data not found. /// Throws an exception if authentication fails. Future signInWithEmailAndPassword({ @@ -57,20 +59,20 @@ class AuthRepository { ); return await getUserFromFirestore(firebaseUser.user!.uid); } catch (e) { - throw Exception('Sign-in error: $e'); + _errorService.showError(message: 'Utilisateur ou mot de passe incorrect'); } } /// Creates a new user account with email and password. - /// + /// /// Creates a Firebase Auth account and stores additional user information /// in Firestore. - /// + /// /// [email] - User's email address /// [password] - User's password /// [nom] - User's last name /// [prenom] - User's first name - /// + /// /// Returns the created [User] model if successful. /// Throws an exception if account creation fails. Future signUpWithEmailAndPassword({ @@ -95,17 +97,16 @@ class AuthRepository { await _firestore.collection('users').doc(user.id).set(user.toMap()); return user; - } catch (e) { - throw Exception('Registration error: $e'); + _errorService.showError(message: 'Erreur lors de la création du compte'); } } /// Signs in a user using Google authentication. - /// + /// /// Handles Google sign-in flow and creates/retrieves user data from Firestore. /// If the user doesn't exist, creates a new user document. - /// + /// /// Returns the [User] model if successful, null if Google sign-in was cancelled. /// Throws an exception if authentication fails. Future signInWithGoogle() async { @@ -115,7 +116,7 @@ class AuthRepository { if (firebaseUser.user != null) { // Check if user already exists in Firestore final existingUser = await getUserFromFirestore(firebaseUser.user!.uid); - + if (existingUser != null) { return existingUser; } @@ -133,24 +134,25 @@ class AuthRepository { } return null; } catch (e) { - throw Exception('Google sign-in error: $e'); + _errorService.showError(message: 'Erreur lors de la connexion Google'); } + return null; } /// Signs in a user using Apple authentication. - /// + /// /// Handles Apple sign-in flow and creates/retrieves user data from Firestore. /// If the user doesn't exist, creates a new user document. - /// + /// /// Returns the [User] model if successful, null if Apple sign-in was cancelled. /// Throws an exception if authentication fails. Future signInWithApple() async { try { final firebaseUser = await _authService.signInWithApple(); - if (firebaseUser?.user != null) { - final existingUser = await getUserFromFirestore(firebaseUser!.user!.uid); - + if (firebaseUser.user != null) { + final existingUser = await getUserFromFirestore(firebaseUser.user!.uid); + if (existingUser != null) { return existingUser; } @@ -167,33 +169,34 @@ class AuthRepository { } return null; } catch (e) { - throw Exception('Apple sign-in error: $e'); + _errorService.showError(message: 'Erreur lors de la connexion Apple'); } + return null; } /// Signs out the current user. - /// + /// /// Clears the authentication state and signs out from Firebase Auth. Future signOut() async { await _authService.signOut(); } /// Sends a password reset email to the specified email address. - /// + /// /// [email] - The email address to send the reset link to - /// + /// /// Throws an exception if the operation fails. Future resetPassword(String email) async { await _authService.resetPassword(email); } /// Retrieves user data from Firestore by user ID. - /// + /// /// Fetches the user document from the 'users' collection and converts /// it to a [User] model. - /// + /// /// [uid] - The Firebase user ID to look up - /// + /// /// Returns the [User] model if found, null otherwise. Future getUserFromFirestore(String uid) async { try { @@ -207,4 +210,4 @@ class AuthRepository { return null; } } -} \ No newline at end of file +} diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 2110fe6..dbff737 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,6 +1,7 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:travel_mate/services/error_service.dart'; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; /// Service for handling Firebase authentication operations. /// @@ -178,8 +179,83 @@ class AuthService { /// Signs in a user using Apple authentication. /// - /// This method is currently a placeholder for future Apple authentication support. - Future signInWithApple() async { - // TODO: Implement Apple sign-in + /// Handles the complete Apple Sign-In flow including platform initialization + /// and credential exchange with Firebase. Supports both iOS and Android platforms. + /// + /// Returns a [UserCredential] containing the authenticated user's information. + /// Throws various exceptions if authentication fails or platform is not supported. + Future signInWithApple() async { + try { + await ensureInitialized(); + + // Check if Apple Sign-In is available on this platform + final bool isAvailable = await SignInWithApple.isAvailable(); + if (!isAvailable) { + throw FirebaseAuthException( + code: 'ERROR_APPLE_SIGNIN_NOT_AVAILABLE', + message: 'Apple Sign-In is not available on this platform', + ); + } + + // Request Apple ID credential with platform-specific configuration + final AuthorizationCredentialAppleID credential = + await SignInWithApple.getAppleIDCredential( + scopes: [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName, + ], + // Configuration for Android/Web + webAuthenticationOptions: WebAuthenticationOptions( + clientId: 'be.devdayronvl.TravelMate', + redirectUri: Uri.parse( + 'https://your-project-id.firebaseapp.com/__/auth/handler', + ), + ), + ); + + // Create OAuth credential for Firebase + final OAuthCredential oauthCredential = OAuthProvider("apple.com") + .credential( + idToken: credential.identityToken, + accessToken: credential.authorizationCode, + ); + + // Sign in with Firebase + UserCredential userCredential = await firebaseAuth.signInWithCredential( + oauthCredential, + ); + + // Update display name if it's the first sign-in and we have name info + if (userCredential.additionalUserInfo?.isNewUser == true && + credential.givenName != null && + credential.familyName != null) { + final String displayName = + '${credential.givenName} ${credential.familyName}'; + await userCredential.user?.updateDisplayName(displayName); + } + + return userCredential; + } on SignInWithAppleException catch (e) { + _errorService.logError('Apple Sign-In error: $e', StackTrace.current); + throw FirebaseAuthException( + code: 'ERROR_APPLE_SIGNIN_FAILED', + message: 'Apple Sign-In failed: ${e.toString()}', + ); + } on FirebaseAuthException catch (e) { + _errorService.logError( + 'Firebase error during Apple Sign-In: $e', + StackTrace.current, + ); + rethrow; + } catch (e) { + _errorService.logError( + 'Unknown error during Apple Sign-In: $e', + StackTrace.current, + ); + throw FirebaseAuthException( + code: 'ERROR_APPLE_SIGNIN_UNKNOWN', + message: 'Unknown error during Apple Sign-In: $e', + ); + } } } diff --git a/pubspec.lock b/pubspec.lock index 1516e49..728df64 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -944,6 +944,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + sign_in_with_apple: + dependency: "direct main" + description: + name: sign_in_with_apple + sha256: "8bd875c8e8748272749eb6d25b896f768e7e9d60988446d543fe85a37a2392b8" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + sign_in_with_apple_platform_interface: + dependency: transitive + description: + name: sign_in_with_apple_platform_interface + sha256: "981bca52cf3bb9c3ad7ef44aace2d543e5c468bb713fd8dda4275ff76dfa6659" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + sign_in_with_apple_web: + dependency: transitive + description: + name: sign_in_with_apple_web + sha256: f316400827f52cafcf50d00e1a2e8a0abc534ca1264e856a81c5f06bd5b10fed + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 33c77d6..0783493 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: intl: ^0.20.2 firebase_storage: ^13.0.3 url_launcher: ^6.3.2 + sign_in_with_apple: ^7.0.1 dev_dependencies: flutter_launcher_icons: ^0.13.1