Display error message outside build widget - firebase

I am using a Model class to authenticate user before registering or logging.the problem is that i don't know a way to print error message to the user in snackbar,because no widget is defined in this class.
How can i display error message to user from Model Class?
Model class:
class FireAuth {
static Future<User> registerUsingEmailPassword({
String name,
String email,
String password,
}) async {
FirebaseAuth auth = FirebaseAuth.instance;
User user;
try {
UserCredential userCredential = await auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
user = userCredential.user;
await user.updateDisplayName(name);
await user.reload();
user = auth.currentUser;
//check if email is registered before
//add user data to firestore
CollectionReference users = FirebaseFirestore.instance.collection('users');
users.doc(user.uid).set({
'uid':user.uid,
'img_url':'0',
'name': name,
'phone': '',
'email': email,
'job_title':'',
'university':'',
'procedures':'',
'expert_in':'',
})
.then((value) => print("User Added"))
.catchError(
(error) => print("Failed to add user: $error"));
} on FirebaseAuthException catch (e) {
if (e.code == 'weak-password') {
print('The password provided is too weak.');
} else if (e.code == 'email-already-in-use') {
print('The account already exists for that email.');
}
} catch (e) {
print(e);
}
return user;
}
}
I need 'The account already exists for that email.' error message to display to user,not only printing it in log.

Excellent question, and I'll try to answer in the general so as to benefit your overall pattern in handling this very important case.
Depending on BuildContext is a common inconvenience in flutter. And it often comes up, but for good reason. You can think of it like this: You need the context because you need to specify where in the tree that UI is going to show. Knowing how to handle these cases makes the difference between beginner and more advanced flutter developers.
So one way is to pass the BuildContext around, but I wouldn't recommended it.
Lets say I have a function foo that returns some Future Rather than change the signature of the function to accept context, you can simply await the function and use the context in the callback already in your UI. For example,
instead of
Future foo(BuildContext context) {
try {
// await some async process
// Use context to show success.
} catch (e) {
// Use context to show failure.
}
}
You can do this
GestureDetector(
onTap: () async {
try {
await foo();
// Use context to show success.
} catch (e) {
// Use context to show failure.
}
},
child: // some child
),
The point is in the second example the context is already there in the widget. The signuture of foo is simpler. It requires some restructuring. Here I'm assuming that the series of events is traced back to a GestureDetector but it could be anything else.

Related

Firebase Authentication with Flutter not working

