How can you manually trigger onAuthStateChanged for Firebase Authentication in Flutter? - firebase

I have a an app built around a StreamBuilder listening for FirebaseAuth.instance.onAuthStateChanged
return new StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { ...
I'm trying to reload the user and use onAuthStateChanged to pick up when an email is verified. User.reload() is insufficient with the StreamBuilder. It's a similar issue to this onAuthStateChanged doesn't get called when email is verified in flutter
I thought a solution would be to force a logout and login but I'm at a loss on how to do that - e.g. step 2, login again to trigger onAuthStateChanged.
_auth.signOut(); //sign out and immediate sign in
final FirebaseUser user = await _auth.signIn----( //which signin function?

Have you tried refreshing auth token?
https://firebase.google.com/docs/auth/admin/manage-sessions

Use _auth.userChanges() stream instead of _auth.onAuthStateChanges()
return StreamBuilder(
stream: _auth.userChanges(),
builder: (ctx, userSnapshot) {
User? user = _auth.currentUser;
if (userSnapshot.hasData && user != null && user.emailVerified) {
// load screen on successful login
}
// fallback to auth screen
},
);
and on successful verification call refreshToken in your verification method
Future<void> verifyEmail() async {
user = _auth.currentUser;
await user!.reload();
if (user!.emailVerified) {
setState(() {
_isEmailBeingVerified = false;
});
user!.refreshToken;
}
}

Related

Flutter/Firebase problem with logout after email user verification

i'am building an app with flutter and firebase..(I'am new in flutter development). What I did is a system where the user can signup and signin. Once the user signup an email verification is sent to the user email account. I'll try to put all the step below
signup
redirect to email verification widget..(this check if user has verified the email with a Timer) if yes, navigator push new Page (HomePage).
the main logic is this one.
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "My App Name",
debugShowCheckedModeBanner: false,
home: AuthService().handleAuth(),
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity
),
);
}}
this is what the AuthService().handleAuth() does:
handleAuth() {
return StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
print(snapshot.hasData);
if (snapshot.hasData && emailVerificationNeeded() == false) {
return HomePage();
}
return LoginPage();
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
},
);
}
The verifyPage check with a timer if user has verified the email
Future<void> checkEmailVerified() async {
user = auth.currentUser!;
await user.reload();
if (user.emailVerified) {
timer.cancel();
Navigator.of(context)
.pushReplacement(MaterialPageRoute(builder: (context) => HomePage()));
}
}
Until here everything work fine! Now for a test purpose in the HomePage there is a button that fire the following action
FirebaseAuth.instance.signOut();
if I click in the button and I logout the user still remain in the home page instead of going back to the LoginPage. This problem happen only the first time when the user is redirected in the Verification page. On all the other case if I'm verified and I'm logged in, once I click on the Logout button the user is redirect back to the Login page.
Any ideas?
Thank you all
The StreamBuilder isn't reacting to changes because you've navigated to another route.
You can use FirebaseAuth.instance.userChanges instead of FirebaseAuth.instance.authChanges as authChanges only "notifies about changes to the user's sign-in state (such as sign-in or sign-out)." while userChanges listens to different states such as sign-in, sign-out, different user & token refresh.
And then you can call FirebaseAuth.instance.currentUser!.reload(); when you verify the user which removes the need to navigate to the HomePage manually.
So when you call FirebaseAuth.instance.signOut, it automatically signs you out.
Code Changes:
Update handleAuth to this:
handleAuth() {
return StreamBuilder(
stream: FirebaseAuth.instance.userChanges(),
...
);
}
Update checkEmailVerified to this:
Future<void> checkEmailVerified() async {
user = auth.currentUser!;
await user.reload();
if (user.emailVerified) {
timer.cancel();
}
}

Firebase sign out is not working in flutter application

