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'; 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 = ''; 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 signout() async { if (userListener != null) await userListener!.cancel(); return _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"); if (claims == null) return; String? cid = claims["cid"]; User? user; if (cid != null && cid != "") { user = await _getUserFromFirestore(cid); } if (user == null) { controller.add(null); return; } // add privileges String? privileges = claims["pr"]; if (privileges != null && privileges != "") { user.privileges = privileges.split(":").toList(); } else { user.privileges = []; } controller.add(user); } 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()); } 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(); if (claims == null) return null; String cid = claims["cid"]; return cid; } Future _startUserListener() async { if (userListener != null) userListener!.cancel(); String? _userID = await _getCurrentUserID(); if (_userID == null) { return; } Stream snapshot = FirebaseFirestore.instance .collection(user_collection) .doc(_userID) .snapshots(); 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; } try { // get privilege from claim fb.IdTokenResult idToken = await firebaseUser.getIdTokenResult(true); String? privileges = idToken.claims?["pr"] ?? ''; if (privileges != null && privileges != "") { user.privileges = privileges.split(":").toList(); } controller.add(user); } catch (e) { controller.add(null); } }); } StreamSubscription? userListener; Stream user() { // ignore: close_sinks StreamSubscription? authListener; Future _start() async { await authListener?.cancel(); authListener = _fb.authStateChanges().listen((firebaseUser) { 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; } }