I have bloc, which listen to changes of currentUser state from firebase.
AuthBloc({#required AuthService authService})
: assert(authService != null),
_authService = authService,
super(AuthInitial()) {
_userSubscription = _authService.currentUser
.listen((user) => add(AuthenticationUserChanged(user)));
}
Then add event and the event call this function
Stream<AuthState> _mapAuthenticationUserChangedToState(
AuthenticationUserChanged event) async* {
if (event.user != null) {
// TO GET CUSTOM USER FROM FIRESTORE because of expiration, etc.
var user = await _authService.getUser(event.user.uid);
if (user.expiration == "") {
yield NotAuthorized(user);
} else {
var isAfter =
DateTime.now().toUtc().isAfter(DateTime.fromMillisecondsSinceEpoch(
user.expiration.millisecondsSinceEpoch,
isUtc: false,
).toUtc());
if (isAfter) {
yield NotAuthorized(user);
} else {
yield Authenticated(event.user);
}
}
} else {
yield Unautheticated();
}
But in my RegisterCubit I have signUpSubmitted method, which creates Firebase user and immediately after this is called my AuthBloc, which make sense.
But in my Bloc i need my custom Firestore user, which i have to create after FirebaseUser creation(because I need UID and email).
And this is the problem.
Future<void> signUpFormSubmitted() async {
if (!state.status.isValidated) return;
emit(state.copyWith(status: FormzStatus.submissionInProgress));
try {
await _authService
.registerUser(state.email.value, state.password.value)
.then((value) async {
// THIS IS CALLED AFTER BLOC
// I NEED TO CALL IT AFTER _registerUser() but in front of BLOC
var user =
ApplicationUser(uid: value.user.uid, email: value.user.email);
await _authService.addUserToDocument(user);
});
emit(state.copyWith(status: FormzStatus.submissionSuccess));
} on Exception {
print(Exception);
emit(state.copyWith(status: FormzStatus.submissionFailure));
}
}
My AuthService methods
Future<UserCredential> registerUser(email, password) =>
_auth.createUserWithEmailAndPassword(email: email, password: password);
Future<void> addUserToDocument(ApplicationUser user) {
return _db.collection('users').doc(user.uid).set({
'uid': user.uid,
'firstname': '',
'lastname': '',
'age': '',
'expiration': '',
'email': user.email
});
}
Related
I am trying to login with google and have the data mapped to a firebase user. I'm using getX. So far this works HOWEVER it automatically logs me back in as the same user if I logout and then try to log back in again. I'll send the code for my login page and the page where the logout button is if needed, but I suspect this may have to do with my AuthController which I've included here
class AuthController extends GetxController {
static AuthController instance = Get.find();
GoogleSignIn googleSignIn = GoogleSignIn();
Rxn<User> firebaseUser = Rxn<User>();
Rxn<UserModel> firestoreUser = Rxn<UserModel>();
final RxBool admin = false.obs;
String usersCollection = "users";
#override
void onReady() async {
//run every time auth state changes
ever(firebaseUser, handleAuthChanged);
firebaseUser.bindStream(user);
super.onReady();
}
handleAuthChanged(firebaseUser) async {
//get user data from firestore
if (firebaseUser?.uid != null) {
firestoreUser.bindStream(streamFirestoreUser());
print("You are logged in as ${firebaseUser.email}");
await isAdmin();
}
//this is for new users
if (firebaseUser == null) {
print('Send to signin');
Get.offAll(LoginPage());
} else {
Get.offAll(AppSetup());
}
}
// Firebase user one-time fetch
Future<User> get getUser async => auth.currentUser!;
// Firebase user a realtime stream
Stream<User?> get user => auth.authStateChanges();
//Streams the firestore user from the firestore collection
Stream<UserModel> streamFirestoreUser() {
print('streamFirestoreUser()');
return firebaseFirestore
.doc('/users/${firebaseUser.value!.uid}')
.snapshots()
.map((snapshot) => UserModel.fromSnapshot(snapshot));
}
//get the firestore user from the firestore collection
Future<UserModel> getFirestoreUser() {
return firebaseFirestore
.doc('/users/${firebaseUser.value!.uid}')
.get()
.then((documentSnapshot) => UserModel.fromSnapshot(documentSnapshot));
}
//Method to handle user sign in using email and password
// User registration using email and password
googleLogin(BuildContext context) async {
final GoogleSignInAccount? googleUser = await googleSignIn.signIn();
if (googleUser != null) {
final googleAuth = await googleUser.authentication;
if (googleAuth.accessToken != null && googleAuth.idToken != null) {
try {
await auth
.signInWithCredential(
GoogleAuthProvider.credential(
idToken: googleAuth.idToken,
accessToken: googleAuth.accessToken),
)
.then((firebaseUser) async {
print('uid: ' + firebaseUser.user!.uid.toString());
print('email: ' + firebaseUser.user!.email.toString());
//create the new user object from the login modelled data
UserModel _newUser = UserModel(
id: firebaseUser.user!.uid,
email: firebaseUser.user!.email!,
name: firebaseUser.user!.email!,
photoURL: firebaseUser.user!.photoURL,
cart: [],
);
//create the user in firestore here with the _addUserToFirestore function
_updateUserFirestore(_newUser, firebaseUser.user!);
});
} on FirebaseAuthException catch (error) {
Get.snackbar('auth.signUpErrorTitle'.tr, error.message!,
snackPosition: SnackPosition.BOTTOM,
duration: Duration(seconds: 10),
backgroundColor: Get.theme.snackBarTheme.backgroundColor,
colorText: Get.theme.snackBarTheme.actionTextColor);
}
}
}
}
void _updateUserFirestore(UserModel user, User _firebaseUser) {
firebaseFirestore.doc('/users/${_firebaseUser.uid}').update(user.toJson());
update();
}
updateUserData(Map<String, dynamic> data) {
logger.i("UPDATED");
firebaseFirestore
.collection(usersCollection)
.doc(firebaseUser.value!.uid)
.update(data);
}
//check if user is an admin user
isAdmin() async {
await getUser.then((user) async {
DocumentSnapshot adminRef =
await firebaseFirestore.collection('admin').doc(user.uid).get();
if (adminRef.exists) {
admin.value = true;
} else {
admin.value = false;
}
update();
});
}
// This is the proper sign out method!
Future<void> signOut() {
return auth.signOut();
}
}
Simply add this line of code into your logout function
> await googleSignIn.signOut()
I am following this guide to build my App. Then, I add a document listener to the "users" collection. Now the problem is even if I log out of the first account and log in to the second account, the stream is still reading the previous document if I manually change the document using the console.
Account _userInfo = Account(email: '', name: '', typeName: '', fcmToken: '', isDisable: false);
Account get userInfo => _userInfo;
StreamSubscription<DocumentSnapshot>? userListener;
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FirebaseAuth.instance.userChanges().listen((user) async {
if (user != null) {
print('User != null');
_loginState = ApplicationLoginState.loggedIn;
await setFcmToken();
listenToMyself();
} else {
print('User null');
userListener?.cancel();
_loginState = ApplicationLoginState.loggedOut;
}
notifyListeners();
});
void listenToMyself() {
print('Listening to: ${FirebaseAuth.instance.currentUser!.uid}');
userListener = usersCollection.doc(FirebaseAuth.instance.currentUser!.uid).snapshots().listen(
(event) {
print("current data: ${FirebaseAuth.instance.currentUser!.uid} ${event.data()}");
_userInfo = Account.fromJson(event.data() as Map<String, dynamic>);
notifyListeners();
},
onError: (error) => print("Listen failed: $error"),
);
}
Future<void> setFcmToken() async {
String? token = await FirebaseMessaging.instance.getToken();
_token = token;
await FirebaseFirestore.instance.collection('users').doc(FirebaseAuth.instance.currentUser!.uid).set({'fcmToken': token}, SetOptions(merge: true));
}
I'm trying to write the code of the ApplicationState in Get to know Firebase for Flutter codelab while practicing Test Driven Development.
The method signOut in the codelab should be like this:
void signOut() {
// The question is about how to test the effect of this invocation
FirebaseAuth.instance.signOut();
}
If I understand it right, FirebaseAuth.instance.signOut() should make FirebaseAuth.instance.userChanges() invoke its listener with Stream<User?> that contains a null User. So the effect of invoking FirebaseAuth.instance.signOut() is not direct. I don't know how to mock this. According to Test Driven Development, I should never write a code unless I do that to let (a failing test) pass.
The problem is how to write a failing test that forces me to write the following code:
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loginState = ApplicationLoginState.loggedIn;
}
////////////////////////////////////////////////////////////
else {
_loginState = ApplicationLoginState.loggedOut; ////
}
////////////////////////////////////////////////////////////
notifyListeners();
});
}
I can mock FirebaseAuth.instance.signOut() like this:
// firebaseAuth is a mocked object
when(firebaseAuth.signOut())
.thenAnswer((realInvocation) => Completer<void>().future);
// sut (system under test)
sut.signOut();
verify(firebaseAuth.signOut()).called(1);
And this forces me to invoke FirebaseAuth.instance.signOut(). But this doesn't force me to write the aforementioned code to let it pass.
I test this code:
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FirebaseAuth.instance.userChanges().listen((user) {
/////////////////////////////////////////////////////////////
if (user != null) {
_loginState = ApplicationLoginState.loggedIn; ////
}
/////////////////////////////////////////////////////////////
else {
_loginState = ApplicationLoginState.loggedOut;
}
notifyListeners();
});
}
By mocking FirebaseAuth.instance.signInWithEmailAndPassword():
final userCredential = MockUserCredential();
// firebaseAuth is a mocked object
when(firebaseAuth.signInWithEmailAndPassword(
email: validEmail, password: password))
.thenAnswer((realInvocation) => Future.value(userCredential));
// sut (system under test)
await sut.signInWithEmailAndPassword(
validEmail, password, firebaseAuthExceptionCallback);
// This is the direct effect on my class, that will happen by the aforementioned code
expect(sut.loginState, ApplicationLoginState.loggedIn);
Please, be patient with me. I'm new to doing testing and Test Driven Development.
Here is what I did.
Instead of calling FirebaseAuth.instance.userChanges().listen() once in the init() method as in the codelab, I called it twice, one time on the signInWithEmailAndPassword() method, and one time on the signOut() method.
Future<void> signInWithEmailAndPassword(String email, String password,
void Function(FirebaseAuthException exception) errorCallback) async {
///////////////////////////////////////////////////////////////////
firebaseAuth.userChanges().listen(_whenNotNullUser); ////
///////////////////////////////////////////////////////////////////
try {
await firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
} on FirebaseAuthException catch (exception) {
errorCallback(exception);
}
}
Future<void> signOut() async {
///////////////////////////////////////////////////////////////////
firebaseAuth.userChanges().listen(_whenNullUser); ////
///////////////////////////////////////////////////////////////////
await firebaseAuth.signOut();
}
void _whenNullUser(User? user) {
if (user == null) {
_loginState = ApplicationLoginState.loggedOut;
notifyListeners();
}
}
void _whenNotNullUser(User? user) {
if (user != null) {
_loginState = ApplicationLoginState.loggedIn;
notifyListeners();
}
}
And this is my test:
test("""
$given $workingWithApplicationState
$wheN Calling signInWithEmailAndPassword()
$and Calling loginState returns ApplicationLoginState.loggedIn
$and Calling signOut()
$and Calling loginState returns ApplicationLoginState.loggedOut
$and Calling signInWithEmailAndPassword()
$then Calling loginState should return ApplicationLogginState.loggedIn
$and $notifyListenersCalled
""", () async {
when(firebaseAuth.signInWithEmailAndPassword(
email: validEmail, password: password))
.thenAnswer((realInvocation) => Future.value(userCredential));
await sut.signInWithEmailAndPassword(
validEmail, password, firebaseAuthExceptionCallback);
expect(sut.loginState, ApplicationLoginState.loggedIn);
reset(notifyListenerCall);
prepareUserChangesForTest(nullUser);
await sut.signOut();
verify(firebaseAuth.signOut()).called(1);
expect(sut.loginState, ApplicationLoginState.loggedOut);
reset(notifyListenerCall);
prepareUserChangesForTest(notNullUser);
when(firebaseAuth.signInWithEmailAndPassword(
email: validEmail, password: password))
.thenAnswer((realInvocation) => Future.value(userCredential));
await sut.signInWithEmailAndPassword(
validEmail, password, firebaseAuthExceptionCallback);
expect(sut.loginState, ApplicationLoginState.loggedIn);
verify(notifyListenerCall()).called(1);
});
Now I'm forced to write the login in both _whenNullUser() and _whenNotNullUser() methods to pass my test.
I have used firebase auth for login and registration. In that I created a map in relatime database to store users credential.
Like This
Future validateForm() async {
FormState formSate = _formKey.currentState;
if (formSate.validate()) {
final User firebaseUser = (await firebaseAuth
.createUserWithEmailAndPassword(
email: _emailcontroller.text,
password: _passwordcontroller.text)
.catchError((errMsg) {
displayToast("Error: " + errMsg.toString(), context);
}))
.user;
if (firebaseUser != null) {
Map userDataMap = {
"name": _namecontroller.text.trim(),
"email": _emailcontroller.text.trim(),
"phone": _phonecontroller.text.trim(),
};
usersRef.child(firebaseUser.uid).set(userDataMap);
displayToast("Succesfully Registered!", context);
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) {
return LocationHome();
}));
} else {
displayToast("User was unable to create", context);
}
}
}
}
displayToast(String msg, BuildContext context) {
Fluttertoast.showToast(msg: msg);
}
Now please help me how to add extra information to this map from another page and also how to access these details from database to app
Register Screen On Pressed method given, I believe there is a problem with calling Firebase user = result.user
onPressed: () async {
if(_formKey.currentState.validate()){
setState(() => loading = true);
dynamic result = await _auth.registerWithEmailAndPassword(email, password);
FirebaseUser user = result.user;
await DatabaseService(uid: user.uid).newUserInfo(
_nameC.text,
_cityC.text,
_contactnoC.toString()
);
if(result == null) {
setState(() {
error = 'Please supply a valid email';
loading = false;
});
}}},
// Database backend
class DatabaseService {
final String uid;
DatabaseService ({this.uid});
final CollectionReference userdata2 = Firestore.instance.collection('UserData');
Future newUserInfo(String name, String city, String contactno) async {
return await userdata2.document(uid).setData({
'name' : name,
'city' : city,
'contactno' : contactno
});
}}
// authentication backend
// register with email and password
Future registerWithEmailAndPassword(String email, String password) async {
try {
AuthResult result = await _auth.createUserWithEmailAndPassword(email: email, password: password);
FirebaseUser user = result.user;
DatabaseService(uid: user.uid);
return _userFromFirebaseUser(user);
} catch (error) {
print(error.toString());
return null;
} }
// user.dart
class User {
final String uid;
User({this.uid});
}