I am implementing the the firebase sign out in flutter application based on the provider id which is not working at all right now.
I am executing the below code.
FirebaseAuth _auth = FirebaseAuth.instance;
if(_auth.currentUser != null) {
User user = _auth.currentUser;
var userset = user.providerData[0].providerId;
if(userset == 'google.com'){
print('google provider');
await GoogleSignIn().signOut();
//Firebase sign out navigation to the login page
} else {
_auth.signOut();
//Firebase sign out navigation to the login page
print('sign out done successfully');
}
}
The code is executed, but the auth state maintains the user logged in status, whether sign in or sign out and upon reloading the app it does not go to the login page and it goes to the home page.
How should I check whether the sign out was successful or not?
Auth State handling in app when the app reloads
Widget _handleAuth() {
return StreamBuilder<User>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, snapshot) {
return (!snapshot.hasData)
? LoginPage()
: HomePage();
},
);
}
You need to also sign out from firebase even if you are using google provider:
if(userset == 'google.com'){
print('google provider');
await GoogleSignIn().signOut();
_auth.signOut();
}

Flutter Firebase check if user is signed in anonymously

I am giving Users the option to sign in via Email and Password or anonymously.
Creating and signing in the User with Email and Password works fine, but I am having problems with displaying different contents for anonymously signed in users.
This is how I create them:
final _auth = FirebaseAuth.instance;
// Create Anonymous User
Future signInAnonymously() {
// return _auth.signInAnonymously();
_auth.signInAnonymously();
}
If a user chooses to get into the app anonymously in the Auth Form, I trigger following function:
Future submitAnon() async {
await signInAnonymously();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomeScreen(),
),
);
}
I am using a Future Builder / Stream to listen to Auth Changes in my main.dart:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
return FutureBuilder(
// Initialize FlutterFire:
future: _initialization,
builder: (context, appSnapshot) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'myapp',
home: appSnapshot.connectionState != ConnectionState.done
? SplashScreen()
: StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (ctx, userSnapshot) {
if (userSnapshot.connectionState ==
ConnectionState.waiting) {
return SplashScreen();
}
if (userSnapshot.hasData) {
return HomeScreen();
}
return AuthScreen();
}),
);
});
}
}
What I tried was passing a "isAnon=true" boolean to HomeScreen in submitAnon() but once the user re-opens the app, he is getting into the app from main.dart, where all the users that signed-up via email also get in. Is there a good way to check inside the App if the User is authenticated anonymously to build my widgets depending on that? e.g. showing Authentication Options instead of actual content that is only directed to e-mail&passsword users?
I think it's too late but here's the simplest way:
FirebaseAuth.instance.currentUser!.isAnonymous // returns a bool
To check if a user signed in to firebase anonymously. Firebase User class returned has an isAnonymous() method that returns true if the user signs in anonymously.
In your code, you can get a firebase user from the streambuilder snapshot. and check if isAnonymous is true. Like below:
StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (ctx, userSnapshot)
{
User user = userSnapshot.data;
print('Is user anonymous: ${user.isAnonymous}');
},
);
should print
'Is user anonymous: true'
if the user signs in anonymously and userSnapshot is not null.
I would propose you use the SharedPreferences library in order to keep track of of whether the user is anonymous or not. You should simply be able to store the isAnon in Shared Preferences to track whether the user is anonymous or not and from there you could use the Provider library to retrieve the isAnon value throughout the app. With a combinations of these two things you should be able to create the behavior you are looking for.
Please let me know if I misunderstood your question or need a further explanation!

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

Flutter Firebase - Stream to listen if user has been disabled / deleted

Below is my simple streambuilder code to listen for auth event changes using AuthStateListener when the user logs in/out.
Essentially would to have the same realtime update when the firebase user has been disabled / deleted in the firebase console so that the user can be logged off from the app.
return StreamBuilder<String>(
stream: auth.onAuthStateChanged,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
final bool isLoggedIn = snapshot.hasData;
if (isLoggedIn) {
return MainPage();
} else {
return LoginPage();

Resources