I am trying to create a signup page which should give an error message if user with particular email id already exist. But it's not working.
signUp() {
if (formkey.currentState!.validate()) {
Map<String, String> userDataMap = {
"name": usernameC.text,
"email": emailC.text
};
setState(() {
isLoading = true;
});
authMethods.signUp(emailC.text, passwordC.text).then((value) {
databaseMethods.uploadUserData(userDataMap);
Navigator.pushReplacement(
context, MaterialPageRoute(builder: (context) => ChatRoom()));
});
}
}
It calls the signUp() function from auth.dart given below
UserData? _userFromFirebase(User? user) {
return user != null ? UserData(userid: user.uid) : null;
}
Future signUp(String email, String pass) async {
try {
UserCredential result = await _auth.createUserWithEmailAndPassword(
email: email, password: pass);
User? user = result.user;
return _userFromFirebase(user);
} catch (e) {
print(e}
}
Every time I signup with same email it doesn't give any error.
If you sign up with the same email you should get this message:
[firebase_auth/email-already-in-use] The email address is already in use by another account.
I use print(e.hashCode) and then use this hash code to show an error message.
Ok I tried this method and it worked out. Just added null check for the "value" attribute in.
authMethods.signUp(emailC.text, passwordC.text).then((value)
It was returning null without any other message. That's why I was unable to see the error.

Future<UserData> returns a Future<UserData> instead of a UserData variable

I have a widget that uses a Future - async that returns a UserData variable, but for some reason, I'm getting a Future variable instead.
a line of code from a widget Build:
dynamic user = Auth.signIn(_email, _password);
Auth Class:
Future<UserData> signIn(String email, String password) async {
try{
UserCredential userCredential = await _auth.signInWithEmailAndPassword(
email: email,
password: password
);
print('signed in!');
return _createUserFromFirebase(userCredential.user.uid, null);
} on FirebaseAuthException catch (e) {
if (e.code == 'user-not-found') {
print('No user found for that email.');
} else if (e.code == 'wrong-password') {
print('Wrong password provided for that user.');
}
return null;
}
}
You could try something like
return Future.value(_createUserFromFirebase(userCredential.user.uid, null);
instead of
return _createUserFromFirebase(userCredential.user.uid, null);
Async cannot be used without it being in a function with await. You can't randomly retrieve an asynchronous variable randomly in the script without loading it asynchronously, because the future might not have the data ready to be used.
If you need the future variable's future value to be in a widget try using the FutureBuilder widget. This will make it so that the user data is loaded asynchronously and a place holder of your choice can be shown before it loads. This would allow you to use the UserData type in your widget.
Or you can get the user by having the button have an async variable that sets a variable in the widget from null to the UserData after signing in.
I've added a new future method inside the widget class that saves the user in a variable:
Future<void> getUserData(String email, String password) async{
_user = await _auth.signIn(email, password);
print('user: $_user');
}

Firebase + Flutter: can't lock access to unverified email accounts

I'd like to block out people who didn't verify their email so i figured out this code for sign up:
// sign up
Future signUp(String email, String password) async {
try {
await _auth.createUserWithEmailAndPassword(
email: email, password: password);
} catch (e) {
print('An error has occured by creating a new user');
print(
e.toString(),
);
}
try {
final FirebaseUser _user = await _auth.currentUser();
await _user.sendEmailVerification();
} catch (error) {
print("An error occured while trying to send email verification");
print(error.toString());
}
try {
await _auth.signOut();
} catch (err) {
print(err);
}
}
and this for sign in:
//Sign In with Email and Pass
Future signInWithEmailAndPassword(String email, String password) async {
FirebaseUser _user = await FirebaseAuth.instance.currentUser();
if (_user != null && _user.isEmailVerified == true) {
try {
await _auth.signInWithEmailAndPassword(
email: email, password: password);
return _user;
} catch (e) {
return null;
}
} else {
return null;
}
}
_auth is just an instance of FirebaseAuth.
The problem is that i can login even if i didnt verify the email.
Firebase Auth doesn't stop accounts from signing in if the user hasn't verified their email address yet. You can check that property _user.isEmailVerified to find out the state of that validation after the user signs in, and you can determine from there what the user should see.
isEmailVerified can be a little bit of trouble to get working correctly.
Make sure you are calling
await FirebaseAuth.instance.currentUser()..reload();
before your are calling isEmailVerified also in my own experience and I don't know if this is just something I was doing wrong but this did not work from my Auth class this did not start working until I put the code directly in initState() of my widget that checks whether the user is verified. Like I said that part might have been something I did wrong. Like stated this will not listen for change you must check yourself either periodically or at a point that you know email is verified.
Future(() async {
_timer = Timer.periodic(Duration(seconds: 10), (timer) async {
await FirebaseAuth.instance.currentUser()
..reload();
var user = await FirebaseAuth.instance.currentUser();
if (user.isEmailVerified) {
timer.cancel();
Navigator.of(context).popAndPushNamed(HearingsScreen.routeName);
}
});
});
So it checks every 10 seconds to see if the user has verified their email not the most elegant solution. The page I have this on just displays a message 'Please verify your email' so its not like this is interrupting other code. If your app is performing other tasks this might not be an option for you. If you want to play around with isEmailVerified go ahead but i spent a week of headaches until i settled on this.

Can't pass Firebaseuser object to a new screen in Flutter

After creating the user in signup screen, I want to pass the user object to a new screen. But the user object is always null in the new screen though user creation is successful. I know current user can be accessed with FirebaseAuth.instance.currentUser() . But why is the user object null when it is passed from signup screen to a new screen?
Future<FirebaseUser> signUpUserWithEmailPass(
String email, String pass) async {
try {
var authResult = await firebaseAuth.createUserWithEmailAndPassword(
email: email,
password: pass,
);
print("On Try");
return authResult.user;
} on PlatformException catch (error) {
throw Exception(errors.toString());
}
}
Navigation to a new screen :
void navigateToHomePage(BuildContext context, FirebaseUser user) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return HomePage(user);
}));
}
Update : Solved
I am using BLoC pattern & I forgot to set user object in the state class's constructor. That's actually creating the problem!
It's null because it's asynchronous, the method signUpUserWithEmailPass returns a Future, then when you call it you can pass the result to the navigateToHomeScreen:
signUpUserWithEmailPass.then((result) => {
print(result);
navigateToHomePage(context,result);
});
This may be due to Asynchronous function. The user object could've been passed before you got the result.
So Kindly wait for signUpUserWithEmailPass() function complete first and then assign it to user object like this:
user = await signUpUserWithEmailPass("email", "password");
navigateToHomePage(context, user);
for this you have to make the parent function async.
Otherwise do this:
signUpUserWithEmailPass.then((user) {
navigateToHomePage(context, user);
});
Hope it helps

