How to check if email already exists in Firestore db - swift

I have implemented Auth method for the Firestore database, but when the user tries to register with the same email, the app crash. I'd like to implement a function to check if the email already exists (if it does, fire UIAlert, otherwise if it doesn't, create a new user).
I have so far:
Auth.auth().createUser(withEmail: email, password: password) { (Result, err) in
let db = Firestore.firestore()
let docRef = db.collection("email users").document("email")
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let emailAlreadyInUseAlert = UIAlertController(title: "Error", message: "Email already registered", preferredStyle: .alert)
emailAlreadyInUseAlert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.present(emailAlreadyInUseAlert, animated: true, completion: nil)
return
} else {
let db = Firestore.firestore()
db.collection("email users").addDocument(data: [
"firstName": firstName,
"lastName": lastName,
"email": email,
"created": Timestamp(date: Date()),
"uid": Result!.user.uid
])
}
self.transitionToHome()
}
}
}
}
func transitionToHome() {
let homeViewController = storyboard?.instantiateViewController(identifier: "HomeViewController") as? HomeViewController
view.window?.rootViewController = homeViewController
view.window?.makeKeyAndVisible()
}
}
At this code the UIAlert doesn't fire, and have an error at : "uid": Result!.user.uid - Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value. When creating new user with unique email, it works as it should, the user is created.
If I change if let document = document, document.exists to if error !=nil, I get the UIAlert when the email already exist and also if it doesnt exist, the code of creating user doesnt execute.
Even tried to implement addsnapshotlistener, no luck.
Any suggestions? Thank you

