I am trying to add a shopping cart functionality to my app wherein the UserModel contains fields name,email,uid and cart. I have created an AuthController extends GetxController where I have created userModel isntance and then in setInitialScreen function I am binding the userModel to a function "listenToUser()" which is a Stream and recieves snapshot from firestore and maps it to the userModel. But on printing the userModel I see null being printed in the console meaning the data is not getting binded which is causing problems as i can't access the cart stored in the userModel.
Edit: I saw that I need to attach obs to userModel like: Rx<model.User>? userModel = model.User().obs; but there's a problem that all the fields in this model are required and how can i pass these field values when they have not yet been intialized.
Console output:
AuthController code
class AuthController extends GetxController {
static AuthController instance = Get.find();
late Rx<User?> _user;
Rx<model.User>? userModel;
#override
void onReady() {
super.onReady();
_user = Rx<User?>(firebaseAuth.currentUser);
_user.bindStream(firebaseAuth.authStateChanges());
ever(_user, _setInitialScreen);
}
_setInitialScreen(User? user) {
if (user == null) {
Get.offAll(() => LoginScreen());
} else {
userModel?.bindStream(listenToUser());
Get.offAll(() => const HomeScreen());
print(userModel); // PRINTING USER MODEL TO SEE IF ITS NULL
// userModel?.bindStream(listenToUser());
}
}
// registering the user
void registerUser(String username, String email, String password) async {
try {
if (username.isNotEmpty && email.isNotEmpty && password.isNotEmpty) {
// save our user to our auth and firebase firestore
UserCredential cred = await firebaseAuth.createUserWithEmailAndPassword(
email: email,
password: password,
);
model.User user = model.User(
name: username, email: email, uid: cred.user!.uid, cart: []);
await firestore
.collection('users')
.doc(cred.user!.uid)
.set(user.toJson());
} else {
Get.snackbar(
'Error Creating Account',
'Please enter all the fields',
);
}
} catch (e) {
Get.snackbar(
'Error Creating Account',
e.toString(),
);
}
}
void loginUser(String email, String password) async {
try {
if (email.isNotEmpty && password.isNotEmpty) {
await firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
print('log success');
} else {
Get.snackbar(
'Error Logging in',
'Please enter all the fields',
);
}
} catch (e) {
Get.snackbar(
'Error Logging in',
e.toString(),
);
}
}
updateUserData(Map<String, dynamic> data) {
print("UPDATED");
firestore.collection('users').doc(_user.value?.uid).update(data);
}
Stream<model.User> listenToUser() => firestore
.collection('users')
.doc(_user.value?.uid)
.snapshots()
.map((snapshot) => model.User.fromSnap(snapshot));
}
User Model code:
class User {
// static const UID = "uid";
// static const NAME = "name";
// static const EMAIL = "email";
String uid;
String name;
String email;
List<CartItemModel> cart;
User(
{required this.name,
required this.email,
required this.uid,
required this.cart});
Map<String, dynamic> toJson() =>
{"name": name, "email": email, "uid": uid, "cart": cart};
// static User fromSnap(DocumentSnapshot snap) {
static User fromSnap(DocumentSnapshot snap) {
var snapshot = snap.data() as Map<String, dynamic>;
return User(
name: snapshot['name'],
email: snapshot['email'],
uid: snapshot['uid'],
cart: _convertCartItems(snapshot['cart'] ?? []));
}
// List<CartItemModel> _convertCartItems(List cartFromDb) {
static List<CartItemModel> _convertCartItems(List cartFromDb) {
List<CartItemModel> _result = [];
// logger.i(cartFromDb.lengt);
print(cartFromDb.length);
cartFromDb.forEach((element) {
_result.add(CartItemModel.fromMap(element));
});
return _result;
}
}
Also I referred this github for shopping cart functionality but I have made some changes to make it null safe: cart functionality github
Use Rxn<T>() for nullable rx:
final _user = Rxn<User>();
Then on onInit():
_user.bindStream(firebaseAuth.authStateChanges());
Related
I think I am getting too many writes to firebase as this is counting every login, or at least it's not preventing write cycles if the user already exists. I need to know how to do that. I am using flutter getx. Here is my login controller:
class LoginController extends GetxController {
static LoginController instance = Get.find();
Rxn<User> fbUser = Rxn<User>();
final googleSignIn = GoogleSignIn();
RxBool isLoggedIn = false.obs;
Rx<UserModel> userModel = UserModel().obs;
String usersCollection = "coffeeusers";
// Rx<UserModel> usrModel = UserModel().obs;
GoogleSignInAccount? _googleAcc;
UserModel? _userModel;
#override
void onReady() {
super.onReady();
fbUser = Rxn<User>(auth.currentUser);
fbUser.bindStream(auth.userChanges());
ever(fbUser, setInitialScreen);
}
UserModel? get loggedInUserModel => _userModel;
setInitialScreen(User? user) {
if (user == null) {
print("going to login page...");
Get.offAll(() => LoginPage());
} else {
print("The user is ${user.displayName}");
userModel.bindStream(listenToUser());
Get.offAll(() => AppSetup());
}
}
void googleLogin() async {
final googleUser = await googleSignIn.signIn();
if (googleUser == null) return;
_googleAcc = googleUser;
final googleAuth = await googleUser.authentication;
final cred = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
try {
await auth.signInWithCredential(cred).then((res) async {
print('Signed in successfully as ' + res.user!.displayName.toString());
print('email: ' + res.user!.email.toString());
UserModel _newUser = UserModel(
id: res.user!.uid,
email: res.user!.email!,
name: res.user!.displayName,
photoURL: res.user!.photoURL,
cart: [],
);
_addUserToFB(_newUser, res.user!);
});
} catch (e) {
debugPrint(e.toString());
Get.snackbar("Sign In Failed", "Try again");
}
}
// void signUp() async {
// try {
// await auth
// .createUserWithEmailAndPassword(
// email: email.text.trim(), password: password.text.trim())
// .then((result) {
// String _userId = result.user.uid;
// _addUserToFirestore(_userId);
// _clearControllers();
// });
// } catch (e) {
// debugPrint(e.toString());
// Get.snackbar("Sign In Failed", "Try again");
// }
// }
void signOut() async {
googleSignIn.signOut();
auth.signOut();
}
//maybe add clear controllers method?
updateUserData(Map<String, dynamic> data) {
logger.i("UPDATED");
firebaseFirestore
.collection(usersCollection)
.doc(fbUser.value?.uid)
.update(data);
}
Stream<UserModel> listenToUser() => firebaseFirestore
.collection(usersCollection)
.doc(fbUser.value!.uid)
.snapshots()
.map((snapshot) => UserModel.fromSnapshot(snapshot));
_addUserToFB(UserModel usr, User firebaseUser) {
firebaseFirestore.collection(usersCollection).doc(usr.id).set({
"displayName": usr.name,
"id": usr.id,
"photoURL": usr.photoURL,
"email": usr.email,
"cart": usr.cart
});
}
}
And here is my login user model:
class UserModel {
static const ID = "id";
static const NAME = "name";
static const EMAIL = "email";
static const CART = "cart";
static const PHOTOURL = "photoURL";
static const REVIEWS = "reviews";
String? id;
String? name;
String? email;
String? photoURL;
List<CartItemModel>? cart;
List<CartItemModel>? reviews;
UserModel({
this.id,
this.photoURL,
this.email,
this.name,
this.cart,
this.reviews,
});
UserModel.fromSnapshot(DocumentSnapshot<Map<String, dynamic>> snapshot) {
name = snapshot.data()?['NAME'] ?? '';
email = snapshot.data()?['EMAIL'] ?? '';
photoURL = snapshot.data()?['PHOTOURL'] ?? '';
id = snapshot.data()?['ID'] ?? '';
cart = _convertCartItems(snapshot.data()?['CART'] ?? []);
}
List<CartItemModel> _convertCartItems(List cartFomDb) {
List<CartItemModel> result = [];
if (cartFomDb.isNotEmpty) {
for (var element in cartFomDb) {
result.add(CartItemModel.fromMap(element));
}
}
return result;
}
List cartItemsToJson() => cart!.map((item) => item.toJson()).toList();
Map<String, dynamic> toJson() => {
"uid": id,
"email": email,
"name": name,
"photoUrl": photoURL,
"cart": cart,
"reviews": reviews,
};
}
I know I also need to fix this so that the user can add a review of the place. The reviews will essentially be a sub list. All this has to be specific to the user, at least the editing potential. I want other people to be able to read the reviews etc. But for now I need to work on reducing my writes
I get the error: The argument type 'MyUser? Function(User?, String)' can't be assigned to the parameter type 'MyUser? Function(User?)'
Auth.dart:
RED LINE are under "_userFromFirebaseUser"
import 'package:firebase_auth/firebase_auth.dart';
import 'package:hyttekos/models/user.dart';
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
//MyUser Function(User? event, String name)? get userFromFirebaseUser => null;
// create user obj
MyUser? _userFromFirebaseUser(User? user, String name) {
// ignore: unnecessary_null_comparison
return user != null ? MyUser(uid: user.uid, groupId: '', name: name) : null;
}
// auth change user stream
Stream<MyUser?> get user {
return _auth.authStateChanges().map(_userFromFirebaseUser); **RED LINE are under "_userFromFirebaseUser"**
}
// sign in anon
Future signInAnon(String username) async {
try {
UserCredential userCredidential = await _auth.signInAnonymously();
User? user = userCredidential.user;
return _userFromFirebaseUser(user!, username);
} catch (e) {
// ignore: avoid_print
print(e.toString());
return null;
}
}
}
User.dart:
class MyUser {
final String uid;
final String name;
String groupId;
MyUser({
required this.uid,
required this.name,
required this.groupId,
});
}
On _auth.authStateChanges().map(x) here it only provides user, therefor you can't use _userFromFirebaseUser(User? user, String name). You can get name from user like user.displayName
MyUser? _userFromFirebaseUser(User? user) {
return user != null
? MyUser(uid: user.uid, groupId: '', name: user.displayName ?? "")
: null;
}
_auth.authStateChanges().map(_userFromFirebaseUser);
Or
Stream<MyUser?> get user {
return _auth.authStateChanges().map(
(event) => _userFromFirebaseUser(event, ""),
);
}
I am creating a userModel instance inside my authController and want to add obs from Getx to that userModel like: Rx<model.User>? userModel = model.User().obs;, but the fields inside my model.User are all required.
How can I pass these parameters if they have not yet been initialized as they will get initialized after the user signs in?
I referred this github: Github where he has done it because his parameters are not "required" as it is old, but now flutter is null safe and is forcing "required" parameters in my user model.
AuthController code:
class AuthController extends GetxController {
static AuthController instance = Get.find();
late Rx<User?> _user;
// Rx<model.User>? userModel;
Rx<model.User>? userModel = model.User().obs; //ERROR CAUSING LINE**
#override
void onReady() {
super.onReady();
_user = Rx<User?>(firebaseAuth.currentUser);
_user.bindStream(firebaseAuth.authStateChanges());
ever(_user, _setInitialScreen);
}
_setInitialScreen(User? user) {
if (user == null) {
Get.offAll(() => LoginScreen());
} else {
// userModel?.bindStream(listenToUser());
Get.offAll(() => const HomeScreen());
userModel?.bindStream(listenToUser());
print(userModel);
}
}
// registering the user
void registerUser(String username, String email, String password) async {
try {
if (username.isNotEmpty && email.isNotEmpty && password.isNotEmpty) {
// save our user to our auth and firebase firestore
UserCredential cred = await firebaseAuth.createUserWithEmailAndPassword(
email: email,
password: password,
);
model.User user = model.User(
name: username, email: email, uid: cred.user!.uid, cart: []);
await firestore
.collection('users')
.doc(cred.user!.uid)
.set(user.toJson());
} else {
Get.snackbar(
'Error Creating Account',
'Please enter all the fields',
);
}
} catch (e) {
Get.snackbar(
'Error Creating Account',
e.toString(),
);
}
}
void loginUser(String email, String password) async {
try {
if (email.isNotEmpty && password.isNotEmpty) {
await firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
print('log success');
} else {
Get.snackbar(
'Error Logging in',
'Please enter all the fields',
);
}
} catch (e) {
Get.snackbar(
'Error Logging in',
e.toString(),
);
}
}
updateUserData(Map<String, dynamic> data) {
print("UPDATED");
firestore.collection('users').doc(_user.value?.uid).update(data);
}
Stream<model.User> listenToUser() => firestore
.collection('users')
.doc(_user.value?.uid)
.snapshots()
.map((snapshot) => model.User.fromSnap(snapshot));
}
User model code:
class User {
// static const UID = "uid";
// static const NAME = "name";
// static const EMAIL = "email";
String uid;
String name;
String email;
List<CartItemModel> cart;
User(
{required this.name,
required this.email,
required this.uid,
required this.cart});
Map<String, dynamic> toJson() =>
{"name": name, "email": email, "uid": uid, "cart": cart};
// static User fromSnap(DocumentSnapshot snap) {
static User fromSnap(DocumentSnapshot snap) {
var snapshot = snap.data() as Map<String, dynamic>;
return User(
name: snapshot['name'],
email: snapshot['email'],
uid: snapshot['uid'],
cart: _convertCartItems(snapshot['cart'] ?? []));
}
// List<CartItemModel> _convertCartItems(List cartFromDb) {
static List<CartItemModel> _convertCartItems(List cartFromDb) {
List<CartItemModel> _result = [];
// logger.i(cartFromDb.lengt);
print(cartFromDb.length);
cartFromDb.forEach((element) {
_result.add(CartItemModel.fromMap(element));
});
return _result;
}
}
Add ? operator to tell explicity that the object can be null.
So the code goes like:
class User {
String? uid;
String? name;
String? email;
List<CartItemModel>? cart;
}
I need to retrieve User Data from Firebase and use a builder to pass on the data to UI. When I run the apps, I method is called in on Null.
I tried many ways to call firebase data but I keep receive error message on provider or on calling the data NULL.
The error is most likely coming from the method _getProfileData() below.
_getProfileData(AuthNotifier authNotifier) async {
final uid = await Provider.of(context, listen: false).authNotifier.getCurrentUID();
await Provider.of(context, listen: false)
.collection('Users')
.document(uid)
.get().then((result) {
user.isAdmin = result.data['isAdmin'];
});
}
When I made the changes below by using Provider, another error appears with Provider not working.
final uid = await Provider.of<authNotifier>(context, listen: false).getCurrentUID();
I placed the getter in the API.
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
// GET UID
Future<String> getCurrentUID(User user, AuthNotifier authNotifier) async {
return (await _firebaseAuth.currentUser()).uid;
}
// GET CURRENT USER
Future getCurrentUser(User user, AuthNotifier authNotifier) async {
return await _firebaseAuth.currentUser();
}
Stream<String> get onAuthStateChanged => auth.onAuthStateChanged.map(
(FirebaseUser user) => user?.uid,
);
I structured User Data as below.
class User {
List favorites = [];
String documentID;
String displayName;
String email;
String password;
bool isAdmin;
User({
this.favorites,
this.documentID,
this.displayName,
this.email,
this.password,
this.isAdmin,
});
factory User.fromFirestore(DocumentSnapshot document) {
Map data = document.data;
return User(
favorites: data['favorite'] ?? [],
documentID: document.documentID,
displayName: data['displayName'] ?? '',
email: data['email'] ?? '',
isAdmin: data['isAdmin'] ?? false,
);
}
// get admin => null;
Map<String, dynamic> toMap() {
return {
'displayName': displayName,
'email': email,
'isAdmin': isAdmin,
};
}
}
DatabaseService Class
class DatabaseService {
final String uid;
DatabaseService({this.uid});
//collection reference
final CollectionReference agentsCollection =
Firestore.instance.collection('agents');
Future updateUserData(
String fullname,
String phonenumber,
String email,
String profession,
String city
)
async {
return await agentsCollection.document(uid).setData({
'fullname': fullname,
'phonenumber': phonenumber,
'email': email,
'profession': profession,
'city': city,
});
}
AuthService Class
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
//create user object based on FirebaseUser
User _userFromFirebaseUser(FirebaseUser user) {
return user != null ? User(uid: user.uid) : null;
}
//auth change user stream
Stream<User> get user {
return _auth.onAuthStateChanged
.map(_userFromFirebaseUser);
}
Then I have a function to Register the user in this class as below:
//Register with email and password
Future registerWithEmailAndPassword(String email, String password) async {
try{
AuthResult result = await _auth.createUserWithEmailAndPassword
(email: email, password: password);
FirebaseUser user = result.user;
//create a new document for the user with the uid
await DatabaseService(uid: user.uid).updateUserData( 'hardcoded Name',
'Hardcoded Number','hardcoded email', 'hardcoded profession', 'hardcoded city');
return _userFromFirebaseUser(user);
} catch(e){
print(e.toString());
return null;
}
}
SignUp Class
In this class I call the user (uid) using Provider service. Then I use the function from the AuthService Class which uses the function from the DatabaseService class in order to create a new document in the Database.
RaisedButton(
elevation: 5.0,
padding: EdgeInsets.only(left: 60, right: 60 , top: 15, bottom:15),
color: Colors.blue,
child: Text(
'Sign Up',
style: TextStyle(color: Colors.white),
),
onPressed: () async {
if(_formKey.currentState.validate()){
setState(() => loading= true);
dynamic result = await _auth.registerWithEmailAndPassword(email, password);
await DatabaseService (uid: user.uid).updateUserData(this.fullname,
this.phonenumber, this.email, this.profession, this.city);
if(result == null) {
setState(() {
error = 'please supply a valid email';
loading = false;
});
} else {
Navigator.push(context, new MaterialPageRoute(
builder: (context) => SignIn()
));
}
}
}
),
So Given all this, for some reason it's only storing the hard coded data from the AuthService class and not performing the line in the SignUp class under the onPressed() function; where I am telling it to use the function updateUserData and store the this.email, this.fullname, etc. Also an important note, in the SignUp class it says returned null user (assuming uid).
Please someone help me if you get what I am doing wrong in this context.
I think when parsing the data the hard coded data remains. here is an example of how to fix it.
create a user obj
class User {
final String userID;
final String displayName;
final String email, pushToken;
final String phoneNumber;
final String profilePictureURL;
User({
this.phoneNumber,
this.userID,
this.pushToken,
this.displayName,
this.email,
this.profilePictureURL,
});
Map<String, Object> toJson() {
return {
'pushToken': pushToken,
'phoneNumber': phoneNumber,
'userID': userID,
'displayName': displayName,
'email': email == null ? '' : email,
'profilePictureURL': profilePictureURL,
'appIdentifier': 'my app'
};
}
factory User.fromJson(Map<String, Object> doc) {
User user = new User(
pushToken: doc['pushToken'],
userID: doc['userID'],
displayName: doc['displayName'],
phoneNumber: doc['phoneNumber'],
email: doc['email'],
profilePictureURL: doc['profilePictureURL'],
);
return user;
}
factory User.fromDocument(DocumentSnapshot doc) {
return User.fromJson(doc.data);
}
}
and then the sign up method
//user sign up
static Future<String> signUp(String email, String password) async {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
FirebaseUser user = (await _firebaseAuth.createUserWithEmailAndPassword(
email: email, password: password))
.user;
return user.uid;
}
//if user id doesnt exist add user
static void addUser(User user) async {
checkUserExist(user.userID).then((value) {
if (!value) {
print("user ${user.displayName} ${user.email} added");
Firestore.instance
.document("users/${user.userID}")
.setData(user.toJson());
Fluttertoast.showToast(
msg: "user ${user.displayName} ${user.email} added");
} else {
print("user ${user.displayName} ${user.email} exists");
}
});
}
//check if user exists
static Future<bool> checkUserExist(String userID) async {
bool exists = false;
try {
await Firestore.instance.document("users/$userID").get().then((doc) {
if (doc.exists)
exists = true;
else
exists = false;
});
return exists;
} catch (e) {
return false;
}
}
and to handle your button onPressed: and it will save your data to firestore
await signUp(email, password).then((uID) {
addUser(new User(
userID: uID,
email: email,
displayName: fullname,
phoneNumber: number,
pushToken: pushToken,
profilePictureURL: ''));
Hey #Ggriffo i used this code in the onPressed function:
dynamic result = await _auth.registerWithEmailAndPassword
(email, password).then((user) {
DatabaseService (uid: user.uid).updateUserData(this.fullname,
this.phonenumber, this.email, this.profession, this.city);
});
and now it works! i used this logic from your onPressed piece of code. Thanks!