import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:google_sign_in/google_sign_in.dart'; import '../config/auth_config.dart'; import '../firebase_options.dart'; import '../models/app_user.dart'; import '../utils/auth_log.dart'; import '../utils/firebase_auth_domain_check.dart'; class AuthServiceFirebase { AuthServiceFirebase._(); static final AuthServiceFirebase instance = AuthServiceFirebase._(); final FirebaseAuth _firebaseAuth = FirebaseAuth.instance; final GoogleSignIn _googleSignIn = GoogleSignIn.instance; bool _googleSignInReady = false; Stream get authStateChanges => _firebaseAuth.authStateChanges().map(_mapUser); AppUser? get currentUser => _mapUser(_firebaseAuth.currentUser); Future initialize() async { authLog('initialize', context: _authEnvironmentContext()); if (kIsWeb) { await logFirebaseAuthorizedDomainsIfWeb(); await _completeWebRedirectIfAny(); // Skip eager google_sign_in init on web. Firebase Auth's signInWithPopup // already loads Google Identity Services, and double-initializing logs // a "google.accounts.id.initialize() is called multiple times" warning. // We lazy-init GIS in _signInWithGoogleWeb() only if the popup fails and // we need the GIS fallback path. return; } if (googleWebOAuthClientId != null) { await _googleSignIn.initialize( serverClientId: googleWebOAuthClientId, ); } else { await _googleSignIn.initialize(); } _googleSignInReady = true; authLog('google_sign_in initialized (mobile/desktop)'); } /// After a redirect-based OAuth round-trip, Firebase completes sign-in here. Future _completeWebRedirectIfAny() async { authLog('web: checking getRedirectResult (return from redirect flow)'); try { final UserCredential credential = await _firebaseAuth.getRedirectResult(); final User? user = credential.user; if (user != null) { authLog( 'web: redirect sign-in completed', context: { 'uid': user.uid, if (user.email != null) 'email': user.email!, 'provider': credential.credential?.providerId ?? 'unknown', }, ); } else { authLog('web: getRedirectResult — no user (normal on fresh load)'); } } on FirebaseAuthException catch (e, st) { authLog( 'web: getRedirectResult FirebaseAuthException', error: e, stackTrace: st, context: firebaseExceptionContext(e), ); } catch (e, st) { authLog( 'web: getRedirectResult failed', error: e, stackTrace: st, ); } } Future signInWithGoogle() async { authLog('signInWithGoogle start', context: _authEnvironmentContext()); if (kIsWeb) { await _signInWithGoogleWeb(); return; } _ensureGoogleSignInReady(); try { final GoogleSignInAccount account = await _googleSignIn.authenticate(); final String? idToken = account.authentication.idToken; if (idToken == null) { throw StateError('Google Sign-In did not return an ID token.'); } final credential = GoogleAuthProvider.credential(idToken: idToken); await _firebaseAuth.signInWithCredential(credential); authLog('signInWithCredential succeeded', context: { 'uid': _firebaseAuth.currentUser?.uid ?? '', }); } catch (e, st) { authLog('native sign-in failed', error: e, stackTrace: st); rethrow; } } /// Web: try Firebase popup first; on failure use full-page redirect. /// /// [GoogleSignIn.authenticate] is not implemented on web (google_sign_in 7.x); /// use [signInWithRedirect] instead of a GIS programmatic fallback. Future _signInWithGoogleWeb() async { authLog('web: sign-in start', context: _authEnvironmentContext()); try { authLog('web: attempting signInWithPopup(GoogleAuthProvider)'); final UserCredential credential = await _firebaseAuth.signInWithPopup( GoogleAuthProvider(), ); authLog( 'web: signInWithPopup succeeded', context: { 'uid': credential.user?.uid ?? '', if (credential.user?.email != null) 'email': credential.user!.email!, }, ); return; } on FirebaseAuthException catch (e, st) { authLog( 'web: signInWithPopup FirebaseAuthException', error: e, stackTrace: st, context: firebaseExceptionContext(e), ); if (_isUserCancelledAuth(e)) { rethrow; } await _signInWithGoogleRedirect(triggerCode: e.code); return; } catch (e, st) { authLog( 'web: signInWithPopup unexpected error', error: e, stackTrace: st, ); if (e is UnimplementedError) { authLog( 'web: caught UnimplementedError from popup path — using redirect', ); } await _signInWithGoogleRedirect(triggerCode: e.runtimeType.toString()); } } /// Full-page redirect when popup/iframe auth fails. Future _signInWithGoogleRedirect({String? triggerCode}) async { authLog( 'web: attempting signInWithRedirect after popup failure', context: { 'trigger': triggerCode ?? 'unknown', ..._authEnvironmentContext(), }, ); try { await _firebaseAuth.signInWithRedirect(GoogleAuthProvider()); authLog( 'web: signInWithRedirect invoked — page will navigate away; ' 'on return, getRedirectResult runs in initialize()', ); } on FirebaseAuthException catch (e, st) { authLog( 'web: signInWithRedirect FirebaseAuthException', error: e, stackTrace: st, context: firebaseExceptionContext(e), ); rethrow; } catch (e, st) { authLog( 'web: signInWithRedirect failed', error: e, stackTrace: st, ); rethrow; } } Future signOut() async { authLog('signOut'); if (_googleSignInReady) { await _googleSignIn.signOut(); } await _firebaseAuth.signOut(); } Future getIdToken({bool forceRefresh = false}) async { final User? user = _firebaseAuth.currentUser; if (user == null) { return null; } try { return await user.getIdToken(forceRefresh); } catch (e, st) { authLog( 'getIdToken failed', error: e, stackTrace: st, context: {'uid': user.uid}, ); rethrow; } } void _ensureGoogleSignInReady() { if (_googleSignInReady) { return; } throw StateError( 'Google Sign-In is not configured. Set googleWebOAuthClientId in ' 'lib/config/auth_config.dart for Android, or run a full restart after ' 'changing auth settings.', ); } bool _isUserCancelledAuth(FirebaseAuthException e) { return e.code == 'popup-closed-by-user' || e.code == 'cancelled-popup-request' || e.code == 'auth/popup-closed-by-user'; } Map _authEnvironmentContext() { final FirebaseOptions options = DefaultFirebaseOptions.currentPlatform; return { if (kIsWeb) 'pageOrigin': Uri.base.origin, if (kIsWeb) 'pageHost': Uri.base.host, 'authDomain': options.authDomain ?? '', 'projectId': options.projectId, 'hasGoogleWebClientId': googleWebOAuthClientId != null, 'platform': kIsWeb ? 'web' : 'native', }; } AppUser? _mapUser(User? user) { if (user == null) { return null; } return AppUser( uid: user.uid, email: user.email, displayName: user.displayName, photoUrl: user.photoURL, ); } }