Using Flutter ChangeNotifierProvider for authentication - firebase

I am using ChangeNotifierProvider to handle app state for my flutter app.
My main.dart file
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_app/services/auth.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<AuthService>(
create: (context) => AuthService(), // initializes auth.dart file here
child: MaterialApp(
initialRoute: '/',
onGenerateRoute: RouteGenerator.generateRoute,
debugShowCheckedModeBanner: false,
title: '...',
home: WelcomeScreen(),
));
}
}
I am trying to change the value of the uid field here in auth.dart
auth.dart file
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
class AuthService with ChangeNotifier {
final FirebaseAuth _auth = FirebaseAuth.instance;
String uid = ""; //initial value of uid
UserM _userFromFirebaseUser(User user) {
return user != null ? UserM(uid: user.uid) : null;
}
Stream<UserM> get user {
return null;
}
Future signInWithEmail(String email, String password) async {
try {
UserCredential result = await _auth.signInWithEmailAndPassword(
email: email, password: password);
User user = result.user;
uid = user.uid; //trying to change uid here
print('user id: $uid'); //new value is printed here
notifyListeners();
return _userFromFirebaseUser(user);
} catch (e) {
print(e.toString());
return null;
}
}
uid changes but then when i try to get the new value in another file, i still get the old value which is the empty string declared on top.
This is how i am trying to access it
final auth = Provider.of<AuthService>(context, listen: true).uid;
print(auth);
What am I doing wrong please?

I don't get why there is a need to use Provider to get the uid of a user in Firebase. You can get the uid synchronously by doing currentUser.uid.
Here is an example:
print(FirebaseAuth.instance.currentUser.uid);

Related

W/System ( 6271): Ignoring header X-Firebase-Locale because its value was null

I get this error at my terminal Ignoring header X-Firebase-Locale because its value was null when user sign in or sign up. The screen doesnt change state when user sign in. I have enabled email password sign in, use SHA-1 and SHA-256 (get from ./gradlew signingReport) at my firebase.console for this app but still, keep getting this error from the terminal and I have no idea how to solve this. Really need your help.
Below is my code and the directories.
auth.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:twitterclone/models/users.dart';
class AuthService {
FirebaseAuth auth = FirebaseAuth.instance;
UserModel? _userFromFirebaseUser(User? user) {
return user != null ? UserModel(id: user.uid) : null;
}
Stream<UserModel?> get user {
return auth.authStateChanges().map(_userFromFirebaseUser);
}
Future signUp(emailSignUp, passwordSignUp) async {
try {
User user = (await auth.createUserWithEmailAndPassword(
email: emailSignUp, password: passwordSignUp) as User);
_userFromFirebaseUser(user);
} on FirebaseAuthException catch (e) {
if (e.code == 'weak-password') {
print('The password provided is too weak.');
} else if (e.code == 'email-already-in-use') {
print('The account already exists for that email.');
}
} catch (e) {
print(e);
}
}
Future signIn(emailSignIn, passwordSignIn) async {
try {
User user = (await auth.signInWithEmailAndPassword(
email: emailSignIn, password: passwordSignIn) as User);
_userFromFirebaseUser(user);
} on FirebaseAuthException catch (e) {
print(e);
} catch (e) {
print(e);
}
}
Future signOut() async {
try {
return await auth.signOut();
} catch (e) {
print(e.toString());
return null;
}
}
}
wrapper.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:twitterclone/models/users.dart';
import 'package:twitterclone/screens/auth/welcome.dart';
import 'package:twitterclone/screens/mainscreen/feedscreen.dart';
class Wrapper extends StatelessWidget {
const Wrapper({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final user = Provider.of<UserModel?>(context);
if (user == null) {
print('no user');
return Welcome();
}
print('has user');
return FeedScreen();
}
}
user.dart
class UserModel {
final String? id;
UserModel({this.id});
}
main.dart
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:provider/provider.dart';
import 'package:twitterclone/models/users.dart';
import 'package:twitterclone/services/auth/auth.dart';
import 'package:twitterclone/wrapper/wrapper.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _initialization,
builder: (context, snapshot) {
// Check for errors
if (snapshot.hasError) {
return Text('Oops, something went error. Try to restart the app.');
}
// Once complete, show your application
if (snapshot.connectionState == ConnectionState.done) {
return StreamProvider<UserModel?>.value(
value: AuthService().user,
initialData: null,
child: MaterialApp(home: Wrapper()),
);
}
// Otherwise, show something whilst waiting for initialization to complete
return Text('Loading...');
},
);
}
}
I hope you fotgot to add ChangeNotifier .
replace class AuthService { ... with class AuthService with ChangeNotifier{ ... .

Google authentication problem (Flutter and Firebase)

I am flutter-newbie and I have one problem: I want to add Google authentication to my flutter app with firebase. This is my code for login screen:
import 'package:PixiCall/resources/firebase_repository.dart';
import 'package:PixiCall/screens/home_screen.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
#override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
FirebaseRepository _repository = FirebaseRepository();
#override
Widget build(BuildContext context) {
return Scaffold(
body: loginButton(),
);
}
Widget loginButton() {
return FlatButton(
padding: EdgeInsets.all(35),
child: Text(
'Login',
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w900,
letterSpacing: 1.2,
),
),
onPressed: () => performLogin,
);
}
void performLogin() {
_repository.signIn().then((User user) {
if (user != null) {
authenticateUser(user);
} else {
print('There was an error');
}
});
}
void authenticateUser(User user, BuildContext context) {
_repository.authenticateUser(user).then((isNewUser) {
if (isNewUser) {
_repository.addDataToDb(user).then((value) {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) {
return HomeScreen();
}));
});
} else {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) {
return HomeScreen();
}));
}
});
}
}
I have this error here:
lib/screens/login_screen.dart:39:25: Error: Too few positional arguments: 2 required, 1 given.
authenticateUser(user);
What is the other parameter which I have to add?
Also I think that I have one more mistake in other file. This is the code from other file:
import 'package:PixiCall/models/user.dart';
import 'package:PixiCall/utils/utilities.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
class FirebaseMethods {
final FirebaseAuth _auth = FirebaseAuth.instance;
GoogleSignIn _googleSignIn = GoogleSignIn();
static final FirebaseFirestore firestore = FirebaseFirestore.instance;
//user class
User1 user = User1();
Future<User> getCurrentUser() async {
User currentUser;
currentUser = await _auth.currentUser;
return currentUser;
}
Future<User> signIn() async {
GoogleSignInAccount _signInAccount = await _googleSignIn.signIn();
GoogleSignInAuthentication _signInAuthentication =
await _signInAccount.authentication;
final AuthCredential credential = GoogleAuthProvider.credential(
accessToken: _signInAuthentication.accessToken,
idToken: _signInAuthentication.idToken,
);
User user = await _auth.signInWithCredential(credential);
return user;
}
Future<bool> authenicateUser(User user) async {
QuerySnapshot result = await firestore
.collection('users')
.where('email', isEqualTo: user.email)
.get();
final List<DocumentSnapshot> docs = result.docs;
//if user is registered then length of list > 0 or else less than 0
return docs.length == 0 ? true : false;
}
Future<void> addDataToDb(User currentUser) async {
String username = Utils.getUsername(currentUser.email);
user = User1(
uid: currentUser.uid,
email: currentUser.email,
name: currentUser.displayName,
profilePhoto: currentUser.photoURL,
username: username);
firestore.collection('users').doc(currentUser.uid).set(user.toMap(user));
}
}
This is the mistake in console:
lib/resources/firebase_methods.dart:32:17: Error: A value of type 'UserCredential' can't be assigned to a variable of type 'User'.
Sorry if I confused you, as I said, I am newbie. If you want any other informations please ask here.
For the first mistake , you defined a function void authenticateUser(User user, BuildContext context) so when you use it, it expects 2 arguments a User type object and a BuildContext object
Then you are calling this function in
void performLogin() {
_repository.signIn().then((User user) {
if (user != null) {
authenticateUser(user);
} else {
print('There was an error');
}
});
}
You are passing only the User object, missing the BuildContext
For GoogleSingIn this was my solution:
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
final GoogleSignIn _googleSignIn = GoogleSignIn();
//create user object based on FB json
User _userFromFirebaseUser(FirebaseUser user) {
return user != null
? User(uid: user.uid, emailVerified: user.isEmailVerified)
: null;
}
//auth change user stream
Stream<User> get user {
return _auth.onAuthStateChanged.map(_userFromFirebaseUser);
}
Future<User> singInUsingGoogle() async {
int age;
String email;
String name;
String lastname;
final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
final AuthCredential credential = GoogleAuthProvider.getCredential(
idToken: googleAuth.idToken, accessToken: googleAuth.accessToken);
final result = await _auth.signInWithCredential(credential);
FirebaseUser user = result.user;
print(result.user.providerData);
await DatabaseService(uid: user.uid).createUserData(
//user info
)

Save Data to Firebase Realtime Database in Flutter

In my code the user is Sign in through phone authentication.After login i want to get the user id of the user and add it to the Firebase database along with its personal details.
But when i have done this the Path in database is direct rather than it should be via first UserId then under that the personal details.
I have also provide the image with the database output.
Code:
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/cupertino.dart';
import 'package:udharibook/Screens/SignInPage.dart';
import 'package:udharibook/Screens/dashboard.dart';
class AuthService {
String UserId ='';
final DBRef = FirebaseDatabase.instance.reference().child('Users');
final FirebaseAuth _auth = FirebaseAuth.instance;
handleAuth(){
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext, snapshot){
if(snapshot.hasData){
getCurrentUser();
writeData();
return DashboardPage();
}
else {
return SignIn();
}
},
);
}
getCurrentUser() async{
final FirebaseUser user = await _auth.currentUser();
final uid = user.uid;
// Similarly we can get email as well
//final uemail = user.email;
UserId = uid;
print('User ID: '+UserId);
//print(uemail);
}
void writeData(){
DBRef.child(UserId).set({
'id':'ID1',
'Name':'Mehul Jain',
'Phone':'8856061841'
});
}
signOut(){
FirebaseAuth.instance.signOut();
}
signIn(AuthCredential authCreds){
FirebaseAuth.instance.signInWithCredential(authCreds);
}
signInWithOTP(smsCode,verId){
AuthCredential authCreds = PhoneAuthProvider.getCredential(
verificationId: verId,
smsCode: smsCode
);
signIn(authCreds);
}
}
Database Image
Try the following:
void writeData() async{
final FirebaseUser user = await _auth.currentUser();
final uid = user.uid;
DBRef.child(uid).set({
'id':'ID1',
'Name':'Mehul Jain',
'Phone':'8856061841'
});
}
Remove the getCurrentUser() method and retrieve the uid in writeData()
getCurrentUser() method is an async method, so it not sure that your userId will be having value before the writeData() method is called.
so what you can do is call writeData() after completion of getCurrentUser() method i.e. :
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/cupertino.dart';
import 'package:udharibook/Screens/SignInPage.dart';
import 'package:udharibook/Screens/dashboard.dart';
class AuthService {
String UserId ='';
final DBRef = FirebaseDatabase.instance.reference().child('Users');
final FirebaseAuth _auth = FirebaseAuth.instance;
handleAuth(){
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext, snapshot){
if(snapshot.hasData){
getCurrentUser();
//call writeData() from inside the above method
return DashboardPage();
}
else {
return SignIn();
}
},
);
}
getCurrentUser() async{
final FirebaseUser user = await _auth.currentUser();
final uid = user.uid;
// Similarly we can get email as well
//final uemail = user.email;
UserId = uid;
print('User ID: '+UserId);
//print(uemail);
//Here you add the method calling
writeData();
}
void writeData(){
DBRef.child(UserId).set({
'id':'ID1',
'Name':'Mehul Jain',
'Phone':'8856061841'
});
}
signOut(){
FirebaseAuth.instance.signOut();
}
signIn(AuthCredential authCreds){
FirebaseAuth.instance.signInWithCredential(authCreds);
}
signInWithOTP(smsCode,verId){
AuthCredential authCreds = PhoneAuthProvider.getCredential(
verificationId: verId,
smsCode: smsCode
);
signIn(authCreds);
}
}

