I get the idea of automatically navigating based on if the user has data or not, but what I'm facing a problem with is how to check if the user has verified their email and based on that navigate to the new route.
note: i tried using "FirebaseAuth.instance.currentUser.emailVerified == condition" but it didnt work
class WrapperView extends GetView<WrapperController> {
const WrapperView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: ((BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
Get.log('has data');
return ProfileView();
} else {
Get.log('has no data');
return SplashView();
}
}),
);
}
}
Related
Firebase Flutter authStateChanges snapshot.data always return true,
Even i deleted the user from Firebase Authentication.
I readed some article that they said Firebase still store the token and will refresh it after 1 Hours,
But i wait 6 Hours the authStateChanges snapshot still returning true
Is that any wrong with my code or my Stream Builder?
Or how can i make a private route in Flutter to see that client is logged in or not,
Like if they not logged in they will redirected to Login page, and if they logged in they will redirected to Homepage etc
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.hasData) {
print('Snapshot => $snapshot');
return HomeScreen();
} else {
print('Not Logged In!!!');
return GetStartedScreen();
}
} else {
return Text('Loading...');
}
},
),
);
}
}
To be honest, I don't know exactly the answer to your specific problem, but I can advice you to refactor the builder part like:
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: Text("Is Loading...");
}
else if (snapshot.hasError){
return Center(child: Text("Error: ${snapshot.error}");
}
else if (snapshot.hasData) {
print('Snapshot => $snapshot');
return HomeScreen();
} else {
print('Not Logged In!!!');
return GetStartedScreen();
}
}
},
Listen:False
I'm trying to stop rebuild LandingUser Widget but it doesn't work wiht listen : false . And if it keep it rebuilding I faces another issue I explained it // in the code below .
LandingUser class
class LandingUser extends StatelessWidget {
const LandingUser({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
print("Landing User Rebuild ----------");
final auth = Provider.of<AuthService?>(context , listen: false); // false not working
print(auth);
return StreamBuilder<MyUser?>(
stream: auth!.onChangeStateUser,
builder: (context, snapshot) {
print(snapshot.hasData);
print(snapshot);
if (snapshot.connectionState == ConnectionState.active) {
MyUser? user = snapshot.data;
print("user $user");
if (user == null) {
return Scaffold(body: Text('test login rebuild'),); // LogingScreen();
} else {
return Scaffold(body: Text('test Not null User rebuild'),); // UserProfile();
}
}else{
//if(user==null){return LoginScreen();} //without if .. CircularProgressIndicator don't stop because I used in Login Screen page TextFormFeild and when foucse the widget rebuild itself and start loading again cuz the widget rebuilt .. this is another issue _!_
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
},
);
}
}
Root widget main :
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
print("Main rebulided");
return Provider<AuthService?>.value(
value: AuthService(),
child: MaterialApp(
//.......
),
);
}
}
auth!.onChangeStateUser Stream getter:
#override
Stream<MyUser?>? get onChangeStateUser => _auth.authStateChanges().map((event) => _userFromFirebase(event));
_userFromFirebase Private Func :
MyUser? _userFromFirebase(User? user) {
if (user == null) {
return null;
}
return MyUser(uid: user.uid);
}
Thanks in advance
Provider.of<AuthService?>(context , listen: false);
The above code means the widget it's being used in will not rebuilt when the value of AuthService changes.
It does not mean your widget will not be rebuilt when something else like the auth!.onChangeStateUser stream is being used in a StreamBuilder.
So while the value of AuthService isn't changing, the stream from the instance of AuthService is changing and that is what is causing the rebuilds.
Intended Flow: Only show the home page when the user verifies their email.
What is actually happening: The user can pop the navigation stack to show the home page.
I'm using Firebase to handle my authentication flow and currently am using a StreamBuilder to show either the landing page or the home page at the root of the app depending on if a user is verified and not null.
//create user object based on Firebase User
UserApp _userFromFirebaseUser(User user) {
return user != null ? UserApp(uid: user.uid, isVerified: user.emailVerified):null;
}
//userapp type class
class UserApp {
final String uid;
final bool isVerified;
UserApp({this.isVerified, this.uid});
}
Then I have a StreamBuilder that lives on top of my main.dart file:
class AuthWidgetBuilder extends StatelessWidget {
const AuthWidgetBuilder({Key key, #required this.builder}) : super(key: key);
final Widget Function(BuildContext, AsyncSnapshot<UserApp>) builder;
#override
Widget build(BuildContext context) {
final authService = Provider.of<AuthService>(context, listen: false);
return StreamBuilder<UserApp>(
stream: authService.onAuthStateChanged,
builder: (context, snapshot) {
final UserApp user = snapshot.data;
//only show home page if user is non null and also if they verified their email.
if (user != null && authService.currentUser().isVerified == true) {
return MultiProvider(
providers: [
Provider<UserApp>.value(value: user),
],
child: builder(context, snapshot),
);
}
return builder(context, snapshot);
});
}
}
class AuthWidget extends StatelessWidget {
const AuthWidget({Key key, #required this.userSnapshot}) : super(key: key);
final AsyncSnapshot<UserApp> userSnapshot;
#override
Widget build(BuildContext context) {
if (userSnapshot.connectionState == ConnectionState.active) {
return userSnapshot.hasData ? HomePage() : LandingPage();
}
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
Then this is my main.dart file with the StreamBuilder and AuthService class on top of the widget tree:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Provider<AuthService>(
create: (_) => AuthService(),
child: AuthWidgetBuilder(builder: (context, userSnapshot) {
return MaterialApp(
home: AuthWidget(userSnapshot: userSnapshot),
);
}));
}
How come I can pop the navigation stack to reveal the home page, even when I have not yet verified the user through email? What do I need to change in my code to make sure that the user can only see the home page, after verifying their email? Is there any resource I can look at to understand these concepts better?
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();
}
I am getting this exception while running my app:
type 'FirebaseUser' is not a subtype of type 'String'
The app is working. It does the phone authentication and then move to the next page based on same given condition. But when i hit the back button it throws the exception also in the app it is visible.
This is my code.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'AccountDetialsPage.dart';
import 'Homepage.dart';
import 'Phoneverification.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final Firestore firestore = Firestore.instance;
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: FutureBuilder(
future: getCurrentUser(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return Text("Loading...");
}
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, snapshot) {
if (snapshot.hasData){
return StreamBuilder(
stream: firestore
.collection('users')
.document(snapshot.data)
.snapshots(),
builder: (context, snapshot){
if (snapshot.hasData){
if (snapshot.data["verified"] == true){
return HomePage();
}
else{
return AccountDetialsPage();
}
}
else{
return Text('Loading StreamBuilder');
}
}
);
}
else{
return Phoneverification();
}
},
);
}),
);
}
Future<String> getCurrentUser() async {
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseUser user = await _auth.currentUser();
final uid = user.uid;
var number = user.phoneNumber;
return number.toString();
}
}
enter image description here
In your StreamBuilder you set the future to:
stream: FirebaseAuth.instance.onAuthStateChanged,
And then use that in the builder to get data from Firestore with:
stream: firestore
.collection('users')
.document(snapshot.data)
.snapshots(),
The onAuthStateChanged listener is invoked with a FirebaseUser, so snapshot.data in here is a FirebaseUser. Since document(...) expects a string, that's what you get an error about.
Most likely you're looking to use the UID of the user to look up the document, in which case you'd do:
stream: firestore
.collection('users')
.document(snapshot.data.uid)
.snapshots(),
In general your code looks confusing though: in the FutureBuilder you have a future for the current user, and then in your stream builder you also use a listener for that same user.
My feeling is that you're actually trying to use a FutureBuilder for the user, and then a StreamBuilder for the data from Firestore. If that is the case, you're probably looking for something more like:
home: FutureBuilder(
future: getCurrentUser(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return Text("Loading...");
}
return StreamBuilder(
stream: firestore
.collection('users')
.document(snapshot.data)
.snapshots(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasData){
if (snapshot.data["verified"] == true){
return HomePage();
}
else{
return AccountDetialsPage();
}
}
}
},
);
}),
There might be typos in the above, but the important thing is that the first stream here contains the snapshots from Firestore based on the future: getCurrentUser().