Some Problems In Flutter Firebase Login - firebase

I tried coding a login and registration form in an app with firebase auth. There are some problems in my login from.
Please look at this loginForm function which will execute when login button is pressed.
Future loginForm() async {
FormState formSate = _formKey.currentState;
if (formSate.validate()) {
final User firebaseUser = (await firebaseAuth
.signInWithEmailAndPassword(
email: _emailcontroller.text,
password: _passwordcontroller.text)
.catchError((errMsg) {
displayToast("Error: " + errMsg.toString(), context);
}))
.user;
if (firebaseUser != null) {
setState(() {
loading = true;
});
usersRef.child(firebaseUser.uid).once().then((DataSnapshot snap) async {
if (snap.value != null) {
SharedPreferences preferences =
await SharedPreferences.getInstance();
preferences.setString("email", _emailcontroller.text);
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) {
return LocationHome();
}));
displayToast("Succesfully LoggedIn!", context);
} else {
firebaseAuth.signOut();
displayToast("No user found! Please try SignUp", context);
}
});
} else {
displayToast("Error Occured! Cannot log you in", context);
}
}
}
}
You can see here that after login I have programmed it to navigate to Location Page.
But to make user stay logged in I have used a StreamBuilder and checking if snapshot.hasdata in the main file
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MaterialApp(
title: 'TaakStore',
home: StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
if (snapshot.hasData) {
print(snapshot);
return Home();
} else {
return Login();
}
},
),
));
}
In this, you can see that if snapshot.hasdata it should navigate to home screen and if not data then nav to the login screen. The first time when a user opens the app the snapshot has no data so it will open a login screen which is perfect. But the problem is when the user clicks on login button instead of going to location screen it is directly going to home screen because the snapshot has data which is ridiculous.
If someone understand my problem please help me

I think the problem is occuring by using the streamBuilder as streamBuilder continously keeps looking for stream or data and as soon it found the appropriate data it performs the assigned function which is navigating the user to the homeScreen() instead of LocationScreen()
Repleace StreamBuilder on the Main() with the bellow code:
if (FirebaseAuth.instance.currentUser != null) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => Home(),
),
);
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => Location(
),
),
);
}
This will not keep on looking for the stream and only execute the function once when the app is restarted. The same method have been suggested by FirebaseFlutter .

Related

How to maintain Firebase Authentication after refresh with Flutter web?

I am using the authStateChanges stream from Firebase with flutter. I have two views, one for mobile and the another one for a web application. I want to redirect the user to the SignIn screen if he is not connected, logged in or authenticated. At first it works well but then when i am logged in and refresh the browser i got the SignIn screen loaded for like 1 second and then the Web screen appears again. I checked with print what's going on and from what i saw, the authStateChanges Stream is null for that 1-2 seconds(when SignIn screen appears) and then has a value when the stream receives the connected user. Is there a way to check, or wait until this authentication is done before loading the SignIn screen when it must not load it ?
My main component contains the StreamBuilder as following:
Widget build(BuildContext context) {
final firebaseAuthService = Provider.of<FirebaseAuthService>(context);
return StreamBuilder<User>(
stream: firebaseAuthService.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
User user = snapshot.data;
if (user == null) {
//first time no connection
return SignIn();
}
if (kIsWeb) {
return WebMain(user: user);
}
// load mobile version
return MobileMain();
}
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
});
}
Here you can find my FirebaseAuth wrapper class which contains the methods from firebase:
class FirebaseAuthService {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
User _user;
bool get isAuthenticated {
return _user == null ? false : true;
}
User get user {
return _user;
}
Future<User> signInWithEmailAndPassword(
String userEmail, String userPassword) async {
return _user = await _firebaseAuth
.signInWithEmailAndPassword(email: userEmail, password: userPassword)
.then((userCredential) => userCredential.user);
}
Stream<User> authStateChanges() {
_user = _firebaseAuth.currentUser;
return _firebaseAuth.authStateChanges();
}
Future<void> signOut() async {
return _firebaseAuth.signOut();
}
}
While I am not sure why authStateChanges does not notify when the user sign in state is changed (usually a second later), a similar function does seem to work for your use case.
Try idTokenChanges()
FirebaseAuth.instance.idTokenChanges().listen((event) {
print("On Data: ${event}");
});
This event will return your Firebase User object. When refreshed, it might return 'null' initially, but within a second, returns your signed in User. You could potentially make the sign in page wait a couple of seconds to make sure a signed in user isn't being initialized.
EDIT:
While there may be better solutions, this is currently working for me.
final subscription = FirebaseAuth.instance.idTokenChanges().listen(null);
subscription.onData((event) async {
if(event != null) {
print("We have a user now");
isLoading = false;
print(FirebaseAuth.instance.currentUser);
subscription.cancel();
Navigator.push(
context,
MaterialPageRoute(builder: (context) => OverviewController())
);
} else {
print("No user yet..");
await Future.delayed(Duration(seconds: 2));
if(isLoading) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => LoginController())
);
isLoading = false;
subscription.cancel();
}
}
});
For me, the below code seems to work fine. Although there is a warning in docs that says "You should not use this getter to determine the user's current state, instead use [authStateChanges], [idTokenChanges] or [userChanges] to subscribe to updates."
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Diary Book',
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
primarySwatch: Colors.green,
),
home: (FirebaseAuth.instance.currentUser == null)
? LoginPage()
: MainPage(),
);
}
}
I haven't encountered any issues using the above code. I Will let you know if do. If someone can comment any future errors this may have that would be great
FirebaseAuth.instance.authStateChanges().listen(
(event) {
if (event == null) {
print('----user is currently signed out');
} else {
print('----user is signed in ');
}
runApp(
const MyApp()
);
},
);

