import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:fcs/domain/constants.dart'; import 'package:fcs/domain/entities/auth_result.dart' as fcs; import 'package:fcs/domain/entities/auth_status.dart'; import 'package:fcs/domain/entities/setting.dart'; import 'package:fcs/domain/entities/user.dart'; import 'package:fcs/domain/exceiptions/signin_exception.dart'; import 'package:fcs/helpers/api_helper.dart'; import 'package:fcs/helpers/firebase_helper.dart'; import 'package:firebase_auth/firebase_auth.dart' as fb; import 'package:logging/logging.dart'; import '../services/services.dart'; class AuthFb { final log = Logger('AuthFb'); static final AuthFb instance = AuthFb._(); AuthFb._(); late StreamController controller; static final fb.FirebaseAuth _fb = fb.FirebaseAuth.instance; static String _verificationId = ''; static bool _logIn = false; StreamSubscription? userListener; StreamSubscription? userAuthListener; Future sendSmsCodeToPhoneNumber(String phoneNumber) { Completer completer = Completer(); bool codeSentCompleted = false; final fb.PhoneVerificationCompleted verificationCompleted = (fb.AuthCredential credential) async { fb.UserCredential _authResult; try { _authResult = await _fb.signInWithCredential(credential); print("PhoneVerificationCompleted :$_authResult"); } catch (e) { print("Exception:$e"); // throw e; completer.completeError(SigninException(e.toString())); return; } fcs.AuthResult auth = fcs.AuthResult(authStatus: AuthStatus.AUTH_VERIFIED); completer.complete(auth); print( 'Inside _sendCodeToPhoneNumber: signInWithPhoneNumber auto succeeded: ${_authResult.user}'); }; final fb.PhoneVerificationFailed verificationFailed = (fb.FirebaseAuthException authException) async { print( 'Phone number verification failed. Code: ${authException.code}. Message: ${authException.message}'); completer.completeError(SigninException( "Phone number verification failed:${authException.message}")); }; final fb.PhoneCodeSent codeSent = (String verificationId, [int? forceResendingToken]) async { _verificationId = verificationId; print("codeSent " + phoneNumber); codeSentCompleted = true; if (!completer.isCompleted) completer.complete(fcs.AuthResult(authStatus: AuthStatus.SMS_SENT)); }; final fb.PhoneCodeAutoRetrievalTimeout codeAutoRetrievalTimeout = (String verificationId) { print("codeAutoRetrievalTimeout $verificationId "); _verificationId = verificationId; if (codeSentCompleted) { if (!completer.isCompleted) completer.complete(fcs.AuthResult(authStatus: AuthStatus.SMS_SENT)); } else { completer.completeError(SigninException("SMS code failed")); } }; _fb.verifyPhoneNumber( phoneNumber: phoneNumber, timeout: const Duration(seconds: 0), verificationCompleted: verificationCompleted, verificationFailed: verificationFailed, codeSent: codeSent, codeAutoRetrievalTimeout: codeAutoRetrievalTimeout); return completer.future; } Future signInWithPhoneNumber(String smsCode) async { try { final fb.AuthCredential credential = fb.PhoneAuthProvider.credential( verificationId: _verificationId, smsCode: smsCode); await _fb.signInWithCredential(credential); await _addUserToStream(refreshIdToken: true); } on Exception catch (e) { return Future.error(SigninException(e.toString())); } return Future.value(fcs.AuthResult(authStatus: AuthStatus.AUTH_VERIFIED)); } Future signoutStart() async { await userListener?.cancel(); await userAuthListener?.cancel(); } Future signoutEnd() async { await _fb.signOut(); } Future _addUserToStream({bool refreshIdToken = false}) async { fb.User? firebaseUser = _fb.currentUser; if (firebaseUser == null) return null; Map? claims = await getClaims(refreshIdToken: refreshIdToken); log.info("Claims:$claims"); String? cid = claims["cid"]; User? user; if (cid != null && cid != "") { user = await _getUserFromFirestore(cid); } if (user == null) { controller.add(null); return; } loadUserClaim(claims, user); controller.add(user); } loadUserClaim(Map claims, User user) { // add privileges String? privileges = claims["pr"]; if (privileges != null && privileges != "") { user.privileges = privileges.split(":").toList(); } else { user.privileges = []; } } Future _getUserFromFirestore(String userID) async { DocumentSnapshot snap = await FirebaseFirestore.instance .collection(user_collection) .doc(userID) .get(); if (snap.exists) { User user = User.fromMap(snap.data() as Map, snap.id); return user; } return null; } Future isLogin() async { final fb.User? firebaseUser = _fb.currentUser; return Future.value(firebaseUser != null); } Future signup(String userName) async { await requestAPI("/signup", "POST", payload: { 'user_name': userName, }, token: await getToken()); await _addUserToStream(refreshIdToken: true); _startUserListener(); return; } Future joinInvite(String userName) async { await requestAPI("/join_invite", "POST", payload: { 'user_name': userName, }, token: await getToken()); // refresh token once signup await _addUserToStream(refreshIdToken: true); _startUserListener(); return; } Future hasInvite() async { var invited = await requestAPI("/check_invitation", "GET", token: await getToken()); return invited["invited"]; } Future updateProfileName(String newUserName) async { return await requestAPI("/profile", "PUT", payload: {"user_name": newUserName}, token: await getToken()); } Future updatePreferredCurrency(String currency) async { return await requestAPI("/currency", "PUT", payload: {"preferred_currency": currency}, token: await getToken()); } Future deleteAccount() async { return await requestAPI("/accounts", "DELETE", token: await getToken()); } Stream settings() async* { Stream snapshot = FirebaseFirestore.instance .collection(config_collection) .doc(setting_doc_id) .snapshots(); await for (var snap in snapshot) { Setting setting = Setting.fromMap(snap.data() as Map); yield setting; } } Future _getCurrentUserID() async { fb.User? firebaseUser = _fb.currentUser; if (firebaseUser == null) return null; Map? claims = await getClaims(); String cid = claims["cid"]; return cid; } Future _startUserListener() async { _startAuthListener(); String? _userID = await _getCurrentUserID(); if (_userID == null) { return; } Stream snapshot = FirebaseFirestore.instance .collection(user_collection) .doc(_userID) .snapshots(); userListener?.cancel(); userListener = snapshot.listen((snap) async { User user = User.fromMap(snap.data() as Map, snap.id); fb.User? firebaseUser = _fb.currentUser; if (firebaseUser == null) { userListener?.cancel(); return; } if (!_logIn) return; try { // get privilege from claim Map claims = await getClaims(refreshIdToken: true); loadUserClaim(claims, user); controller.add(user); } catch (e) { controller.add(null); } }); } Future _startAuthListener() async { String? authId = _fb.currentUser?.uid; if (authId == null) return; Stream snapshot = FirebaseFirestore.instance .collection(authCollection) .doc(authId) .snapshots(); userAuthListener?.cancel(); userAuthListener = snapshot.listen((snap) async { if (snap.exists) { Map map = snap.data() as Map; String userID = map['user_id'] ?? ""; User? user = await Services.instance.userService.getUser(userID); if (user == null) return; if (_fb.currentUser == null) { userAuthListener?.cancel(); return; } Map claims = await getClaims(refreshIdToken: true); loadUserClaim(claims, user); log.info("_startAuthListener: $user"); if (_logIn) { controller.add(user); } } }); } Stream user() { // ignore: close_sinks StreamSubscription? authListener; Future _start() async { await authListener?.cancel(); authListener = _fb.authStateChanges().listen((firebaseUser) async { _logIn = firebaseUser != null; if (firebaseUser == null) { controller.add(null); } else { _addUserToStream(refreshIdToken: true); _startUserListener(); } }); } void _stop() { userListener?.cancel(); authListener?.cancel(); } controller = StreamController( onListen: _start, onPause: _stop, onResume: _start, onCancel: _stop); return controller.stream; } }