import 'dart:async'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_auth/firebase_auth.dart' as fb; import 'package:google_sign_in/google_sign_in.dart'; import 'auth_state.dart'; import 'auth_user.dart'; /// Clean abstraction interface over authentication. /// The rest of the application should depend on this interface, /// keeping Firebase implementation details completely isolated. abstract class AuthService { Stream get authStateChanges; AuthState get currentState; AuthUser? get currentUser; bool get isConfigured; Future signInWithEmailAndPassword(String email, String password); Future createUserWithEmailAndPassword(String email, String password, {String? displayName}); Future signInWithGoogle(); Future signInAnonymously(); Future sendPasswordResetEmail(String email); Future getIdToken([bool forceRefresh = false]); Future signOut(); void dispose(); } /// Authoritative Firebase Authentication Service class FirebaseAuthService implements AuthService { fb.FirebaseAuth? _firebaseAuth; final GoogleSignIn? _googleSignIn; final _stateController = StreamController.broadcast(); AuthState _currentState = const AuthState.initial(); StreamSubscription? _fbAuthSubscription; FirebaseAuthService({ fb.FirebaseAuth? firebaseAuth, GoogleSignIn? googleSignIn, }) : _firebaseAuth = firebaseAuth ?? _resolveFirebaseAuth(), _googleSignIn = googleSignIn ?? GoogleSignIn() { _init(); } static fb.FirebaseAuth? _resolveFirebaseAuth() { try { if (Firebase.apps.isNotEmpty) { return fb.FirebaseAuth.instance; } } catch (_) {} return null; } @override Stream get authStateChanges => _stateController.stream; @override AuthState get currentState => _currentState; @override AuthUser? get currentUser => _currentState.user; @override bool get isConfigured { if (_firebaseAuth != null) return true; _firebaseAuth = _resolveFirebaseAuth(); if (_firebaseAuth != null && _fbAuthSubscription == null) { _init(); } return _firebaseAuth != null; } void _init() { final auth = _firebaseAuth; if (auth == null) { // Firebase is not configured or offline during startup _updateState(const AuthState.unauthenticated(isFirebaseConfigured: false)); return; } try { // Listen to native Firebase auth state stream _fbAuthSubscription = auth.authStateChanges().listen( (fb.User? user) { if (user == null) { _updateState(const AuthState.unauthenticated(isFirebaseConfigured: true)); } else { final appUser = _mapFirebaseUser(user); if (user.isAnonymous) { _updateState(AuthState.anonymous(appUser, isFirebaseConfigured: true)); } else { _updateState(AuthState.authenticated(appUser, isFirebaseConfigured: true)); } } }, onError: (dynamic error) { _updateState(AuthState.error( _formatAuthError(error), isFirebaseConfigured: true, )); }, ); } catch (e) { _updateState(const AuthState.unauthenticated(isFirebaseConfigured: false)); } } void _updateState(AuthState state) { _currentState = state; if (!_stateController.isClosed) { _stateController.add(state); } } AuthUser _mapFirebaseUser(fb.User user) { String? provider; if (user.providerData.isNotEmpty) { provider = user.providerData.first.providerId; } else if (user.isAnonymous) { provider = 'anonymous'; } return AuthUser( uid: user.uid, email: user.email, displayName: user.displayName, photoUrl: user.photoURL, isAnonymous: user.isAnonymous, providerId: provider, createdAt: user.metadata.creationTime, lastSignInTime: user.metadata.lastSignInTime, ); } @override Future signInWithEmailAndPassword(String email, String password) async { final auth = _firebaseAuth; if (!isConfigured || auth == null) { _updateState(AuthState.error( 'Cloud authentication is unavailable in offline/unconfigured mode. Local ride recording is active.', user: _currentState.user, isFirebaseConfigured: false, )); return; } _updateState(AuthState.loading( user: _currentState.user, isFirebaseConfigured: true, )); try { final credential = await auth.signInWithEmailAndPassword( email: email.trim(), password: password, ); if (credential.user != null) { final user = _mapFirebaseUser(credential.user!); _updateState(AuthState.authenticated(user, isFirebaseConfigured: true)); } } catch (e) { _updateState(AuthState.error( _formatAuthError(e), user: _currentState.user, isFirebaseConfigured: true, )); } } @override Future createUserWithEmailAndPassword( String email, String password, { String? displayName, }) async { final auth = _firebaseAuth; if (!isConfigured || auth == null) { _updateState(AuthState.error( 'Account registration requires cloud configuration. Local riding remains fully functional.', user: _currentState.user, isFirebaseConfigured: false, )); return; } _updateState(AuthState.loading( user: _currentState.user, isFirebaseConfigured: true, )); try { final credential = await auth.createUserWithEmailAndPassword( email: email.trim(), password: password, ); if (credential.user != null) { if (displayName != null && displayName.trim().isNotEmpty) { await credential.user!.updateDisplayName(displayName.trim()); } final user = _mapFirebaseUser(credential.user!); _updateState(AuthState.authenticated(user, isFirebaseConfigured: true)); } } catch (e) { _updateState(AuthState.error( _formatAuthError(e), user: _currentState.user, isFirebaseConfigured: true, )); } } @override Future signInWithGoogle() async { final auth = _firebaseAuth; if (!isConfigured || auth == null) { _updateState(AuthState.error( 'Google Sign-In is unavailable without Firebase credentials. Local ride features remain available.', user: _currentState.user, isFirebaseConfigured: false, )); return; } _updateState(AuthState.loading( user: _currentState.user, isFirebaseConfigured: true, )); try { final googleSignIn = _googleSignIn ?? GoogleSignIn(); final GoogleSignInAccount? googleUser = await googleSignIn.signIn(); if (googleUser == null) { // User cancelled the sign-in modal/flow _updateState(_currentState.copyWith(status: _currentState.user != null ? AuthStatus.authenticated : AuthStatus.unauthenticated, clearError: true)); return; } final GoogleSignInAuthentication googleAuth = await googleUser.authentication; final credential = fb.GoogleAuthProvider.credential( accessToken: googleAuth.accessToken, idToken: googleAuth.idToken, ); final result = await auth.signInWithCredential(credential); if (result.user != null) { final user = _mapFirebaseUser(result.user!); _updateState(AuthState.authenticated(user, isFirebaseConfigured: true)); } } catch (e) { _updateState(AuthState.error( _formatAuthError(e), user: _currentState.user, isFirebaseConfigured: true, )); } } @override Future signInAnonymously() async { final auth = _firebaseAuth; if (!isConfigured || auth == null) { // Local fallback anonymous user representation final localAnon = AuthUser( uid: 'local_${DateTime.now().millisecondsSinceEpoch}', displayName: 'Local Guest', isAnonymous: true, providerId: 'local', ); _updateState(AuthState.anonymous(localAnon, isFirebaseConfigured: false)); return; } _updateState(AuthState.loading( user: _currentState.user, isFirebaseConfigured: true, )); try { final credential = await auth.signInAnonymously(); if (credential.user != null) { final user = _mapFirebaseUser(credential.user!); _updateState(AuthState.anonymous(user, isFirebaseConfigured: true)); } } catch (e) { _updateState(AuthState.error( _formatAuthError(e), user: _currentState.user, isFirebaseConfigured: true, )); } } @override Future sendPasswordResetEmail(String email) async { final auth = _firebaseAuth; if (!isConfigured || auth == null) { _updateState(AuthState.error( 'Password reset is unavailable in offline mode.', user: _currentState.user, isFirebaseConfigured: false, )); return; } try { await auth.sendPasswordResetEmail(email: email.trim()); } catch (e) { _updateState(AuthState.error( _formatAuthError(e), user: _currentState.user, isFirebaseConfigured: true, )); } } @override Future getIdToken([bool forceRefresh = false]) async { final auth = _firebaseAuth; if (auth == null) return null; final user = auth.currentUser; if (user == null) return null; try { return await user.getIdToken(forceRefresh); } catch (_) { return null; } } @override Future signOut() async { _updateState(AuthState.loading( user: _currentState.user, isFirebaseConfigured: isConfigured, )); try { if (_googleSignIn != null) { try { await _googleSignIn.signOut(); } catch (_) {} } final auth = _firebaseAuth; if (auth != null) { await auth.signOut(); } } catch (_) {} _updateState(AuthState.unauthenticated(isFirebaseConfigured: isConfigured)); } String _formatAuthError(dynamic error) { if (error is fb.FirebaseAuthException) { switch (error.code) { case 'user-not-found': return 'No account found for this email address.'; case 'wrong-password': return 'Incorrect password. Please check and try again.'; case 'invalid-credential': return 'Invalid credentials. Please verify your email and password.'; case 'email-already-in-use': return 'An account already exists with this email address.'; case 'invalid-email': return 'The email address format is invalid.'; case 'weak-password': return 'Password must be at least 6 characters.'; case 'user-disabled': return 'This account has been disabled.'; case 'operation-not-allowed': return 'Sign-in method is currently disabled in configuration.'; case 'network-request-failed': return 'Network unavailable. You can continue recording rides offline.'; case 'too-many-requests': return 'Too many attempts. Please try again in a few moments.'; default: return error.message ?? 'Authentication error (${error.code}).'; } } return error?.toString() ?? 'An unexpected authentication error occurred.'; } @override void dispose() { _fbAuthSubscription?.cancel(); _stateController.close(); } }