How to auto-login Firebase users at the start of an app? - firebase

I am trying to implement a Home class which shows a Login screen for new users or auto-logins previously signed in users and brings them to directly to the app. However, when the code runs it shows login screens for signed in users.
From my understanding, the Future function is initially returning a null and the code finishes with "Login Page" being shown even though the Future later returns the current User.
class Home extends StatelessWidget {
FirebaseAuth _auth = FirebaseAuth.instance;
Future<FirebaseUser> getCurrentUser() async {
return _auth.currentUser();
}
Widget userLoggedIn() {
getCurrentUser().then((user) {
if (user != null) {
//User is auto-logged in = build main app
return new Scaffold(
body: Center(
child: Text("Main App"),
),
);
} else if (user == null) {
//New user = return null
return null;
});
}
#override
Widget build(BuildContext context) {
return userLoggedIn() ??
//New user = build login page
new Scaffold(
body: Center(
child: Text("Login Page"),
),
);
}
}
Could you please help me fix this?

You can start the auto-login progress in your LoginPage's initState,
LoginPage must be StatefulWidget to use initState
#override
void initState() {
FirebaseAuth.instance.currentUser().then((user) {
if (user != null) { //if there isn't any user currentUser function returns a null so we should check this case.
Navigator.pushAndRemoveUntil(
// we are making YourHomePage widget the root if there is a user.
context,
MaterialPageRoute(builder: (context) => YourHomePage()),
(Route<dynamic> route) => false);
}
});
super.initState();
}
signOut with that method:
FirebaseAuth.instance.signOut()

You need to wait until _auth.currentUser() returns the user.
Use await keyword and not .then().
Change your getCurrentUser() method.
Return a FirebaseUser and not a Future.
FirebaseUser currentUser = await authService.getCurrentUser();

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()
);
},
);

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();
}
}

Persist user Auth Flutter Firebase

