I am trying to register a user in Firebase, AND add that user to a "users" collection with additional fields. When registering a user, I only want these commands to execute if BOTH of them are successful. For example, I don't want to register a user in Firebase if the user fails to be added to the users collection. But I also don't want the user to be added to the users collection if the firebase createUser function fails.
func register(withEmail email: String, password: String, fullname: String, username: String) {
Auth.auth().createUser(withEmail: email, password: password) { [self] result, error in
if let error = error {
print("Failed to register with error \(error.localizedDescription)")
return
}
guard let user = result?.user else { return }
let data = ["email": email,
"username": username.lowercased(),
"fullname": fullname,
"uid": user.uid,
"listOfUserActions": listOfUserActions]
Firestore.firestore().collection("users")
.document(user.uid)
.setData(data) { _ in
self.didAuthenticateUser = true
}
}
}
The way I have this set up right now, if the user was added to FirebaseAuth but the post to "users" failed, wouldn't this just break the app if I have functionality depending on the "users" collection?
You may use a combination of the methods from the createUser and setData methods to make sure that both of those operations succeed before setting didAuthenticateUser to true.
Here's the modified code:
func register(withEmail email: String, password: String, fullname: String, username: String) {
Auth.auth().createUser(withEmail: email, password: password) { [self] result, error in
if let error = error {
print("Failed to register with error \(error.localizedDescription)")
return
}
guard let user = result?.user else { return }
let data = ["email": email,
"username": username.lowercased(),
"fullname": fullname,
"uid": user.uid,
"listOfUserActions": listOfUserActions]
Firestore.firestore().collection("users")
.document(user.uid)
.setData(data) { error in
if let error = error {
print("Failed to set data with error \(error.localizedDescription)")
return
}
self.didAuthenticateUser = true
}
}
}
Related
In Swift I am creating a mobile application using Firebase Authentication and Cloud Database. The authentication works perfectly fine with users logging in and out of the app. Where I am having problems is when loading and saving the user's non-private data to the cloud database. When initially solving this problem I only want users to be able access their own data instead of everyone's data too.
So, in my firebase console for the database rule, I had entered the following from the Firebase website: Firebase Cloud Database Security.
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
When using these rules, I get the error in Swift:
Error Domain=FIRFirestoreErrorDomain Code=7 "Missing or insufficient permissions." UserInfo={NSLocalizedDescription=Missing or insufficient permissions.}
However, when the user Id it not checked, such as:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null;
}
}
}
It works as expected. Or when manually inputting my own user Id:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == "...";
}
}
}
It works as expected too, but obviously I can't use my own Id for everyone else. Therefore I am stuck and not sure how to fix this issue. I have tried using different names for the collections and tried using a different name for the userId attribute, but both altercations achieved little success. Therefore, I believe that the problem is with the 'userId' path. But I am so lost I think for now I will have to go with the second rule set until I can find a solution.
Here is my code in swift for getting the user's data:
func getUserData(userEmail: String, completionHandler: #escaping (_ documentID: String?, _ userType: UserType?, _ tokensUsed: Double?) -> ()) {
var documentID: String = ""
var userType: UserType = .free
var tokensUsed: Double = 0
let db = Firestore.firestore()
db.collection("users").getDocuments { snapshot, error in
if error != nil {
print(error!)
completionHandler(nil, nil, nil)
return
}
guard let snapshot = snapshot else {
completionHandler(nil, nil, nil)
return
}
snapshot.documents.map { document in
documentID = document.documentID
let documentUserEmail = document["userEmail"] as? String ?? ""
if documentUserEmail == userEmail {
let documentUserType = document["userType"] as? String ?? "free"
if documentUserType == "premium" {
userType = .premium
}
let userTokens = document["tokensUsed"] as? Int ?? 0
return completionHandler(documentID, userType, tokensUsed)
}
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
completionHandler(nil, nil, nil)
}
}
Here is the code for creating the data:
func createUserData(userId: String, userEmail: String , completionHandler: #escaping (_ documentID: String?, _ userId: String?, _ userType: UserType?, _ tokensUsed: Int?) -> ()) {
let db = Firestore.firestore()
db.collection("users").addDocument(data: ["userId": userId, "userEmail": userEmail, "lastLogin": Date(), "tokensUsed": 0, "userType": "free"]) { error in
getUserData(userId: userId, userEmail: userEmail) {
documentID, userType, tokensUsed in
if documentID != nil {
completionHandler(documentID!, userId, userType!, tokensUsed!)
} else {
print("Error")
}
}
if let error = error {
print(error)
}
}
}
Here is the code for updating the data:
func updateUserData(documentID: String, userId: String, userEmail: String, userTokens: Int, userType: String) {
let db = Firestore.firestore()
db.collection("users").document(documentID).updateData(["userId": userId, "userEmail": userEmail, "lastLogin": Date(), "tokensUsed": userTokens, "userType": userType])
}
Here is the database structure on firebase console (while hiding the email and userId):
Thanks in advance!
I'm trying to add additional information to my cloud function so that way my Stripe customer has all of the data saved in the Firebase Database. However, my question is how can I implement the constants in my cloud function correctly so the information uploads correctly? Without the fullname, username, and profileImage in my cloud function and my registration function in the functions section, it creates the Stripe customer. How do I structure the constants for those three fields so they can upload as well? Or should I create an email and password registration screen, so I can create the stripeID, then create another screen for additional information to add to the reference? Thank you!
Cloud Function:
exports.createStripeCustomer = functions.https.onCall( async (data, context) => {
const email = data.email
const uid = context.auth.uid
const fullname = context.auth.uid.fullname
const username = context.auth.uid.username
const profileImage = context.auth.uid.profileImage
if (uid === null) {
console.log('Illegal access attempt due to unauthenticated attempt.')
throw new functions.https.HttpsError('internal', 'Illegal access attempt')
}
return stripe.customers.create({
email : email,
fullname : fullname,
username : username,
profileImage : profileImage
}).then( customer => {
return customer["id"]
}).then( customerId => {
admin.database().ref("customers").child(uid).set(
{
stripeId: customerId,
email: email,
fullname: fullname,
username: username,
profileImage: profileImage,
id: uid
}
)
}).catch( err => {
throw new functions.https.HttpsError('internal', 'Unable to create Stripe customer.')
})
})
AuthService Function:
static func createCustomer(credentials: CustomerCredentials, completion: #escaping(DatabaseCompletion)) {
guard let imageData = credentials.profileImage.jpegData(compressionQuality: 0.3) else { return }
let filename = NSUUID().uuidString
let storageRef = STORAGE_REF.reference(withPath: "/customer_profile_images/\(filename)")
storageRef.putData(imageData, metadata: nil) { (meta, error) in
if let error = error {
debugPrint(error.localizedDescription)
return
}
storageRef.downloadURL { (url, error) in
guard let profileImageUrl = url?.absoluteString else { return }
Auth.auth().createUser(withEmail: credentials.email, password: credentials.password) { (result, error) in
if let error = error {
debugPrint(error.localizedDescription)
return
}
guard let uid = result?.user.uid else { return }
let values = ["email" : credentials.email,
"fullname" : credentials.fullname,
"username" : credentials.username,
"uid" : uid,
"profileImageUrl" : profileImageUrl] as [String : Any]
CustomerDataService.saveCustomerData(uid: uid, fullname: credentials.fullname, email: credentials.email,
username: credentials.username, profileImagUrl: profileImageUrl)
REF_CUSTOMERS.child(uid).setValue(values, withCompletionBlock: completion)
}
}
}
}
Registration Function:
#objc func handleCreateAccount() {
guard let profileImage = profileImage else {
self.simpleAlert(title: "Error", msg: "Please select a profile image.")
return
}
guard let email = emailTextField.text?.lowercased() , email.isNotEmpty ,
let fullname = fullnameTextField.text , fullname.isNotEmpty ,
let username = usernameTextField.text?.lowercased() , username.isNotEmpty ,
let password = passwordTextField.text , password.isNotEmpty ,
let confirmPassword = confirmPasswordTextField.text , confirmPassword.isNotEmpty else {
self.simpleAlert(title: "Error", msg: "Please fill out all fields.")
return
}
if password != confirmPassword {
self.simpleAlert(title: "Error", msg: "Passwords don't match, please try again.")
return
}
showLoader(true, withText: "Registering Account")
let credentials = CustomerCredentials(email: email, fullname: fullname, username: username,
password: password, profileImage: profileImage)
AuthService.createCustomer(credentials: credentials) { (error, ref) in
if let error = error {
Auth.auth().handleFireAuthError(error: error, vc: self)
self.showLoader(false)
return
}
Functions.functions().httpsCallable("createStripeCustomer").call(["email": credentials.email,
"fullname": credentials.fullname,
"username": credentials.username,
"profileImage": credentials.profileImage]) { result, error in
if let error = error {
Auth.auth().handleFireAuthError(error: error, vc: self)
self.showLoader(false)
return
}
}
self.showLoader(false)
guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else { return }
guard let tab = window.rootViewController as? MainTabController else { return }
tab.setupNavigationControllers()
self.handleDismissal()
}
}
To complete what I was trying to accomplish, I created a screen for customers to create an e-mail and password. This way the StripeID could be created, and then I created another screen to add the full name, username and profile image, and updated the database reference.
I am new to Swift programming and am trying to understand the concept of handlers. My saveDataToFirestore function is being called twice - I'm pretty it's due to the completion handlers, but I can't figure it out
I have a button in the SignUpView that the user presses once they've inputted their information
struct SignUpView: View {
#ObservedObject var user = UserViewModelTEMP()
...
user.signUp(firstName: firstName, lastName: lastName, email: email, password: password, reenterPassword: passwordReEnter) { (result, error) in
if error != nil {
self.error = true
} else {
self.isSignedUp = true
self.password = ""
}
}
...
This is the ViewModel where first the signUp function is called, then the user's name and email is passed to another function so that their info can be save to the database
class UserViewModelTEMP : ObservableObject {
#Published var user = [UserTEMP]()
var handle: AuthStateDidChangeListenerHandle?
private let db = Firestore.firestore()
func signUp (firstName: String, lastName: String, email: String, password: String, reenterPassword: String, handler: #escaping AuthDataResultCallback) {
//Ensure that passwords match
if reenterPassword != password {
print("Passwords do not match") // make this into a popup
return
}
//Authenticate with Firebase
Auth.auth().createUser(withEmail: email, password: password, completion: handler)
// Save user data to Firestore
saveDataToFirestore(firstName: firstName, lastName: lastName, email: email)
}
func saveDataToFirestore(firstName: String, lastName: String, email: String) {
print("entry one")
// ensure user is signed in
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
print("entry 2")
if let user = user {
//if we have a user, create a new user model and save to Firebase
print("Got user: \(user.uid)")
let ref = self.db.collection("users").document(user.uid)
ref.setData([
"first_name" : firstName,
"last_name" : lastName,
"email" : email
]) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added: \(ref.documentID)")
}
}
} else {
// when we don't have a user, set the session to nil
//self.user = []
}
}
}
Output looks like this
entry one
entry 2
Got user: <user_id_1>
Document added: <document>
entry 2
Got user: <user_id_2>
Document added: <document>
Side note, the user_ids in the output don't match, when they should (even if being called twice).
I am unsure how to fix this problem
I am trying to access a user's uid in Firebase Authentication. I created a createUser completion block in my code and at the end of the block I want to check for the user in which I named firUser. When I try to add firUser.uid in my User I get the error message
"Value of type 'AuthDataResult' has no member ‘uid’"
Below is a copy of the code I wrote hopefully some one can help me.
Auth.auth().createUser(withEmail: email, password: password, completion: { (firUser, error) in
if error != nil {
// report error
} else if let firUser = firUser {
let newUser = User(uid: firUser.uid, username: username, fullName: fullName, bio: "", website: "", follows: [], followedBy: [], profileImage: self.profileImage)
newUser.save(completion: { (error) in
if error != nil {
// report
} else {
// Login User
Auth.auth().signIn(withEmail: email, password: password, completion: { (firUser, error) in
if let error = error {
// report error
print(error)
} else {
self.dismiss(animated: true, completion: nil)
}
})
}
})
}
})
According to the guide, when using .createUser,
If the new account was successfully created, the user is signed in,
and you can get the user's account data from the result object that's
passed to the callback method.
Notice in the sample, you get back authResult, not a User object. authResult contains some information, including the User. You can get to the User using authResult.user.
In addition, when calling the method, if successful, the user is already signed in, so there's no reason to sign them in again. I changed the parameter name to authResult from the sample to help eliminate some of the confusion.
Auth.auth().createUser(withEmail: email, password: password, completion: { authResult, error in
if let error = error {
// report error
return
}
guard let authResult = authResult else { return }
let firUser = authResult.user
let newUser = User(uid: firUser.uid, username: username, fullName: fullName, bio: "", website: "", follows: [], followedBy: [], profileImage: self.profileImage)
newUser.save(completion: { (error) in
if let error = error {
// report
} else {
// not sure what you need to do here anymore since the user is already signed in
}
})
})
I suspect there is something wrong with my code, I cannot pinpoint what it is that I am doing wrong here. The error is on this line here: self.setUserInfo(firstLastName: firstLastName, user: user, username: username, location: location, biography: biography, password: password, pictureData: pictureData) which is in my signUp function.
Cannot convert value of type 'User?' to expected argument type 'User!'
func signUP(firstLastName: String, username: String, email: String, location: String, biography: String, password: String, pictureData: NSData!) {
Auth.auth().createUser(withEmail: email, password: password) { (user, error) in
if error == nil{
self.setUserInfo(firstLastName: firstLastName, user: user, username: username, location: location, biography: biography, password: password, pictureData: pictureData)
}else{
print(error?.localizedDescription)
}
}
}
private func setUserInfo(firstLastName: String, user: User!, username: String, location: String, biography: String, password: String, pictureData: NSData!){
let imagePath = "profileImage\(user.uid)/userPic.jpg"
let imageRef = storageRef.child(imagePath)
let metaData = StorageMetadata()
metaData.contentType = "image/jpeg"
imageRef.putData(pictureData as Data, metadata: metaData){(newMetaData, error)
in
if error == nil{
let changeRequest = User.createProfileChangeRequest()
changeRequest.displayName = username
if let photoURL = newMetaData!.downloadURL(){
changeRequest.photoURL = photoURL
}
changeRequest.commitChanges(completion: { (error) in
if error == nil{
self.saveUserInfo(firstLastName: firstLastName, user: user, username: username, location: location, biography: biography, password: password)
print("user info set")
}else{
print(error?.localizedDescription)
}
})
}else{
print(error?.localizedDescription)
}
}
}
In the method setUserInfo you've set unwrapped value of User parameter by using "!", but in your auth. completion block it seems that that value is optional.
Here is how you can solve it:
Auth.auth().createUser(withEmail: email, password: password) { [weak self] (user, error) in
guard let unwrappedError = error,
let unwrappedUser = user,
let strongSelf = self
else {
print(error?.localizedDescription)
return
}
strongSelf.setUserInfo(firstLastName: firstLastName, user: unwrappedUser, username: username, location: location, biography: biography, password: password, pictureData: pictureData)
}
EDIT: I added a strongSelf pattern and used a guard statement, since you were explicitly retaining self here (a no no in network callback closures!), and guard statements are the preferred way to handle a validity check in this scenario because it makes it more clear what your intent is.