Flutter: How to listen to the FirebaseUser is Email verified boolean?

My Idea:
I want to use the Firebase Auth Plugin in Flutter to register the users.
But before they can access the App, they have to verify their Email address.
Therefor I push the Firebase users after registration to a verification screen. This is just a loading screen which tells the user that he has to verify his email.
But now: How can I continuously listen, if the users email is verified or not and send him (when true) to the Homescreen?
I'm new to Flutter and I don't know if I have to use a Streams or Observables or a while Loop or setState() or something else for such a boolean check. And I also don't know how to setup a solution.
This is my basic code for register a user:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
final Firestore _db = Firestore.instance;
Future<FirebaseUser> get getUser => _auth.currentUser();
Stream<FirebaseUser> get user => _auth.onAuthStateChanged;
Future<FirebaseUser> edubslogin(String email, String password) async {
try {
final FirebaseUser user = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
await user.sendEmailVerification();
//email verification somewhere here
updateUserData(user);
return user;
} catch (error) {
print(error);
return null;
}
}
I've tried this:
if (user.isEmailVerified == true) {
//go to Homescreen
return true;
} else {
//show verification screen(loading spinner)
return false;
}
But I don't get a boolean value true out of isEmailVerified.
What do I have to do?
I faced the same situation in my app. My solution was to create a periodic timer into the initState method of a strategic route to hold the app until the e-mail is verified. It is not so elegant as using a listener but works fine.
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
class _AccountConfirmationState extends State<AccountConfirmation> {
late Timer _timer;
#override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 5), (timer) async {
await FirebaseAuth.instance.currentUser?.reload();
final user = FirebaseAuth.instance.currentUser;
if (user?.emailVerified ?? false) {
timer.cancel();
Navigator.pop(context, true);
}
});
}
#override
void dispose() {
super.dispose();
_timer.cancel();
}
#override
Widget build(BuildContext context) {
//TODO: Implement your amazing waiting screen here
}
}
This verification isn't as straightforward as you'd hope. First, there is the problem of recognizing that the user has verified their email. Second, there is the issue that there isn't any sort of a notification you can listen to that will automatically trigger a change in your app.
Check this thread for info about emailVerified: https://github.com/flutter/flutter/issues/20390#issuecomment-514411392
I was only able to verify the user if I 1) Created their account, 2) Signed them in, 3) Then checked to make sure they verified their email.
final FirebaseAuth _auth = FirebaseAuth.instance;
var _authenticatedUser = await _auth.signInWithEmailAndPassword(email: _email, password: _password);
//where _email and _password were simply what the user typed in the textfields.
if (_authenticatedUser.isEmailVerified) {
//Verified
} else {
//Not verified
}
Part 2: How do you get your app to recognize that the user has confirmed their email? Find a way to trigger the function that checks confirmation. A button would be easy enough. If you want it to see "automatic" then I guess you could create a timer that checks for email verification every 10 seconds or so.
Well I created a stream to handle this. Not so elegant but works. Use a StreamProvider.value() to handle events.
Stream<userVerificationStatus> checkUserVerified() async* {
bool verified = false;
yield userVerificationStatus(status: Status.LOADING);
while (!verified) {
await Future.delayed(Duration(seconds: 5));
FirebaseUser user = await _auth.currentUser();
if(user!=null)await user.reload();
if (user == null) {
yield userVerificationStatus(status: Status.NULL);
} else {
print("isemailverified ${user.isEmailVerified}");
await user.reload();
verified = user.isEmailVerified;
if(verified)
yield userVerificationStatus(status: Status.VERIFIED);
else
yield userVerificationStatus(status: Status.NOT_VERIFIED);
}
}
}
True. None of the FirebaseAuth idTokenChanges() , authStateChanges() or userChanges() will send you an event if the user verifies their email. I'm using a combination of the methods to get an email verification update in my app and it seems to be working well.
First I check the status in the initState() method and start a timer if email is not verified
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
//Get Authenticated user
user = context.read<AuthenticationService>().currentUser();
_isEmailVerified = user.emailVerified;
if (!_isEmailVerified) _startEmailVerificationTimer();
}
I also listen for app background/foreground events in case the user happens to leave the app to confirm their email ( If you also do this, add WidgetsBindingObserver to your class)
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
user = context.read<AuthenticationService>().reloadCurrentUser();
if (user.emailVerified) {
setState(() {
_isEmailVerified = user.emailVerified;
});
timer?.cancel();
} else {
if (!timer.isActive) _startEmailVerificationTimer();
}
}
}
This is the _startEmailVerificationTimer() method
_startEmailVerificationTimer() {
timer = Timer.periodic(Duration(seconds: 5), (Timer _) {
user = context.read<AuthenticationService>().reloadCurrentUser();
if (user.emailVerified) {
setState(() {
_isEmailVerified = user.emailVerified;
});
timer.cancel();
}
});
}
Don't forget to dispose the timer
#override
void dispose() {
timer?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
My Firebase User methods in case anyone is interested:
User currentUser() {
return _firebaseAuth.currentUser;
}
User reloadCurrentUser() {
User oldUser = _firebaseAuth.currentUser;
oldUser.reload();
User newUser = _firebaseAuth.currentUser;
return newUser;
}
In order for the app to recognise if the user has verified their email you can achieve this with a simple user.reload.
In order to test it yourself implement a button with onPressed code:
FlatButton(
child: Text("check"),
textColor: Colors.white,
onPressed: () async {
try {
FirebaseUser user = await _firebaseAuth.currentUser();
await user.reload();
user = await _firebaseAuth.currentUser();
print( user.isEmailVerified);
} catch (e) {
return e.message;
}
}),
I had the same problem with the latest version of firebase auth.
But I found out there is a function for reloading the current user which signed in
Future<bool> get userVerified async {
await FirebaseAuth.instance.currentUser.reload();
return FirebaseAuth.instance.currentUser.emailVerified;
}
referesh token after checking current user emailVerified is true
var user = FirebaseAuth.instance.currentUser;
await user?.reload();
if (user?.emailVerified == true) {
await FirebaseAuth.instance.currentUser?.getIdToken(true);
//rest code..
}
also please let me know if this a correct way of doing things.
I have found a way by updating firebase user profile and calling it in init() like below function.
void _checkEmailVerification() async {
await widget.auth.getCurrentUser().then((user) {
UserUpdateInfo userUpdateInfo = new UserUpdateInfo();
userUpdateInfo.displayName = user.displayName;
user.updateProfile(userUpdateInfo).then((onValue) {
setState(() {
_isEmailVerified = user.isEmailVerified;
});
});
});
}
Auth state change listener didn't work for me. Field isEmailVerified remains false even after user verifies his email.
My workaround:
Started from the assumption that user leaves the app to verify his email (which mean app is paused), and he returns to the app after verifying it (app resumes).
What I did was attach a WidgetsBinding to a relevant stateful widget where I wanted to display if email was verified (but can be done elsewhere). This involves two steps.
First step is to attach the binding:
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
Second step is to override the didChangeAppLifecycleState to reload the user. I created a function that does the reload and sets a new firebaseUser object
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed && !firebaseUser.isEmailVerified)
refreshFirebaseUser().then((value) => setState(() {}));
super.didChangeAppLifecycleState(state);
}
Future<void> refreshFirebaseUser() async {
await firebaseUser.reload();
firebaseUser = FirebaseAuth.instance.currentUser;
}
So what this is basically doing is to reload firebase user object everytime the user returns to the app, while its email is not verified. I chose this solution over setting and cancelling a timer as it avoided setting a recurrent action through a timer which could be overkill for this particular problem.
Since authOnChanged only listens for sign in and sign out actions, in your sign in method, first sign out then try to sign in.
await _firebaseAuth.signOut();
authResult = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);
return authResult.user;
In the onAuthChanged, when you control if user.isEmailVerified, it will work since you have signed out and it will update the user even if you haven't signed in yet because sign out will trigger your onAuthChanged even if you haven't signed in.
It is like cheating but the only way that I have found without timeout is this.

Resources