why it doesn't keep the user logged in

it doesnt matter if the user is logged in or not it goes to the main page.
anf if i make the login page the home page everytime i restart the app it requires to login again.
i want it to be like once log in then be logged in till you log out
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: await getLandingPage(),
routes: {
'upload': (context) => ItemInput(),
'suzuki': (context) => Suzuki(),
'others': (context) => Others(),
},
));
}
Future<Widget> getLandingPage() async {
final FirebaseAuth _auth = FirebaseAuth.instance;
return StreamBuilder<User>(
stream: _auth.authStateChanges(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasData && (!snapshot.data.isAnonymous)) {
return MainPage();
}
return LoginPage();
},
);
}
When the app is started, Firebase automatically restores the user's authentication state. This may take a few moments, as it needs to check with the server whether the account is still active.
During this time, the user will not be signed in yet, so authStateChanges() fires a null. And that's when your code redirects the user to the login page.
You'll want to either wait for a few moments to see if the user state is restored, or move/copy your navigation logic to the login page, so that it redirects to the main page once the user authentication state is restored.
Following on from Frank's answer, this is how to work around the issue of receiving a null on the first authStateChanges() event using a StreamBuilder widget.
#override
Widget build(BuildContext context) {
return StreamBuilder<User>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return _buildWaitingScreen();
default:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
final firebaseUser = snapshot.data;
if (firebaseUser != null) {
//....
}
return SignInPage();
}
}
});
}
If you don't want to use a switch statement, you can check only ConnectionState.active
#override
Widget build(BuildContext context) {
return StreamBuilder<User>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
//...
}
return _buildWaitingScreen();
});
}
According to the docs:
A stream A source of asynchronous data events.
A Stream provides a way to receive a sequence of events. Each event is either a data event, also called an element of the stream, or an error event, which is a notification that something has failed. When a stream has emitted all its event, a single "done" event will notify the listener that the end has been reached.
Since the authStateChanges() returns a Stream then you can use the StreamBuilder to get the result and display the widgets accordingly.
According to the StreamBuilder docs:
As an example, when interacting with a stream producing the integers 0 through 9, the builder may be called with any ordered sub-sequence of the following snapshots that includes the last one (the one with ConnectionState.done):
new AsyncSnapshot.withData(ConnectionState.waiting, null)
new AsyncSnapshot.withData(ConnectionState.active, 0)
new AsyncSnapshot.withData(ConnectionState.active, 1)
...
new AsyncSnapshot.withData(ConnectionState.active, 9)
new AsyncSnapshot.withData(ConnectionState.done, 9)
Therefore the builder of type AsyncWidgetBuilder which is used for asynchronous operation, will call your widgets according to the state of the Stream, for example:
#override
Widget build(BuildContext context) {
final FirebaseAuth _auth = FirebaseAuth.instance;
return new Scaffold(
body: StreamBuilder(
stream: _auth.authStateChanges(),
builder: (context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.hasData)
return MainPage();
else
return LoginPage();
} else
return Center(
child: CircularProgressIndicator(),
);
}));
}
You can use the above code in the splash screen, here the Stream will be in the waiting state where it will display a loading first, and then when it retrieves the data, if it is either null or if there is a user logged in, it will enter the active state and return a widget which satisfies the condition.
https://api.flutter.dev/flutter/widgets/AsyncWidgetBuilder.html
https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
https://api.flutter.dev/flutter/widgets/StreamBuilder/builder.html
After talking with OP. They are using the following plugin google_sign_in, and have an auth.dart file with the following code:
void signOutGoogle() async {
await googleSignIn.signOut();
}
What happened in that case, is that the user signed out from Google auth but was still logged in inside Firebase, so to solve this you can add:
void signOutGoogle() async {
await googleSignIn.signOut();
await _auth.signOut();
}

How to redirect to login page after signout button is pressed in flutter

Here i am using firebase phone authentication, so whenever i click sigout button it gets signed out from firebase instance but it doesn't redirect to the login page.
Here is the code for the sigout button
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('Signout'),
onPressed: () {
AuthService().signOut();
},
)
)
);
Here is the code for AuthService
class AuthService {
handleAuth() {
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
return HomeScreen();
} else {
return LoginPage();
}
});
}
//Sign out
signOut() {
FirebaseAuth.instance.signOut();
}
//SignIn
signIn(AuthCredential authCreds) {
FirebaseAuth.instance.signInWithCredential(authCreds);
}
signInWithOTP(smsCode, verId) {
AuthCredential authCreds = PhoneAuthProvider.getCredential(
verificationId: verId, smsCode: smsCode);
signIn(authCreds);
}
}
How do i redirect it to the login page when signout button is pressed?
Call your login screen and clear out all the previous paths
you can use below shown code
it will clear all the paths and your history and launch new LoginScreen
Navigator.of(context).pushAndRemoveUntil(
new MaterialPageRoute(
builder: (context) =>
new LoginScreen()),
(route) => false);

