Make users UID [Authentication] the same as the users - firestore database - flutter

I need to make the users UID the same as the documents id.
ex:
In [Authentication], I have the user with the following info:
email: jcasasmail#gmail.com
UID: U8EZAjrCWQRvll6CVmI6OpGZwcH3
In [Firestore database], I have a user collection.
In the user collection, I have a document for each user.
I would like to have the document id be the same as the user id.
is this possible?
Below is the snip I have for creating users in [Authentication] and [Firestore database] user collection::
class FirebaseAuthProvider implements AuthProvider {
#override
Future<void> initialize() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
}
#override
Future<AuthUser> createUser({
required String email,
required String password,
}) async {
try {
await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: email,
password: password,
);
final user = currentUser;
if (user != null) {
return user;
} else {
throw UserNotLoggedInAuthException();
}
} on FirebaseAuthException catch (e) {
if (e.code == 'weak-password') {
throw WeakPasswordAuthException();
} else if (e.code == 'email-already-in-use') {
throw EmailAlreadyInUseAuthException();
} else if (e.code == 'invalid-email') {
throw InvalidEmailAuthException();
} else {
throw GenericAuthException();
}
} catch (_) {
throw GenericAuthException();
}
}
}
Below is my FirebaseCloudStorage class
class FirebaseCloudStorage {
final user = FirebaseFirestore.instance.collection('user');
Future<CloudUser> createNewUser({
required String userId,
required String userState,
required String userImage,
required String userFirstName,
required String userLastName,
required String userCellNumber,
required String userCity,
required String userAreaCode,
required String userAddress,
required String userEmail,
}) async {
final document = await user.add({
userIdColumn: userId,
userStateColumn: userState,
userImageColumn: userImage,
userFirstNameColumn: userFirstName,
userLastNameColumn: userLastName,
userCellNumberColumn: userCellNumber,
userCityColumn: userCity,
userAreaCodeColumn: userAreaCode,
userAddressColumn: userAddress,
userEmailColumn: userEmail,
});
final fetchedUser = await document.get();
return CloudUser(
documentId: fetchedUser.id,
userId: userId,
userState: userState,
userImage: userImage,
userFirstName: userFirstName,
userLastName: userLastName,
userCellNumber: userCellNumber,
userCity: userCity,
userAreaCode: userAreaCode,
userAddress: userAddress,
userEmail: userEmail,
);
}
// singleton
static final FirebaseCloudStorage _shared =
FirebaseCloudStorage._sharedInstance();
FirebaseCloudStorage._sharedInstance();
factory FirebaseCloudStorage() => _shared;
}
Below is the sign up screen button that kicks off all the creation
Future createUserAccount() async {
if (_photo == null) return;
try {
setState(() {
_isLoading = true;
});
await AuthService.firebase().createUser(
email: _email.text,
password: _password.text,
// displayName: _userFirstName.text, // not working
);
final userId = AuthService.firebase().currentUser?.id;
final destination = 'user-profile-image/$userId';
final ref = FirebaseStorage.instance.ref(destination);
await ref.putFile(_photo!);
imageUrl = await ref.getDownloadURL();
_userService.createNewUser(
userId: userId as String,
userState: 'new',
userImage: imageUrl as String,
userFirstName: _userFirstName.text,
userLastName: _userLastName.text,
userCellNumber: _userCellphoneNumber.text,
userCity: _userCity.text,
userAreaCode: _userAreaCode.text,
userAddress: _userAddress.text,
userEmail: _email.text,
);
setState(() {
_isLoading = false;
});
AuthService.firebase().sendEmailVerification();
Navigator.of(context).pushNamed(verifyEmailRoute);
} on WeakPasswordAuthException {
await showErrorDialog(
context,
'Weak password',
);
} on EmailAlreadyInUseAuthException {
await showErrorDialog(
context,
'Email is already in use',
);
} on InvalidEmailAuthException {
await showErrorDialog(
context,
'This is an invalid email address',
);
} on GenericAuthException {
await showErrorDialog(
context,
'Failed to register',
);
}
}

You can use a custom id for a document by using set instead of add and specifying the already existing user identifier. So in your createNewUser function you can do it like:
final document = await user.doc(userId).set({
userIdColumn: userId,
userStateColumn: userState,
userImageColumn: userImage,
userFirstNameColumn: userFirstName,
userLastNameColumn: userLastName,
userCellNumberColumn: userCellNumber,
userCityColumn: userCity,
userAreaCodeColumn: userAreaCode,
userAddressColumn: userAddress,
userEmailColumn: userEmail,
});
Note: It might be a better solution to maintain your own user collection from a trusted environment on the server side and not from the client. For example you can use Firebase Authentication triggers to add / delete / modify user data whenever there is a change, for example a new user is signed up. This way you don't have to allow write operations to your user collection from the client.

You can use an onCreate trigger on an authentication event.
If you are using cloud functions you can use the following code to create a new document in your user collection when a new authentication entry is created:
export const createUser = functions.auth
.user()
.onCreate(async (user) => {
// DataObject includes all the data, which should be stored in firestore
const dataObject = {
email: user.email,
};
await dbFirestore
.collection('users')
.doc(user.uid)
.set(dataObject, {merge: true});
});
Information about triggers: https://firebase.google.com/docs/functions/auth-events

Related

flutter firebase login/ auth: Getting too many writes?

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

Flutter - user model is NULL even after binding it using bindStream

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

flutter - how to pass required parameters when they have not yet been initialized

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

The method '[]' was called on null: firebase flutter

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

Flutter Firestore Issue: Not able to store Sign Up form data in Cloud Firestore DB, instead hard coded data is getting stored

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!