Cannot Login using Firebase - firebase

I'm having issues with logging into my app using Firebase Authentication. I got it to work before, but I can't figure out what changes I've made that ended up breaking the code.
Below are the relevant classes-main for controlling what screens are loaded and LoginScreen to sign the user in. Creating new users work, and I can access MyHomePage() after a new user is created. The StreamBuilder part of the code in main also works since the if(userSnapshot.hasData) of code is reached and executed. The screen however just stays on the LoginScreen. There are no platform exception errors either and the console prints out:
Update The issue was because I implemented signOut() incorrectly. I navigated the app screen to the LoginScreen before auth.FirebaseAuth.instance.signOut(). Since StreamBuilder is used to listen for authStateChanges, that navigation to the LoginScreen isn't necessary.
D/FirebaseAuth(31169): Notifying id token listeners about user ( fK9nC6AFGmew1GLsXgj6FxK1zIg2 ). D/FirebaseAuth(31169): Notifying auth state listeners about user ( fK9nC6AFGmew1GLsXgj6FxK1zIg2 ).
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => MyUser(),
),
ChangeNotifierProvider(
create: (context) => Cities(),
),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TestApp',
home: StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (ctx, userSnapshot) {
if (userSnapshot.hasData) {
return MyHomePage();
}
return LoginScreen();
},
),
),
);
}
}
class LoginScreen extends StatefulWidget {
#override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _auth = auth.FirebaseAuth.instance;
var message = '';
void _login(User myUser, BuildContext ctx) async {
try {
await _auth.signInWithEmailAndPassword(
email: myUser.userEmail,
password: myUser.userPassword,
);
} on PlatformException catch (err) {
if (err.message != null) {
message = err.message;
}
} catch (err) {
print(err);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(body: InputForm(_login));
}
}

There's no appear to be error in login
Just add LoginScreen as default in Home like this
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TestApp',
home: LoginScreen(),
),
and then in your logic, after you call signInWithEmailAndPassword firebase method use the Navigator method like this
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MyHomePage()),
);
For more details read this Flutter navigation

Your code seems to work fine except that you're not doing anything (except assigning the value to the message variable which isn't doing anything here) with the PlatformExceptions when you catch them in the block of code below.
on PlatformException catch (err) {
if (err.message != null) {
message = err.message;
}
}
So even if you have a PlatformException, you will not see anything in the console which means the sign in can fail silently.
Solution:
If you want the final catch block to handle all exceptions, you can use the catch block directly.
try {
await _auth.signInWithEmailAndPassword(
email: myUser.userEmail,
password: myUser.userPassword,
);
} catch (err) {
print(err);
}
If you want to print the PlatformException, you can add a print statement like you did in the catch block:
on PlatformException catch (err) {
print(err);
}

Related

Flutter Web/Firebase - Pressing back in browser bypasses verification process

When my user signs up I direct them to a page to inform them that they need to verify their email before continuing:
Here is my verification screen code:
class VerifyScreen extends StatefulWidget {
#override
_VerifyScreenState createState() => _VerifyScreenState();
}
class _VerifyScreenState extends State<VerifyScreen> {
final auth = FirebaseAuth.instance;
User user;
Timer timer;
#override
void initState() {
user = auth.currentUser;
user.sendEmailVerification();
timer = Timer.periodic(
Duration(seconds: 5),
(timer) {
checkEmailVerified();
},
);
super.initState();
}
#override
void dispose() {
timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
height: MediaQuery.of(context).size.height * 0.8,
width: MediaQuery.of(context).size.width * 0.8,
child: Text(
"An email has been sent to ${user.email} please verify before proceeding"),
),
),
);
}
Future<void> checkEmailVerified() async {
user = auth.currentUser;
await user.reload();
if (user.emailVerified) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => OurHomePage(),
),
);
timer.cancel();
}
}
}
Problem Statement: When I press the back arrow on my chrome browser:
I get returned to my homepage with the user signed in which I don't want. I would like my user to verify their email before being able to continue. Here's the drawer on my homepage after I press the back button without verifying the email:
I use provider to pass my user object around my app. Here is my main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MultiProvider(
providers: [
Provider(
create: (_) => FirebaseAuthService(),
),
StreamProvider<OurUser>(
create: (context) =>
context.read<FirebaseAuthService>().onAuthStateChanged),
],
child: MaterialApp(theme: OurTheme().buildTheme(), home: OurHomePage()),
),
);
}
I then user Consumer to consume that provider on my Homepage:
class OurHomePage extends StatefulWidget {
#override
_OurHomePageState createState() => _OurHomePageState();
}
class _OurHomePageState extends State<OurHomePage> {
#override
Widget build(BuildContext context) {
return Consumer<OurUser>(
builder: (_, user, __) {
return ChangeNotifierProvider<SignInViewModel>(
create: (_) => SignInViewModel(context.read),
builder: (_, child) {
return Scaffold(appBar: AppBar(title: Text("My Homepage")));
},
);
},
);
}
}
Can anyone help me resolve the issue I'm facing? Thanks in advance.
On homepage check if user is logged in and when is, check if he has verified email. If he has, let him in, otherwise show him some message.