StreamController to manage Firebase Authentication

I am having a hard time with the following Firebase login flow. I created a StreamController to listen primarily listen to the OnAuthStateChanged stream of Firebase, and added a data point to this stream (a User object with additional data I fetched from Firestore). My place would be to listen to that StreamController's stream elsewhere in my application in order to grab the user's full info.
My AuthService class:
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:dispatch/models/user.dart';
import 'package:firebase_auth/firebase_auth.dart';
class AuthService {
static FirebaseAuth _auth = FirebaseAuth.instance;
static Firestore _dbReference = Firestore.instance;
final authStreamController = setStreamController();
// create User, we will need to fetch more information from Firestore in the future
Future<User> _userFromFirebaseUser(FirebaseUser user) async {
//return user != null ? User(uid:user.uid) : null;
return user != null ? (await _getUserDetailsAsync(User(uid:user.uid))) : null;
}
// get user details data
Future<User> _getUserDetailsAsync(User user) async {
try {
QuerySnapshot querySnapshot = await _dbReference.collection('user-details')
.where('auth_id', isEqualTo: user.uid)
.getDocuments();
Map<String, dynamic> userInfo = querySnapshot.documents.first.data;
user.email = userInfo['email'] != null ? userInfo['email'] : null;
user.firstName = userInfo['first_name'] != null ? userInfo['first_name'] : null;
user.lastName = userInfo['last_name'] != null ? userInfo['last_name'] : null;
return user;
} catch(e) {
print(e.toString());
return null;
}
}
// define a stream controller with the OnAuthStateChanged stream as its stream
static StreamController<User> setStreamController(){
StreamController<User> authStreamController = StreamController();
Stream<User> _stream = _auth.onAuthStateChanged.map((FirebaseUser user) => User(uid: user.uid));
authStreamController.addStream(_stream);
return authStreamController;
}
// expose stream controller via a getter
StreamController<User> get streamControllerGetter {
return authStreamController;
}
// sign with email and password
Future signInWithUserAndPassword(String email, String password) async {
try {
AuthResult result = await _auth.signInWithEmailAndPassword(email: email, password: password);
FirebaseUser user = result.user;
// fetch the user's details from Cloud Firestore
// return final user
User appUser = await _userFromFirebaseUser(user);
authStreamController.sink.add(appUser);
return appUser;
} catch(e) {
print(e.toString());
return null;
}
}
// sign out
Future signOut() async {
try {
return await _auth.signOut();
} catch(e) {
print('Sign out failed:');
print(e.toString());
return null;
}
}
}
MyApp class
class MyApp extends StatelessWidget {
// wrap MaterialApp widget with Provider
return StreamProvider<User>.value(
//listening to the user change stream
value: AuthService().streamControllerGetter.stream,
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: LoginWrapper(),
),
);
}
For some reason, this whole thing doesn't work, I don't seem to receive the last User object I added to the sink of the StreamController. Any ideas?

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

Resources