cyberhybridhub/lib/services/auth_service_firebase.dart
2026-06-04 19:31:04 +00:00

260 lines
7.8 KiB
Dart

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<AppUser?> get authStateChanges =>
_firebaseAuth.authStateChanges().map(_mapUser);
AppUser? get currentUser => _mapUser(_firebaseAuth.currentUser);
Future<void> 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<void> _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: <String, Object?>{
'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<void> 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: <String, Object?>{
'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<void> _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: <String, Object?>{
'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<void> _signInWithGoogleRedirect({String? triggerCode}) async {
authLog(
'web: attempting signInWithRedirect after popup failure',
context: <String, Object?>{
'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<void> signOut() async {
authLog('signOut');
if (_googleSignInReady) {
await _googleSignIn.signOut();
}
await _firebaseAuth.signOut();
}
Future<String?> 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: <String, Object?>{'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<String, Object?> _authEnvironmentContext() {
final FirebaseOptions options = DefaultFirebaseOptions.currentPlatform;
return <String, Object?>{
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,
);
}
}