I am trying to use StreamProvider to get my app to display the Features widget when a user is signed in. Here is my code:
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'auth/auth.dart';
import 'features/features.dart';
void main() => runApp(
StreamProvider<FirebaseUser>(
create: (context) => FirebaseAuth.instance.onAuthStateChanged,
child: MaterialApp(home: MyApp()),
updateShouldNotify: (_, __) => true),
);
class MyApp extends StatelessWidget {
static reload(BuildContext context) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => MyApp()),
(Route<dynamic> route) => false,
);
}
#override
Widget build(BuildContext context) {
var _user = Provider.of<FirebaseUser>(context);
print('USER ISSUED: ' + _user.toString());
if (_user == null) return SignInPage();
if (_user != null) return Features();
}
}
I had to create the reload() method, which is called by the various authentication widgets (google, facebook, password) nested below SignInPage to update display of MyApp. I would have expected that Provider.of<FirebaseUser> would rebuild my app automatically, without need to call reload(). The Provider does work. Whenever FirebaseAuth updates the user, it prints USER ISSUED: some_user.... Why does it not execute the next two lines of code to return either my SignInPage when user == null or the Features page when a user is logged in successfully?
I'm new to FlutterFire, but coming from AngularFire, I would have expected this to work smoothly.
I think you should be using:
// (firebase_auth 0.9.0)
StreamProvider.value(
value: FirebaseAuth.instance.onAuthStateChanged,
child: MaterialApp(home: MyApp()),
),
because as per the docs StreamProvider.value listens to value and expose it to all of StreamProvider descendants.
However, using StreamBuilder shall be preferred here as you might want to check if connection is established or not, else everytime the app opens it may show a glimpse of the login screen and then automatically go to the home screen in moderate network conditions, if user authentication is cached.
// (fireabse_auth 0.18.4)
StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (_, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Loading();
}
if (snapshot.data is User && snapshot.data != null) {
return Home();
}
return Authenticate();
})
Also,
I would have expected that Provider.of would rebuild my app automatically
It's StreamProvider that is responsible for listening to the stream, exposing it to the descendants and updating the consumer's state, and not Provider.of<T>(context). Provider.of<T>(context) just attempts to access the data of type T from the nearest parent provider of T.
Related
I have implemented Google sign in to my mobile application, however, it asked me once for my e-mail and password. When I run the app again it skips the login screen and automatically navigates to the home screen.
I tried:
Deleting the app on the menu.
Clearing the cache on settings. (I guess I couldn't do it properly not for sure)
Even deleted the profile which automatically logs in. (still holds this profile as user idk how...)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import './components/google_sign_in.dart';
import 'components/body.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../home/home_screen.dart';
class SignInScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ChangeNotifierProvider(
create: (context) => GoogleSignInProvider(),
child: StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
final provider = Provider.of<GoogleSignInProvider>(context);
if (provider.isSigningIn) {
return buildLodading();
} else if (snapshot.hasData) {
print("data: ${snapshot.data}");
return HomeScreen();
} else {
return Body();
}
})));
}
Widget buildLodading() => Center(child: CircularProgressIndicator());
}
Body() = The login screen.
The program always returns true on snapshot.hasData so that it doesn't go in else.
What you're describing is the expected behavior. When you restart the app, Firebase implicitly tries to restore the user credentials, so that the user doesn't have to sign in each time they start the app.
If you want the user to explicitly require the user to provide their sign-in credentials each time they start the app, sign any existing user out when the app starts.
For example:
void main() {
WidgetsFlutterBinding.ensureInitialized();
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
FirebaseAuth.instance.signOut();
runApp(App());
}
the whole purpose of this is that i want to signout the current user using app, immediately when i open firebase console and delete his account from Authentication tab.
i want the signout process to be done smoothly without any errors.
what i've tried so far:
in my main function():
runApp(MyApp());
and this is myApp class:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<AuthService>(
create: (_) => AuthService(),
),
StreamProvider(
create: (context) => context.read<AuthService>().onAuthStateChanged,
),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
routes: <String, WidgetBuilder>{
'/home': (BuildContext context) => HomeController(),
'/signUp': (BuildContext context) => SignUpView(
authFormType: AuthFormType.signUp,
),
'/signIn': (BuildContext context) => SignUpView(
authFormType: AuthFormType.signIn,
),
'/addGig': (BuildContext context) => Home(passedSelectedIndex: 1),
},
home: HomeController(),
));
}
}
and this is the HomeController():
class _HomeControllerState extends State<HomeController> {
AuthService authService = locator.get<AuthService>();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
final firebaseUserUid = context.watch<String>();
if (firebaseUserUid != null) {
return HomePage();
return MaterialApp(
debugShowCheckedModeBanner: false,
routes: <String, WidgetBuilder>{
'/home': (BuildContext context) => HomeController(),
'/signUp': (BuildContext context) => SignUpView(
authFormType: AuthFormType.signUp,
),
'/signIn': (BuildContext context) => SignUpView(
authFormType: AuthFormType.signIn,
),
'/addGig': (BuildContext context) => Home(passedSelectedIndex: 1),
},
theme: fyreworkTheme(),
builder: EasyLoading.init(),
home: Home(passedSelectedIndex: 0),
);
} else {
return StartPage();
}
}
}
in the build function of MyApp class, the provider is listening to this stream from the AuthService class:
Stream<String> get onAuthStateChanged => _firebaseAuth.onAuthStateChanged.map(
(FirebaseUser user) => user?.uid,
);
so far so good...when i start or restart the App...every thing works as intended...no probs.
what i wanted to achieve is that if i open the firebase console / Authentication tab and i know the identifier of a specific user i want to delete his account and i delete it.
i thought that would signout that user from the app and navigates him to StartPage()..as the whole app is listening to onAuthStateChanged stream from the Provider.
but that didn't achieve what i was trying to do.
how can i sign out a specific user from the App after i delete his data from Authentication tab in firebase console ?
i hope i've described the problem and the desired goal well....
any help would be much appreciated.
Whenever the users sends any request to the firebase, revalidate the user to ensure that the user exists in the database or not. You may only if the user exists in the tables or not (not check validity of the tokens). Incase it does not redirect the user to login page. If it does then the user must have authenticated successfully before. This is a very simple solution.
For a complex one, you could use triggers in firebase and send a push notification to the app on deletion of the record of the user. Such that whenever the app receives the push notification that tells it to reauthenticate, the app should assume that the authentication data has been deleted or moved and would redirect the user to the login screen.
As an alternative approach, you could use firebase functions to trigger on deleteUser to delete the data server-side... this has the benefit of smaller-client, plus reduces the risk of some 'stuck' conditions should either the data or user delete fail.
exports.onAuthUserDelete = functions.auth.user().onDelete(onAuthUserDelete);
async function onAuthUserDelete(user: admin.auth.UserRecord) {
await admin.firestore().doc(`/users/${user.uid}`).delete();
functions.logger.info(`User firestore record deleted: uid=${user.uid}, displayName=${user.displayName}`);
}
I am trying to persist the firebase auth state in a flutter app by using this code from the documentation but when I kill the app in the emulator and open it again it doesn't recognize a user.
I can use sharedpreferences but I want to use only firebase, what am I doing wrong here?
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// Create the initialization Future outside of `build`:
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
final FirebaseAuth auth = FirebaseAuth.instance;
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return FutureBuilder(
// Initialize FlutterFire:
future: _initialization,
builder: (context, snapshot) {
// Check for errors
if (snapshot.hasError) {
return (MaterialApp(
home: Warning(
warning: 'Error',
),
));
}
// once complete show your app
if (snapshot.connectionState == ConnectionState.done) {
print('CONNECTED');
if (AuthService().user() == null) {
return MaterialApp(
home: LoginPage(),
);
} else {
return MaterialApp(
home: HomePage(),
);
}
}
// if nothing happens return loading
return MaterialApp(
home: //LoginPage()
Warning(
warning: 'Loading',
),
);
},
);
}
}
AuthService class
import 'package:firebase_auth/firebase_auth.dart';
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
// auth change user stream
User user() {
// ignore: deprecated_member_use
_auth.authStateChanges().listen((User user) {
if (user == null) {
return null;
} else {
return user;
}
});
}
}
I hope you can help me to understand the problem and solve it, thank you.
Since authStateChanges returns a Stream, you'll want to use a StreamBuilder in your code to wrap that asynchronous operation too.
Something like this:
// once complete show your app
if (snapshot.connectionState == ConnectionState.done) {
print('CONNECTED');
return StreamBuilder(
stream: FirebaseAuth.instance. authStateChanges(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
return MaterialApp(
home: LoginPage(),
);
} else {
return MaterialApp(
home: HomePage(),
);
}
}
)
}
Unrelated: you're repeated the code to create a MaterialApp quite frequently, which is not needed. For example, in the above snippet we could have only one mention of MaterialApp and get the same result with:
// once complete show your app
if (snapshot.connectionState == ConnectionState.done) {
print('CONNECTED');
return StreamBuilder(
stream: FirebaseAuth.instance. authStateChanges(),
builder: (BuildContext context, snapshot) {
return MaterialApp(
home: snapshot.hasData && snapshot.data != null ? HomePage() : LoginPage(),
)
}
)
}
If you do this for all mentions of MaterialApp and other duplication, you can reduce the code significantly, making it less error prone and easier to maintain.
It does persist. You are just not using the auth state listener correctly. The return statements from your authStateChanges listener are not actually escaping the call to user(). On top of that, the listener could return null the first time. It's not until some time later that the Firebase SDK determines that the user is actually valid and signed in. Your listener will get a second callback at that time. Your code need to be ready for this to happen - it can't just blindly take the first value, as the auth state might change over time.
I suggest adding some debug logging in your auth state listener to see how this actually works. Also I suggest reading this blog to understand how auth state listeners work in more detail.
You can use my code, You can use userChanges() instead of authStateChanges()
final Stream<User?> firebaseUserChanges = firebaseAuth.userChanges();
Flutter & Firebase : Why does the login page always appear briefly before going to home screen every time I restart my app even though the user is already logged in?
I understand that my app needs sometimes to render the user from firebase. But the user is already logged anyway. So how can I proceed the home screen immediately since the user is already logged in? Is there a way to save the user data into the phone memory?
class _WrapperState extends State<Wrapper> {
#override
Widget build(BuildContext context) {
final user = Provider.of<SystemUser>(context);
if (user == null) {
print('In Authenicate or Login');
return Authenticate();
} else {
print('In HomeScreen');
return NavigationWrapper(); // goto homescreen
}
}
}
Please have a look and give some pointers.
You can save user auth state in shared preferences, but even in that case you will need to load some data before navigate to auth screen or home screen.
I recommend you use the following solution. This way you load auth state and then navigate the corresponding pages according to the auth state.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class _WrapperState extends State<Wrapper> {
#override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: Provider.of<SystemUser>(context).isAuthenticate(),
builder: (context, snapshot) {
// while loading data
if (snapshot.data == null) {
return CircularProgressIndicator();
}
// if has error
if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
// retrieve data - check for authentication
// authenticated, go to homescreen
if (snapshot.data == true) {
print('In HomeScreen');
return NavigationWrapper();
}
// not authenticated, go to auth screen
print('In Authenicate or Login');
return Authenticate();
},
);
}
}
// in SystemUser provider
// check for auth state and return corresponding value
Future<bool> isAuthenticate() async {
// you can implement shared prefereces
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('authKey') ?? false;
}
// whenever you logged in the user just call
prefs.setString('authKey', true);
What I suggest You to do is to use StreamBuilder to check if the user is logged in or not like this:
class MainScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, AsyncSnapshot<User> snapshot) {
if(snapshot.connectionState == ConnectionState.waiting)
return Center(child: CupertinoActivityIndicator());
else if(!snapshot.hasData || snapshot.data == null)
return LoginPage();
else if (snapshot.hasError)
return Center(child: Text('${snapshot.error}'));
return HomePage();
},
);
}
}
I use it like this in every project that needs FirebaseAuth, I hope it helps.
You can do the following using the Consumer element.
Install the Provider package for flutter and make a bool isAuth. When the user logs in, set isAuth = true, then add an if statement inside home: like in the image I provided. Hopefully that will solve your problem.
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();
}