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

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.

Related

How does firebase passwordless authentication work with dynamic link for a flutter app?

When I click on verification link from my email , it opens my app running in background but didChangeAppLifecycleState method returns data as null and deepLink as set in firebase instead of the whole emailLink from my email, resulting in SignInWithEmailAndLink to fail as its supposed to match email address entered in widget from the link recieved.
Here's the code taken from this article https://medium.com/firebase-developers/dive-into-firebase-auth-on-flutter-email-and-link-sign-in-e51603eb08f8 :-
1.didChangeAppLifecycleState method
#override
void didChangeAppLifecycleState(AppLifecycleState state) async {
if (state == AppLifecycleState.resumed) {
final PendingDynamicLinkData data =
await FirebaseDynamicLinks.instance.getInitialLink();
if( data?.link != null ) {
handleLink(data?.link);
}
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLink) async {
final Uri deepLink `enter code here`= dynamicLink?.link;
handleLink(deepLink);
}, onError: (OnLinkErrorException e) async {
print('onLinkError');
print(e.message);
});
}
}
handleLink method
void handleLink(Uri link) async {
if (link != null) {
final User user = (await _auth.signInWithEmailAndLink(
email: _userEmail,
link: link.toString(),
))
.user;
if (user != null) {
setState(() {
_userID = user.uid;
_success = true;
});
} else {
setState(() {
_success = false;
});
}
} else {
setState(() {
_success = false;
});
}
setState(() {});
}
main method (initializing firebase)
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
Note: - Deeplink is a concept still new to me in flutter, so I have set it randomly in firebase. My register page has a WidgetsBindingObserver to help resume app lifecycle state. My signup and sign in code is in an email widget and I'm not using forms to validate my textfields.
it seems that after you signin with email link the firebase instance has been not updated before checking auth.SigninWithEMailLink() you need to update it in firebase instance like this
var user= await auth.currentUser();
await user.reload();
user=await auth.currentUser();
if still you are facing problem please provide some more code to let me understand your issue properly

How to navigate on the another page after google sign in

Future<void> signUpWithGoogle() async {
try {
final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
final AuthCredential credential = GoogleAuthProvider.getCredential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
final FirebaseUser user =
(await _auth.signInWithCredential(credential)).user;
return user;
} catch (error) {
print(error);
}
}
I have done that above code to sign in with google and called the function of google sign button like this
signUpWithGoogle().then((value) => Navigator.of(context).push(MaterialPageRoute(builder: (_) {
return HomePage();
})));
But on first time when apk is installed normally the app is asking for choosing the google account
But after log out when i tap on the google SignIn button it is not asking in pop up menu to select the account.
And one more problem is their on clicking on the google signin button firsts it goes to the HomePage() then signIn is hapenning.
#override
void initState() {
super.initState();
getCurrentUser();
}
Future<void> getCurrentUser() async {
FirebaseUser user = await _auth.currentUser();
bool result = await facebookSignIn.isLoggedIn;
if (user != null && user.isEmailVerified == true) {
print("Email");
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) {
return HomePage();
}));
}
}
Is this is the correct method to navigate to the HomePage() for those users who is signed in when app starts
Whenever user logout and sign in again with google account android is smart enough to provide google account directly to the app without giving sign in pop up
for your second problem plz ref this answer -:
Why might a function not get called in initState(){ super.initState()} in flutter, but work perfectly fine if called later in the same page?
first of all i am also new to flutter so this might not be the best solution however this is what i implement in my app for the authentication part
first i created a User class that contain an ID for the user
then i created a stream of user to my app so the app will always be provided with this value and what ever change happen to it in my services this is the code
final FirebaseAuth _auth = FirebaseAuth.instance;
Stream<FirebaseUser> user; // firebase user
User _userFromFireBaseUser(FirebaseUser user) {
return user != null ? User(uid: user.uid) : null;
}
// //auth change user stream
Stream<User> get userStream {
return _auth.onAuthStateChanged.map(_userFromFireBaseUser);
// or we can use .map((FirebaseUser user) => _userFromFireBaseUser(user) );
}
in my main widget
StreamProvider<User>.value(
lazy: false,
value: AuthService().userStream,
child: MaterialApp()//your main widget
then i created a statless wrapper class that read the stream value and according move to a page, in my app i used the wrapper to go to the sign in if the user is null else go to the profile page, in your application i guess it will go to the homepage
class ProfileWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
//print(user.uid);
if (user == null) {
print('no user');
return SignUpPage();
} else if (user != null) {
print('there is user');
print(user.uid);
return ProfilePage();
// print('there is user' + user.displayname);
// print('there is user' + user.photourl);
}
}
}
also you need to add the provider package in your pubspec.yaml file
provider: ^4.1.1
by doing this you don't need to handle any navigation between the home and the sign up, if your user is signed in you will automatically be navigated to the home page.
another solution if that is not what you are looking for, after the google sign function finishes check if the firebase user is not null, if there is a user navigate to your homepage