How to use Firebase Cloud Messaging

I couldn't find any documents about the new version. Versions 7 and 6 have a large number of documents, while 9 is almost nonexistent. Not only me but most people couldn't find it.
I just wanted to send simple notifications to the background. I would be very happy if anyone shared a document about the new version.
Or should I use the old version?
I suppose that you know how to add firebase to your App. If not: https://firebase.google.com/docs/flutter/setup?platform=android
After adding firebase to the App, this is what I do :
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
routes: {
'/': (context) => AppStarter(),
'/message': (context) => NotificationDetails(),
},
),
);
}
class AppStarter extends StatefulWidget{
#override
_AppStarterState createState() => _AppStarterState();
}
class _AppStarterState extends State<AppStarter>
{
FirebaseMessaging messaging = FirebaseMessaging.instance;
Future<void> showMeMyToken()
async {
var myToken = await messaging.getToken();
print("My Token is: " + myToken.toString());
}
#override
void initState() {
super.initState();
showMeMyToken();
FirebaseMessaging.instance.getInitialMessage().then((value) {
if(value != null)
{
Navigator.push(context,
MaterialPageRoute(
builder: (context){return NotificationDetails();},
settings: RouteSettings(arguments: value.data,),
),
);
}
});
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
if (message.notification != null) {
print('Message on Foreground: ${message.notification}');
}
});
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message)
{
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {return NotificationDetails();},
settings: RouteSettings(arguments: message.data,)
),
);
});
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Just a Test',
home: AppHome(),
);
}
}
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print("Handling a background message :-): ${message.data}");
//Here you can do what you want with the message :-)
}
I created a sample app showcasing how to implement the notification system with FCM on version 9.
You can refer to this project and if you need more informations, I'll edit this answer !

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

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/FirebaseAuth : How can I autologin a user at app launch?

I have the following methods to see if the user is already logged in, which in this case I did log in and the getCurrentUser() function works because in the console it does return "USER IS NOT NULL" but the home widget is still null giving me the "EXCEPTION CAUGHT BY WIDGETS LIBRARY" saying that the home can't be null and stuff.
userAPI.dart
Future<FirebaseUser> getCurrentUser() async {
FirebaseUser user = await FirebaseAuth.instance.currentUser();
if (user != null) {
return user;
} else {
return null;
}
}
main.dart
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
Widget home;
APIs().usersAPI.getCurrentUser().then((u) {
if (u == null) {
print('USER IS NULL');
home = WelcomePage();
} else {
print('USER IS NOT NULL');
home = FeedPage();
}
});
return MaterialApp(
title: "Jedi",
debugShowCheckedModeBanner: false,
home: home,
routes: {
'/login' : (context) => new LoginPage(),
'/feed' : (context) => new FeedPage(),
},
);
}
}
You need to make the App a StatefulWidget and call setState when setting the home page
setState(() {
home = WelcomePage();
});
setState(() {
home = FeedPage();
});
Plus you may need to set the home page to something other than null before the API returns.
What probably would be a better pattern is to use a FutureBuilder. This way you will be returning the correct Widget depending on the state you are in.
return MaterialApp(
title: "Jedi",
debugShowCheckedModeBanner: false,
home: FutureBuilder<FirebaseUser>(
future: APIs().usersAPI.getCurrentUser(),
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return CircularProgressIndicator();
default:
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
else
if(snapshot.data == null)
return WelcomePage();
else
return FeedPage();
}
}
),
routes: {
'/login' : (context) => new LoginPage(),
'/feed' : (context) => new FeedPage(),
},
);
}
Advancing the answer given by #aqwert, you need to check for the user is not null/is null after the connection status. See below working example - this assumes autologin if user is not null.
class LandingPage extends StatelessWidget {//call this class from the main.dart
#override
Widget build(BuildContext context) {
return StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
FirebaseUser user = snapshot.data;//get the user status once the connection is established
if (user == null) {
//print("User is NULL::: " + user.toString());
return LoginScreen();//
}
print("User is NOT NULL::: " + user.toString());
return DefaultScreen();//home screen
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),//called in case all fails while waiting for connection status
),
);
}
},
);
Here is my simple solution you can try this, First we need a stateful widget and override the function initState() inside initState() we can work something look like this-
class _MyAppState extends State<MyApp> {
String initPage;
final FirebaseAuth auth=FirebaseAuth.instance;
User currentUser;
#override
void initState() {
super.initState();
try {
currentUser = auth.currentUser;
if(currentUser!=null){
initPage=Chat.id;
/*
here id is static variable which declare as a page name.
*/
}
else{
initPage=Home.id;
}
}
catch(e){
print(e);
initPage=Home.id;
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: initPage,
routes: {
Home.id: (context) => Home(),
Login.id: (context) => Login(),
Registration.id: (context) => Registration(),
Chat.id: (context) => Chat(),
},
);
}
}

Resources