I am using Firebase Auth with google sign in Flutter. I am able to sign in however when I close the app(kill it), I have to sign up all over again. So is there a way to persist user authentication till specifically logged out by the user?
Here is my auth class
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
class Auth {
FirebaseAuth _firebaseAuth;
FirebaseUser _user;
Auth() {
this._firebaseAuth = FirebaseAuth.instance;
}
Future<bool> isLoggedIn() async {
this._user = await _firebaseAuth.currentUser();
if (this._user == null) {
return false;
}
return true;
}
Future<bool> authenticateWithGoogle() async {
final googleSignIn = GoogleSignIn();
final GoogleSignInAccount googleUser = await googleSignIn.signIn();
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
this._user = await _firebaseAuth.signInWithGoogle(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
if (this._user == null) {
return false;
}
return true;
// do something with signed-in user
}
}
Here is my start page where the auth check is called.
import 'package:flutter/material.dart';
import 'auth.dart';
import 'login_screen.dart';
import 'chat_screen.dart';
class Splash extends StatefulWidget {
#override
_Splash createState() => _Splash();
}
class _Splash extends State<Splash> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CircularProgressIndicator(
value: null,
),
),
);
}
#override
void initState() {
super.initState();
_handleStartScreen();
}
Future<void> _handleStartScreen() async {
Auth _auth = Auth();
if (await _auth.isLoggedIn()) {
Navigator.of(context).pushReplacementNamed("/chat");
}
Navigator.pushReplacement(context, MaterialPageRoute(builder: (BuildContext context) => LoginScreen(auth: _auth,)));
}
}
I believe your problem is routing. In my apps I use FirebaseAuth and it works just as you say you wanted to, and I don't persist any login token. However, I don't know why your approach of using a getUser is not working.
Try to adjust your code to use onAuthStateChanged. EDIT: As of 2022, with Flutter 3, I noticed it worked better with userChanges instead.
Basically, on your MaterialApp, create a StreamBuilder listening to _auth.userChanges() and choose your page depending on the Auth status.
I'll copy and paste parts of my app so you can have an idea:
[...]
final FirebaseAuth _auth = FirebaseAuth.instance;
Future<void> main() async {
FirebaseApp.configure(
name: '...',
options:
Platform.isIOS
? const FirebaseOptions(...)
: const FirebaseOptions(...),
);
[...]
runApp(new MaterialApp(
title: '...',
home: await getLandingPage(),
theme: ThemeData(...),
));
}
Future<Widget> getLandingPage() async {
return StreamBuilder<FirebaseUser>(
stream: _auth.userChanges(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasData && (!snapshot.data!.isAnonymous)) {
return HomePage();
}
return AccountLoginPage();
},
);
}
Sorry, it was my mistake. Forgot to put the push login screen in else.
Future<void> _handleStartScreen() async {
Auth _auth = Auth();
if (await _auth.isLoggedIn()) {
Navigator.of(context).pushReplacementNamed("/chat");
}
else {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (BuildContext context) => LoginScreen(auth: _auth,)));
}
}
void main() {
FirebaseAuth.instance.authStateChanges().listen((User user) {
if (user == null) {
runApp(MyApp(auth : false);
} else {
runApp(MyApp(auth : false);
}
});
}
class MyApp extends StatefulWidget {
final bool auth;
MyApp({this.auth});
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
return MaterialApp(
......
......
home: widget.auth ? MainScreen() : AuthScreen();
);
You can use shared_preferences to keep alive your session even when you kill the app.
Here is the documentation https://pub.dartlang.org/packages/shared_preferences.
Also I've heard that it's possible to use sqlite to persist the session.
Add this code. It should work fine.
FirebaseAuth auth = FirebaseAuth.instance;
auth.setPersistence(Persistence.SESSION);
You can use my code, You can use userChanges() instead of authStateChanges()
Notifies about changes to any user updates.
This is a superset of both [authStateChanges] and [idTokenChanges]. It provides events on all user changes, such as when credentials are linked, unlinked and when updates to the user profile are made. The purpose of this Stream is for listening to realtime updates to the user state (signed-in, signed-out, different user & token refresh) without manually having to call [reload] and then rehydrating changes to your application.
final Stream<User?> firebaseUserChanges = firebaseAuth.userChanges();
One more simple example:
Future<bool> isUserLoggedIn() async {
final User? user = FirebaseAuth.instance.currentUser;
return user != null;
}
class InitialScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<bool>(
future: isUserLoggedIn(),
builder: (_, snapshot) {
if (snapshot.hasData) {
if (snapshot.data ?? false) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => UnauthScreen()));
} else {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => HomeScreen()));
}
}
return const Center(child: CircularProgressIndicator());
},
),
);
}
}
I was able to achieve it by checking the firebase instance currentUser value. if null I routed to my Signup page. If not, then I routed to my HomePage. Not sure if there is anything wrong with this implementation (its working well so far) but seems simpler than the StreamBuilder solution posted above.
home: getLandingPage(),
routes: {
(...)
}
Widget getLandingPage() {
if (_auth.currentUser == null) {
return SignupPage();
} else {
return HomePage();
}
}

Flutter: Question about async function to retrieve Firebase user ID