Regarding error: it's a common practice to return success code as error code instead of setting error to nil, and google docs seems mention it as well.
The other issue is because you are forcibly unwrapping items that can legitimately be nil.
Instead, use guard to isolate any invalid cases and exit:
guard error == nil || case FirestoreErrorCode.OK = error else {
// got error; process it and
return
}
guard let result = result else {
// got no error, but no result either
// fail and
return
}
//if you are here, it means you've got no error and `result` is not nil.
Also notice that result should not be capitalized in callback:
Auth.auth().createUser(withEmail: email, password: password) { (result, err) in ...

You may not need a custom function to check if an email already exists as that's a default error Firebase Auth will catch and allow you to handle when a user is created.
For example, this code will catch situations where the user is attempting to use an email that already exists.
func createUser() {
let email = "test#thing.com"
Auth.auth().createUser(withEmail: email, password: "password", completion: { authResult, error in
if let x = error {
let err = x as NSError
switch err.code {
case AuthErrorCode.wrongPassword.rawValue:
print("wrong password")
case AuthErrorCode.invalidEmail.rawValue:
print("invalid email")
case AuthErrorCode.accountExistsWithDifferentCredential.rawValue:
print("accountExistsWithDifferentCredential")
case AuthErrorCode.emailAlreadyInUse.rawValue:
print("email already in use")
default:
print("unknown error: \(err.localizedDescription)")
}
return
}
let x = authResult?.user.uid
print("successfully created user: \(x)")
})
}
There's a number of Authentication Error Codes so you can handle a wide variety of errors without any special error handling.
And the AuthErrorCode API has some more useful information which is demonstrated in the answer code.

Related

SwiftUI: Check if Firebase RealtimeDatabase has a specific Child the register the value or return error

I am currently building an app with an account system.
Firebase is very new to me, that's why I watched a lot of tutorials, and now its working fine.
I want to implement that the user can choose a unique username at the registration. My problem is, I really don't know how to check if this name is already taken.
I found some code for that, but that's not working, I will show you the code for the RegistrationService file.
I hope someone can explain to me how to implement this username verification. It should return an error if the username is already taken and do continue the registration if its a valid username.
Thank you!
import Combine
import Firebase
import FirebaseDatabase
import Foundation
enum RegistrationKeys: String {
case firstName
case lastname
case info
case username
}
protocol RegisterService {
func register(with details: RegistrationDetails) -> AnyPublisher<Void, Error>
}
final class RegisterServiceImpl: RegisterService {
func register(with details: RegistrationDetails) -> AnyPublisher<Void, Error> {
Deferred {
Future { promise in
Auth.auth()
.createUser(
withEmail: details.email,
password: details.password
) { res, error in
if let err = error {
promise(.failure(err))
} else {
// Success on User creation
if let uid = res?.user.uid {
let values =
[
RegistrationKeys.firstName.rawValue: details.firstName,
RegistrationKeys.lastname.rawValue: details.lastName,
RegistrationKeys.info.rawValue: details.info,
] as [String: Any]
let db = Database.database(url: "theurl")
Database.database(url: "the url")
.reference()
.child("usernames")
.child("\([RegistrationKeys.info.rawValue: details.username] as [String : Any])")
// here should be the check and then continue if its valid
db
.reference()
.child("users")
.child(uid)
.updateChildValues(values) { error, ref in
if let err = error {
promise(.failure(err))
} else {
promise(.success(()))
}
}
} else {
promise(.failure(NSError(domain: "Invalid user ID", code: 0, userInfo: nil)))
}
}
}
}
}
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
I can see two possibilities to solve your problem:
If the e-mail can serve as the username
Firebase authentication already sends back an error message in case the e-mail (the one used when creating the user) already exists. If the e-mail passed in the following function is not unique, an error will be thrown:
Auth.auth()
.createUser(
withEmail: details.email,
password: details.password
) { res, error in
if let err = error {
promise(.failure(err))
If an additional username besides the e-mail is required
If you need usernames in addition to the e-mails, you can store them under a node "usernames", like we see in your example. Personally, I would hash them instead of storing them plain.
The structure could simply be:
{
usernames: {
username_1: true,
username_2: true,
...
username_n: true
}
}
The example below checks to see if a new username exists and stores the result in the variable isUsernameTaken:
let db = Database.database(url: "the url").reference()
let newUsername = "seeIfItIsTaken"
db.child("usernames").child(newUsername).getData() { error, snapshot in
guard error == nil else {
print("Found error \(error)")
return
}
let isUsernameTaken = snapshot.exists()
}

How to catch error when Firestore collection already exists?

Right now my app is crashing when a collection already exists in Firebase Firestore. I want to catch this error when it happens, but my current implementation doesn't catch anything as the addSnapshotListener() method does not throw any error.
Current Code
let db = Firestore.firestore()
do {
try db.collection(chatName).addSnapshotListener { (Query, Error) in
if Error != nil || Query?.documents != nil {
let alert = UIAlertController(title: "Chat Name Already Exists", message: "This chat name already exists, try again with a different name", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Okay", style: .default, handler: { (UIAlertAction) in
alert.dismiss(animated: true, completion: nil)}))
AllChatsViewController().present(alert, animated: true)
completion()
}
else {
self.addChatToProfile(withName: chatName) {
completion()
}
}
}
}
catch {
let alert = UIAlertController(title: "Chat Name Already Exists", message: "This chat name already exists, try again with a different name", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Okay", style: .default, handler: { (UIAlertAction) in
alert.dismiss(animated: true, completion: nil)}))
AllChatsViewController().present(alert, animated: true)
completion()
}
Error After App Crashes
Thread 1: "Invalid collection reference. Collection references must have an odd number of segments, but has 0"
How can I catch this error so I can display an UIAlertController with the error?
I would use a different approach.
To test if a collection exists, read that collection by name and determine if there are any documents via snapshot.count equals 0
The gotcha here is that a collection could have a large amount of data and there's no reason to read all of that in or attach a listener so we need to use a known field within that collection to limit the results.
I would suggest a function with a closure that returns true if the collection exists, false if not and then take action based on that result.
You'll need the name of the collection you want to test and then the name of a known field within that collection to query for to limit the results.
The field name is important in that if the collection has 1M documents, you don't want to read them all in - you just want to read one and .orderBy with a limit will do that.
So here's a calling function
func checkCollection() {
self.doesCollectionExist(collectionName: "test_collection", fieldName: "msg", completion: { isEmpty in
if isEmpty == true {
print("collection does not exist")
} else {
print("collection found!")
}
})
}
and then the function that checks to see if the collection exists by reading one document and returns false if not, true if it does.
func doesCollectionExist(collectionName: String, fieldName: String, completion: #escaping ( (Bool) -> Void ) ) {
let ref = self.db.collection(collectionName)
let query = ref.order(by: fieldName).limit(toLast: 1)
query.getDocuments(completion: { snapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
if snapshot!.count == 0 {
completion(true)
} else {
completion(false)
}
})
}
That error doesn't have anything to do with a collection not existing The error suggests that chatName is an empty string, which is an invalid parameter. Instead of catching an error, you should instead first validate that chatName is a valid collection name string before sending it to the Firestore API.
If you query a collection that doesn't exist, you won't get that error message at all. Instead, you will simply get no documents in the result set.
you will make something like this:
Firestore.firestore().collection(chatName).addSnapshotListener { (query, error) in
if let error = error {
//in this part you have the error, do something like present alert with error or something you want
print(error)
}
// in this part the success

Document ID retrieval

this is supposed to take the user ID from the result!.user.uid and store it a variable or function in order for me to use it again.
the problem is that I dont know how to get it to store the value outside of this function.
Ive tried to make it store to a variable outside of the initial button function, and Ive also tried to return it outside of the function by removing a part of the code which made it become a void. Im not sure where i need to go/what else I can try and do in order to fix this problem.
If anybody know how do I retrieve my document ID from this code your help would be greaty appreciated
#IBAction func NextButtonTapped(_ sender: Any) {
//validate the fileds
let Error = validateFields()
if Error != nil {
// there is somthing wrong with the fields show error message
showError(Error!)
}
else {
// create cleaned versions of the data
let Password = PasswordTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let Email = EmailTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let Firstname = FirstnameTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let Lastname = LastnameTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let Age = AgeTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
// create the user
Auth.auth().createUser(withEmail: Email, password: Password) { (results, Err) in
// check for errors
if Err != nil {
// there was an error creating the user
self.showError("Error creating user")
}
else {
// user was created succesfully store first and last name
let db = Firestore.firestore()
db.collection("users").document(results!.user.uid).setData(["first name":Firstname, "last name":Lastname, "age":Age, "uid":results!.user.uid]) { (Error) in
if Error != nil {
// show error message
self.showError("error saving user data")
}
//showing users document id
}
//transition to the home screen
self.transitionToHome()
}
}
}
}
I have no idea what to do any help would be amazing,
thank you very much!!!!
Define a uid variable outside of the IBAction function like so.
var uid: String? = nil
Then, within the createUser function
self.uid = results!.user.uid

Swift & Firebase Cannot assign value of type 'AuthDataResult?' to type 'User?

I am revisiting an old app but before I work on new features I want to fix all errors. I'm not sure if this is caused by newer Swift syntax or updates to Firebase (or both) but code which previously worked is now causing 2 errors.
var user = Auth.auth().currentUser
Auth.auth().createUser(withEmail: theEmail, password: thePassword, completion: { (user, error) in
if let theError = error {
var errMessage = "An unknown error occured."
if let errCode = AuthErrorCode(rawValue: (theError._code)) {
switch errCode {
case .invalidEmail:
errMessage = "The entered email does not meet requirements."
case .emailAlreadyInUse:
errMessage = "The entered email has already been registered."
case .weakPassword:
errMessage = "The entered password does not meet minimum requirements."
default:
errMessage = "Please try again."
}
}
completion(nil, errMessage)
} else {
self.user = user
completion(user, nil)
}
})
On self.user = user the error message is Cannot assign value of type 'AuthDataResult?' to type 'User?'
On completion(user, nil) the error message is Cannot convert value of type 'AuthDataResult?' to expected argument type 'User?'
Could somebody explain why this code which once worked no longer works and what I can do to bring it up to current expectations
Replace
self.user = user
with
Auth.auth().createUser(withEmail: theEmail, password: thePassword, completion: { (authData, error) in
.....
if let res = authData?.user {
self.user = res
}
}

Function not stopping after handleComplete

I created a basic Google Places app that lets users check-in to a location. When a user tries to check in, I loop through the list of likelihood places to verify that the user is actually at the location in the app. However, when I try to escape the loop after confirming the location is correct, my function still ends up going to my "else" situation (an error message that asks the user to please check in to the correct location).
The following function gets called in viewWillAppear:
func checkIn(handleComplete:#escaping (()->())){
guard let currentUserID = User.current?.key else {return}
// Specify the place data types to return.
let fields: GMSPlaceField = GMSPlaceField(rawValue: UInt(GMSPlaceField.name.rawValue) |
UInt(GMSPlaceField.placeID.rawValue))!
placesClient.findPlaceLikelihoodsFromCurrentLocation(withPlaceFields: fields, callback: {
(placeLikelihoodList: Array<GMSPlaceLikelihood>?, error: Error?) in
if let error = error {
print("An error occurred: \(error.localizedDescription)")
return
}
if let placeLikelihoodList = placeLikelihoodList {
for likelihood in placeLikelihoodList {
let place = likelihood.place
if likelihood.likelihood >= 0.75 && place.placeID! == self.hangoutID {
let place = likelihood.place
print("Current Place name \(String(describing: place.name!)) at likelihood \(likelihood.likelihood)")
print("Current PlaceID \(String(describing: place.placeID!))")
self.delta = 0.0
// update checkin
DispatchQueue.main.async {
let hangoutRef = self.db.collection("users").document(currentUserID).collection("hangout").document(self.hangoutID).updateData([
"lastCheckin": Date()
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}
handleComplete()
}
}
self.presentDismissableAlert(title: "", message: "Please check in to the hangout to join this chat", button: "OK", dismissed: { (UIAlertAction) in
self.performSegue(withIdentifier: "unwindSegueToChats", sender: self)
})
}
})
}
If the correct conditions are met, the code will land on the handleComplete() line but then it will still execute the dismissableAlert underneath and segue the user out of the room. How can I fix the flow so that the app will cycle through the list of likely Places and stop the function on handleComplete if the correct condition is met, or else then proceed to the error message if the correct conditions are not met (user is not at the correct Place)?
Thanks