How to check if phone number is already registered in firebase authentication using flutter

So i am making a simple sign up and login screens in flutter application that uses phone authentication of firebase. For sign up im able to register new user, as the user provides his phone number and gets OTP. But for login i wanna check if the entered number is already registered. If so he gets otp and logs in or if not registered then asks to sign up first.
Firebase admin SDK supports this. Here's how to set up firebase admin (documentation). After you set up admin, you can use cloud_functions package to call APIs from the firebase admin SDK and the API we'll be using is one that allows us to get a user by phone number (documentation). If the API response is a user record, we know a phone exists.
In this example, I'm using node.js. In functions/index.js:
exports.checkIfPhoneExists = functions.https.onCall((data, context) => {
const phone = data.phone
return admin.auth().getUserByPhoneNumber(phone)
.then(function(userRecord){
return true;
})
.catch(function(error) {
return false;
});
});
In your dart code:
final HttpsCallable callable = CloudFunctions.instance.getHttpsCallable(functionName: 'checkIfPhoneExists');
dynamic resp = await callable.call({'phone': _phone});
if (resp.data) {
// user exists
}
Once the OTP is sent to the user you can verify if the user is a new user or an existing one in verify OTP function
verifyOtp(String input, context) async {
String retVal = "error";
OurUser _user = OurUser();
print(input);
final AuthCredential credential = PhoneAuthProvider.credential(
verificationId: _verificationId, smsCode: input);
try {
// await _auth.signInWithCredential(credential);
UserCredential _authResult = await _auth.signInWithCredential(credential);
// Here i have to save the details of the user in the database
if (_authResult.additionalUserInfo.isNewUser) {
currentUser.uid = _authResult.user.uid;
currentUser.phone = _inputText;
currentUser.type = "Customer";
retVal = await OurDatabase().createUser(currentUser);
} else {
// get the information of the user from the database this already exists
currentUser = await OurDatabase().getUserInfo(_authResult.user.uid);
if(currentUser!= null) {
Navigator.pushNamedAndRemoveUntil(
context, "/homescreen", (route) => false);
}
}
print("End of the await");
// when signup with the otp
if (retVal == "success") {
print("why not inside this mane");
Navigator.pushNamedAndRemoveUntil(
context, "/homescreen", (route) => false);
}
saveAllData();
} catch (e) {
print(e);
print("Something went wrong");
//prin
}
}
Now this is when you want to verify OTP from the user and after the top is verified you can know if the user was indeed a new user or an old one but what if you wanted to know that beforehand then the best possible solution would be to create a new collection in the firestore that would have only one document(so you are charged only for one document read) that would just contain all the numbers of the users that are registered within your application,
I used a simple straight forward way and it worked just fine.
First, add the mobile number to the firebase database in a separate node when the user creates the account.
await dbref.child("RegisteredNumbers").push().set({
"phoneNo": FirebaseAuth.instance.currentUser!.phoneNumber,
});
whenever a user tries to log in or signup check in this node if the provided number is available in It or not.
Future<bool> checkNumberIsRegistered({required String number}) async {
bool isNumberRegistered = false;
try {
await dbref.child("RegisteredNumbers").once().then((data) {
for (var i in data.snapshot.children) {
String data = i.child("phoneNo").value.toString();
if (number == data) {
isNumberRegistered = true;
return isNumberRegistered;
} else {
isNumberRegistered = false;
}
}
});
return isNumberRegistered;
} catch (e) {
return false;
}
}
Hope it helps

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.

Firebase/Flutter: reload() not refreshing user.isEmailVerified

I'm sending a verification link when a user registers in the app, but when I try to create a stream that listens for when the user has clicked the verify link in the email.
I'm aware that I somehow need to refresh the user token, but I can't seem to get it to work. I thought reload() method was the one, but maybe I'm just not implementing it correctly.
The problem is that the Stream always returns isEmailVerified == false, only way to make it true is for the user to log out and log in again, which is something I'd like to avoid. How do I do this?
I've created this future:
//CHECKS IF EMAIL IS VERIFIED
Future<bool> checkIfEmailIsVerified() async {
FirebaseUser currUser = await _auth.currentUser();
await currUser.reload();
currUser = await _auth.currentUser();
final bool flag = currUser.isEmailVerified;
if (currUser != null) {
return flag;
} else {
return false;
}
}
and this stream:
//IS EMAILVERIFIED STREAM
Stream<EmailVerified> get emailVerified async* {
final bool isEmailVerified = await checkIfEmailIsVerified();
yield EmailVerified(isEmailVerified);
}
Unfortunately it's necessary to get fresh instance of the user after reload:
User user = FirebaseAuth.instance.currentUser;
if (user != null) {
await user.reload();
user = FirebaseAuth.instance.currentUser;
if (user.emailVerified) {
...
}
}
Try using a single await over chained futures
FirebaseUser currUser = await _auth.currentUser().then((u) => u.reload().then((_) => _auth.currentUser()));
final bool flag = currUser.isEmailVerified;

Resources