App Flowchart
I have a question about async function in flutter. I write an that use Firebase authentication. I want to make it such that the app will read the Firebase User ID at the top level of the app(Root Page in this case) at the init state function and then pass the user object to its child widget. Since the function to retrieve the user ID is an async function, I run into problem that the child widget get a null value for user ID even though it should not be null. I have already use future builder in the children widget but it doesn't work. Does anyone know how to do it correctly.
The exact error I am getting is "A build function returned null. The offending widget is: FutureBuilder. Build functions must never return null."
RootPage (Parent)
class _RootPageState extends State {
AuthStatus authStatus = AuthStatus.notSignIn;
String cuerrentUserId;
#override
void initState() {
super.initState();
widget.auth.currentUser().then((userId) {
setState(() {
authStatus = userId == null ? AuthStatus.notSignIn : AuthStatus.signIn;
cuerrentUserId = userId;
});
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new FutureBuilder<FirebaseUser>(
future: FirebaseAuth.instance.currentUser(),
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot) {
switch(authStatus) {
case AuthStatus.notSignIn:
return new LoginPage(
auth: new Auth(),
onSignedIn: _signedIn,
);
case AuthStatus.signIn:
if (snapshot.connectionState == ConnectionState.done) {
return new HomePage(
auth: widget.auth,
onSignedOut: _signedOut,
userId: snapshot.data.uid,
);
}
else {
}
}
}
),
);
}
HomePage (child)
Future<String> setUserData() async {
currentUser = User(widget.userId);
await currentUser.loadUserData();
_userName = currentUser.name;
_userEmail = currentUser.email;
_userPicURL = currentUser.avatar;
print('current user');
print(currentUser.id);
print(currentUser.email);
return _userName;
}
#override
Widget build(BuildContext context) {
return UserProvider(
user: currentUser,
child: new Container(
child: new FutureBuilder(
future: setUserData(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.data!=null) {
...
You could make your main function async in order to decide during app launch if you should show the login or home page as the first screen.
This could look like the following:
Future<void> main() async {
FirebaseUser currentUser = await FirebaseAuth.instance.currentUser();
bool showHomePage = currentUser != null;
runApp(MyApp(showHomePage));
}
You could use the showHomePage param inside MyApp now to determine which screen should be shown initially. That's it.
Bonus: With this approach you also don't need to show a screen for a friction of a second which may be replaced by another one (e.g. show the home screen --> user is not logged in --> replace with login screen). This could look like a glitch in your app.

How to Signout a user in Flutter with Firebase authentication

I have a problem signing out the current user from my app
the method I am using is as follows:
....
onPressed:_signOut
//jump to function
void _signOut() {
FirebaseAuth.instance.signOut();
FirebaseUser user = FirebaseAuth.instance.currentUser;
//print('$user');
runApp(
new MaterialApp(
home: new LoginPage(),
)
);
}
So now when I press the button it should sign the user out and redirect them to the home page which they will have to login again, however, the redirecting happens but the user data would still be saved so when I press the button again it automatically sign in again with the last account. How can I remove user data so the app asks about their credentials each time they try to login after a logout ?
I feel I am missing something in the linkage between pages and how their behavior change accordingly, but what is it ?
Update: I use google sign in function with firebase authentication
Future<String> _testSignInWithGoogle() async {
final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
final FirebaseUser user = await _auth.signInWithGoogle(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
assert(user.email != null);
assert(user.displayName != null);
assert(!user.isAnonymous);
assert(await user.getToken() != null);
return 'signInWithGoogle succeeded: $user';
}
My login page looks like this:
class LoginPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Login"), backgroundColor: Colors.blue,),
body: new Container(
child: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new IconButton(
icon: new Icon(Icons.account_box, color: Colors.red),
onPressed: _signIn,
iconSize: 80.0,),
new Text("Google Signin")
],
)
)
)
);
}
}
Update: Changed _signOut() method to be async as follows:
Future <LoginPage> _signOut() async{
await FirebaseAuth.instance.signOut();
return new LoginPage();
}
Now when I press on signout, it does not redirect me to the LoginPagae nor does it sign the user out.
Firebase auth's signOut method is asynchronous. You should make your _signOut method async.
Future<void> _signOut() async {
await FirebaseAuth.instance.signOut();
}
so that the call to runApp occurs after the user is signed out.
You should also call _googleSignIn.signOut() when logging out if you want signIn to present the user with an authentication dialog instead of silently and automatically re-using the current Google user.
You need to have an Instances of FirebaseAuth
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
And Then
_signOut() async {
await _firebaseAuth.signOut();
}
Most demos I've looked at only logout of Firebase using a command like _auth.signOut();
This doesn't appear to exist anymore (see Collin's reply above):
_googleSignIn.signOut()
So, I had to use this one method to signout/logout of Google.
_googleSignIn.disconnect();
First create an instance of FirebaseAuth like so
FirebaseAuth auth = FirebaseAuth.instance;
Then add this to either your logout button or any means you wish to use for the logout.
signOut() async {
await auth.signOut();
}
You can also create a function and then call the signOut within your button like so
import 'package:flutter/material.dart';
class SignOut extends StatefulWidget {
#override
_ SignOut State createState() => _ SignOut State();
}
class _ SignOut State extends State< SignOut > {
FirebaseAuth auth = FirebaseAuth.instance;
signOut() async {
await _firebaseAuth.signOut();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Container(
child: RaisedButton(
onPressed: () {
signOut();
},
)
),
),
);
}
}
You decide.
If every answer above is not working, maybe a Page is laying above everything else, then use Navigator.of(context).pop(); to pop the page.
I've searched hours looking for a mistake that wasn't even there just for this small mistake.
you should make your signout function asynchronous
Future<void> _signOut() async {
await FirebaseAuth.instance.signOut();
}
Declare a Future method to call logout function and redirect your page when logout is finished:
Future logout() async {
await _firebaseAuth.signOut().then((value) => Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (context) => LoginPage()),(route) => false)));
}

Resources