How to call a function ASA the app runs in flutter?

I have an application that do login with the help of firestore database and I want to do autologin so I made a boolean and set it to false in the database and made the login function set it to true as he or she sign in, so I want to check if the person have already signed in or not as the app runs, any ideas :) ?
here my code:
void getUserData() async {
try {
var firebaseUser = await FirebaseAuth.instance.currentUser();
firestoreInstance
.collection("Students")
.document(usernameController.text)
.get()
.then((value) {
setState(() {
email = (value.data)['email'];
password = (value.data)['password'];
gender = (value.data)['gender'];
loggedin = (value.data)['token'];
});
});
} catch (e) {
print(e.toString);
}
}
You dont have to use a boolean to check if the user is logged in or not. Firebase authentication already offers that. You can check inside the initState:
#override
void initState() {
super.initState();
FirebaseAuth.instance.currentUser().then((res) {
print(res);
if (res != null) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => Home(uid: res.uid)),
);
}
else
{
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SignUp()),
);
}
});
}
Checks if there is a current user or not and navigates to the required page.
If you have different types of users, then you have to identify them in the database. So authenticate in firebase authentication, and use a userType field in the database:
void registerToFb() {
firebaseAuth
.createUserWithEmailAndPassword(
email: emailController.text, password: passwordController.text)
.then((result) {
firestoreInstance.collection("users").document(result.user.uid).setData({
"email": emailController.text,
"name": nameController.text,
"userType" : "Students"
}).then((res) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => Home(uid: result.user.uid)),
);
});
}).catchError((err) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Error"),
content: Text(err.message),
actions: [
FlatButton(
child: Text("Ok"),
onPressed: () {
Navigator.of(context).pop();
},
)
],
);
});
});
}

Firebase email verification Flutter

I know this question has been asked a lot and I have spent a lot of time reading and trying to implement the answers. So I am trying to get the response from isEmailVerified from Firebase Auth to work and it does work but right now it always returns false unless I refresh the app or close it and reopen it. which is obviously a bad user experience. How do I get the response to update without having to close the app.
here is the relevant pieces of code.
Future<bool> isEmailVerified() async {
FirebaseUser user = await _auth.currentUser();
if (user == null) {
return false;
} else {
await user.reload();
user = await _auth.currentUser();
return user.isEmailVerified;
}
}
main.dart
child: Consumer<Auth>(
builder: (_, auth, __) => MaterialApp(
theme: Provider.of<ThemeNotifier>(context).getTheme(),
home: FutureBuilder(
future: Future.wait([auth.isEmailVerified(), auth.tryAutoLogin()]),
builder: (BuildContext ctx, AsyncSnapshot authResultSnapshot) =>
authResultSnapshot.connectionState == ConnectionState.done
? authResultSnapshot.data[1]
? authResultSnapshot.data[0]
? HearingsScreen()
: SplashScreen(
emailVerified: true,
)
: LoginScreen()
: SplashScreen(),
),
It is not returning true until I restart the app
Things I have tried besides this:
1) await user.getIdToken(refresh: true);
2) sign user out then back in
3) firebase_user_stream package
Any help is appreciated.
I have implemented the same scenario in a splash screen with below code, you can change it as per your requirement. :
//To check is User is logged in
Future<bool> isLoggedIn() async {
FirebaseUser user = await _fireBaseAuth.currentUser();
if (user == null) {
return false;
}
return user.isEmailVerified;
}
and
countDownTime() async {
return Timer(
Duration(seconds: splashDuration),
() async {
if (await userAuth.isLoggedIn()) {
Navigator.pushReplacement(
context,
ScaleRoute(
widget: HomeScreen(),),
);
}
} else {
Navigator.pushReplacement(
context,
ScaleRoute(
widget: LoginScreen(),),
);
}
},
);
}
and
#override
void initState() {
super.initState();
countDownTime();
}
Update
One needs to implement isEmailVerified in initState() function periodically which can be the ideal approach to execute the verification with firebase.
bool _isUserEmailVerified;
Timer _timer;
#override
void initState() {
super.initState();
// ... any code here ...
Future(() async {
_timer = Timer.periodic(Duration(seconds: 10), (timer) async {
await FirebaseAuth.instance.currentUser()..reload();
var user = await FirebaseAuth.instance.currentUser();
if (user.isEmailVerified) {
setState((){
_isUserEmailVerified = user.isEmailVerified;
});
timer.cancel();
}
});
});
}
#override
void dispose() {
super.dispose();
if (_timer != null) {
_timer.cancel();
}
}

Resources