So I am having trouble with my code that creates new users and uploads/stores their user information such as First name, Last name and Email to the firebase database. Whenever I create a user and go onto my firebase databse there is not a new user present.
Auth.auth().createUser(withEmail: usernameTextField.text!, password: passwordTextField.text!) { (user, error) in
if error != nil {
print(error!.localizedDescription)
return
}
let ref = Database.database().reference()
let usersReference = ref.child("users")
let uid = user?.user.uid
let newUserReference = usersReference.child(uid!)
newUserReference.setValue(["username": self.usernameTextField.text!,"firstname": self.firstnameTextField.text!, "lastname": self.lastnameTextField.text!,"email": self.emailTextField.text!])
This should work:
#objc func handleSignUp() {
let ref = Database.database().reference
Auth.auth().createUser(withEmail:usernameTextField.text!, password: passwordTextField.text!) { (user, error) in
if error != nil {
print(error as Any)
return
}
guard let uid = user?.user.uid else {
return
}
// successfully authenticated user
let values = ["name": "some name", "email": "some email", "username": "your username", "userId": user?.user.uid]
let userReference = ref.child("user").child(uid)
let changeRequest = Auth.auth().currentUser?.createProfileChangeRequest()
changeRequest?.displayName = self.usernameField.text
changeRequest?.commitChanges(completion: nil)
userReference.updateChildValues(values, withCompletionBlock: { (err, ref) in
if err != nil {
print(err as Any)
return
}
// add your code here to upload user information to FirebaseDatabase
})
Hope that your problem is solved and the solution works for your purposes
I accidentally wrote
Auth.auth().createUser(withEmail: usernameTextField.text!, password: passwordTextField.text!
instead of:
Auth.auth().createUser(withEmail: emailTextField.text!, password: passwordTextField.text!) { (user, error) in
if error != nil {
print(error!.localizedDescription)
return
} ...
Related
I am using SwiftUI with the Firebase SDK. I am trying to implement Sign In with Microsoft/Azure in my app. I am following what the docs say but when I get to the part that is supposed to make the login window pop up, nothing shows. There are no errors printed to the console. Here is my login function:
func signInWithMicrosoft() {
print("Microsoft Login")
let auth = Auth.auth()
let provider = OAuthProvider(providerID: "microsoft.com", auth: auth)
provider.customParameters = [
"prompt": "consent",
"login_hint": "user#firstadd.onmicrosoft.com"
]
provider.scopes = ["mail.read", "openid.read"]
provider.getCredentialWith(_: nil) { credential, error in
if error != nil {
return
}
if credential != nil {
self.authenticateUserWithMicrosoft(for: credential)
}
}
}
and here is my auth function:
private func authenticateUserWithMicrosoft(for credential: AuthCredential?) {
Auth.auth().signIn(with: credential!) { [unowned self] (_, error) in
if let error = error {
print(error.localizedDescription)
} else {
state = .signedIn
}
}
}
I have tried many fixes such as putting my Auth code inside my AppDelegate class but nothing seems to have worked. Any help would be appreciated.
Summary
I'm developing an application where users can reserve and cancel reservations for classes. In a ReservationButtonView I two buttons that add and remove a user to a workout class respectively. Currently the button I show is based off whether the user's Firebase Auth uid is listed in a Firestore document.
I was having issues when rapidly tapping on the reservation button. Specifically, the reservationCnt would become inaccurate by showing more or less than the actual users reserved for a class.
The only way I have found to resolve this is be using a Firestore transaction that checks to see if a user is in a workout class already. If they are, addReservation() now does nothing. If they aren't, removeReservation() would also do nothing.
At first I thought I could just disable the button and via the logic still in place the code below (.disabled()), but that alone didn't work as I ran into the above described race conditions. What I found out is that arrayUnion and arrayRemove still succeed even when the object I'm looking to add is there and not there respectively. Meaning it is possible for my transaction to not remove a reservedUser that isn't there and also decrease the reservationCnt which can leave me with say no reserved users and a reservationCnt of -1
The Ask
Is there a better way to handle this reservation process? Can I accomplish this without a transaction for at least the removal of users in some way. Ideally, I'd like to have a spinner replace the button as I add or remove a user's reservation to indicate to the user that the app is processing the request. Perhaps I need two variables to manage the disabled() state instead of one?
MVVM Code Snippets
NOTE: I pulled out some button styling to make the code a bit less verbose
ReservationButtonView
struct ReservationButtonView: View {
var workoutClass: WorkoutClass
#ObservedObject var viewModel: WorkoutClassViewModel
#EnvironmentObject var authViewModel: AuthViewModel
var body: some View {
if checkIsReserved(uid: authViewModel.user?.uid ?? "", reservedUsers: workoutClass.reservedUsers ?? []) {
Button(action: {
viewModel.isDisabled = true
viewModel.removeReservation(
documentId: workoutClass.id!,
reservedUserDetails: ["uid": authViewModel.user?.uid as Any, "photoURL": authViewModel.user?.photoURL?.absoluteString ?? "" as Any, "displayName": authViewModel.user?.displayName ?? "Bruin Fitness Member" as Any],
uid: authViewModel.user?.uid ?? "")
}){
Label(
title: { Text("Cancel Reservation")
.font(.title) },
icon: { Image(systemName: "person.badge.minus")
.font(.title) }
)
}.disabled(viewModel.isDisabled)
} else{
Button(action: {
viewModel.isDisabled = true
viewModel.addReservation(
documentId: workoutClass.id!,
reservedUserDetails: ["uid": authViewModel.user?.uid as Any, "photoURL": authViewModel.user?.photoURL?.absoluteString ?? "" as Any, "displayName": authViewModel.user?.displayName ?? "Bruin Fitness Member" as Any],
uid: authViewModel.user?.uid ?? "")
}){
Label(
title: { Text("Reserve")
.font(.title) },
icon: { Image(systemName: "person.badge.plus")
.font(.title) }
)
}
.disabled(viewModel.isDisabled)
}
}
}
func checkIsReserved(uid: String, reservedUsers: [reservedUser]) -> Bool {
return reservedUsers.contains { $0.uid == uid }
}
WorkoutClassModel
struct reservedUser: Codable, Identifiable {
var id: String = UUID().uuidString
var uid: String
var photoURL: URL?
var displayName: String?
enum CodingKeys: String, CodingKey {
case uid
case photoURL
case displayName
}
}
struct WorkoutClass: Codable,Identifiable {
#DocumentID var id: String?
var reservationCnt: Int
var time: String
var workoutType: String
var reservedUsers: [reservedUser]?
enum CodingKeys: String, CodingKey {
case id
case reservationCnt
case time
case workoutType
case reservedUsers
}
}
WorkoutClassViewModel
class WorkoutClassViewModel: ObservableObject {
#Published var isDisabled = false
private var db = Firestore.firestore()
func addReservation(documentId: String, reservedUserDetails: [String: Any], uid: String){
let incrementValue: Int64 = 1
let increment = FieldValue.increment(incrementValue)
let addUser = FieldValue.arrayUnion([reservedUserDetails])
let classReference = db.document("schedules/Redwood City/dates/\(self.stateDate.dbDateFormat)/classes/\(documentId)")
db.runTransaction { transaction, errorPointer in
let classDocument: DocumentSnapshot
do {
print("Getting classDocument for docId: \(documentId) in addReservedUser()")
try classDocument = transaction.getDocument(classReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let workoutClass = try? classDocument.data(as: WorkoutClass.self) else {
let error = NSError(
domain: "AppErrorDomain",
code: -3,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve workoutClass from snapshot \(classDocument)"
]
)
errorPointer?.pointee = error
return nil
}
let isReserved = self.checkIsReserved(uid: uid, reservedUsers: workoutClass.reservedUsers ?? [])
if isReserved {
print("user is already in class so therefore can't be added again")
return nil
} else {
transaction.updateData(["reservationCnt": increment, "reservedUsers": addUser], forDocument: classReference)
return nil
}
} completion: { object, error in
if let error = error {
print(error.localizedDescription)
self.isDisabled = false
} else {
print("Successfully ran transaction with object: \(object ?? "")")
self.isDisabled = false
}
}
}
func removeReservation(documentId: String, reservedUserDetails: [String: Any], uid: String){
let decrementValue: Int64 = -1
let decrement = FieldValue.increment(decrementValue)
let removeUser = FieldValue.arrayRemove([reservedUserDetails])
let classReference = db.document("schedules/Redwood City/dates/\(self.stateDate.dbDateFormat)/classes/\(documentId)")
db.runTransaction { transaction, errorPointer in
let classDocument: DocumentSnapshot
do {
print("Getting classDocument for docId: \(documentId) in addReservedUser()")
try classDocument = transaction.getDocument(classReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let workoutClass = try? classDocument.data(as: WorkoutClass.self) else {
let error = NSError(
domain: "AppErrorDomain",
code: -3,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve reservedUsers from snapshot \(classDocument)"
]
)
errorPointer?.pointee = error
return nil
}
let isReserved = self.checkIsReserved(uid: uid, reservedUsers: workoutClass.reservedUsers ?? [] )
if isReserved {
transaction.updateData(["reservationCnt": decrement, "reservedUsers": removeUser], forDocument: classReference)
return nil
} else {
print("user not in class so therefore can't be removed")
return nil
}
} completion: { object, error in
if let error = error {
print(error.localizedDescription)
self.isDisabled = false
} else {
print("Successfully ran removeReservation transaction with object: \(object ?? "")")
self.isDisabled = false
}
}
}
func checkIsReserved(uid: String, reservedUsers: [reservedUser]) -> Bool {
return reservedUsers.contains { $0.uid == uid }
}
}
App screenshot
Reservation button is the green/grey button at the bottom of the view
As this is a race condition, You have already acknowledged the use of Transactions for the update which is the most desirable as this can ensure the update is successful before allowing the App to change button status.
I.e. by using a transaction and only updating the UI Button state on success, which is explained here
The recommendation is to keep the state of the button mapped to what is in the document, therefore you are likely to exceed rate limits by updating the same field continuously based on the flipping of the button.
Another way to handle this tracking of the state of enrollment is to add a new document that indicates the state of the enrollment for the user to a collection that is the class they are enrolling in.
I.e. Rather than having the class user enrolling into being a document, make that a collection and each time the enrollment state changes, write a new document. This will allow for updates to occur without using transactions and the current state of enrollments is contained within the latest document. This latest document can be read and used as the status of the button within the App with the added benefit that the state will always update to the status contained within Firestore.
I ended up resolving this by adding a disable check conditional before the conditional that decides whether to show the "Reserve" or "Cancel" button.
This way when my Firestore transaction is running the user will see a spinner instead and can't monkey test the button. The spinner helps to show that the reservation operation is in progress. When the transaction hits its completion block I disable the isDisabled Bool and the listener is in sync (the user then sees the newly toggled button state)
if workoutClassVM.isDisabled {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color("bruinGreenColor")))
} else if checkIsReserved(uid: authVM.user?.uid ?? "", reservedUsers: workoutClass.reservedUsers ?? []) {
...
Using SwiftUI, Xcode12.5.1, Swift5.4.2, iOS14.7.1,
My Firebase-Email/Password Login-page shall be extended with other Login possibilities such as Apple-Login (eventually Google-login, Facebook-login etc).
My steps:
log in with Email/Password to Firebase
log out
log in with "Sign in with Apple"
--> Then I get the following error:
Error Domain=FIRAuthErrorDomain Code=17007
"The email address is already in use by another account."
UserInfo={NSLocalizedDescription=The email address is already in use by another account.,
FIRAuthErrorUserInfoNameKey=ERROR_EMAIL_ALREADY_IN_USE}
What I intended to do is to link the existing Email/Password-Firebase-Account to the Sign in with Apple-Account (as described here and here).
But for doing that I would need the error FIRAuthErrorUserInfoUpdatedCredentialKey that allows to retrieve the old user eventually.
In my case, I get ERROR_EMAIL_ALREADY_IN_USE which does not lead to any old user to be linked.
What do I have to do ?
Here is my code:
let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce)
Auth.auth().signIn(with: credential) { (authResult, error) in
if (error != nil) {
print(error?.localizedDescription as Any)
return
}
print("signed in with Apple...")
do {
// if user did log in with Email/Password previously
if let email = try THKeychain.getEmail(),
let password = try THKeychain.getPassword() {
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
if let user = authResult?.user {
// here I am trying to link the existing Firebase-Email/Password account to the just signed-in with Apple account
user.link(with: credential) { (result, linkError) in
print(linkError) // this is where I get FIRAuthErrorUserInfoNameKey=ERROR_EMAIL_ALREADY_IN_USE
// unfortunately, the two accounts are not linked as expected due to this error !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// What is missing ??????????????????
loginStatus = true
}
}
} else {
loginStatus = true
}
} catch {
print(error.localizedDescription)
}
}
On the Firebase-documentation it sais:
Sign in with Apple will not allow you to reuse an auth credential to link to an existing account. If you want to link a Sign in with Apple credential to another account, you must first attempt to link the accounts using the old Sign in with Apple credential and then examine the error returned to find a new credential. The new credential will be located in the error's userInfo dictionary and can be accessed via the FIRAuthErrorUserInfoUpdatedCredentialKey key.
What does the part "...If you want to link a Sign in with Apple credential to another account, you must first attempt to link the accounts using the old Sign in with Apple credential..." exactly mean ? WHAT IS THE old Sign in with Apple credential ????????
And how would I do that ?
In fact, at the linking-call, I actually expected some sort of linkError.userInfo with an updated user to sign in with. But the linkError in my example only gives me the ERROR_EMAIL_ALREADY_IN_USE error without further userInfo.
As Peter Friese mentions in his Blog, I should somehow be able to retrieve a AuthErrorUserInfoUpdatedCredentialKey from the error.userInfo. But in my case, the linkError does not have any kind of such information - unfortunately!
Here is an excerpt of Peter's example: (again not applicable in my case for some unknown reason?????)
currentUser.link(with: credential) { (result, error) in // (1)
if let error = error, (error as NSError).code == AuthErrorCode.credentialAlreadyInUse.rawValue { // (2)
print("The user you're signing in with has already been linked, signing in to the new user and migrating the anonymous users [\(currentUser.uid)] tasks.")
if let updatedCredential = (error as NSError).userInfo[AuthErrorUserInfoUpdatedCredentialKey] as? OAuthCredential {
print("Signing in using the updated credentials")
Auth.auth().signIn(with: updatedCredential) { (result, error) in
if let user = result?.user {
// TODO: handle data migration
self.doSignIn(appleIDCredential: appleIDCredential, user: user) // (3)
}
}
}
}
}
Reversing the order of linking made me advance a tiny bit.
If I press the Sign in with Apple button, my code now logs in with Firebase-Email/Password first (i.e. the necessary credentials are taken from the Keychain). And on a second step, links with the Apple-credentials. And by doing so, the linking finally gives me the desired AuthErrorUserInfoUpdatedCredentialKey in the link-callback.
There I retrieve the updatedCredential to log in with Apple.
See code below.
HOWEVER, I STILL DON'T KNOW WHY AFTER LOGIN THIS WAY, MY DATA IS STILL MISSING ???????
HOW DOES THIS DATA-MIGRATION STEP WORK ???
Shouldn't the user.link(with: appleCredentials) { ... } do the job ?
What do I need to do in order to get the very same Firebase-Data, no matter the login method ???
let appleCredentials = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce)
do {
// if user did log in with Email/Password anytime before
if let email = try THKeychain.getEmail(),
let password = try THKeychain.getPassword() {
let firebaseEmailCredentials = EmailAuthProvider.credential(withEmail: email, password: password)
Auth.auth().signIn(with: firebaseEmailCredentials) { (authResult, error) in
if let user = authResult?.user {
user.link(with: appleCredentials) { (result, linkError) in
if let linkError = linkError, (linkError as NSError).code == AuthErrorCode.credentialAlreadyInUse.rawValue {
print("The user you're signing in with has been linked.")
print("Signing in to Apple and migrating the email/pw-firebase-users [\(user.uid)]` data.")
if let updatedCredential = (linkError as NSError).userInfo[AuthErrorUserInfoUpdatedCredentialKey] as? OAuthCredential {
print("Signing in using the updated credentials")
Auth.auth().signIn(with: updatedCredential) { (result, error) in
if let _ = result?.user {
print("signed in with Apple...")
// TODO: handle data migration
print("Data-migration takes place now...")
loginStatus = true
}
}
}
}
else if let error = error {
print("Error trying to link user: \(error.localizedDescription)")
}
else {
if let _ = result?.user {
loginStatus = true
}
}
}
}
}
} else {
// case where user never logged in with firebase-Email/Password before
Auth.auth().signIn(with: appleCredentials) { (result, error) in
if let _ = result?.user {
print("signed in with Apple...")
loginStatus = true
}
}
}
} catch {
print(error.localizedDescription)
}
Below is the code for my signup page. I want to make it so that when someone creates an account on the sign up page, I create a document in the users collection and include uuid in the document. However, session.session?.uid ends up being nil. Does anyone know why this is?
struct SignUpView: View {
#State var email = ""
#State var password = ""
#State var name = ""
#State var error = ""
#EnvironmentObject var session: SessionStore
func signUp() {
let db = Firestore.firestore()
let user = db.collection("users").document()
let test = db.collection("users").document(user.documentID).collection("routines").document()
session.signUp(email: email, password: password) { (result, error) in
if let error = error {
self.error = error.localizedDescription
print("This is the error \(error)")
return
} else {
self.email = ""
self.password = ""
}
}
user.setData(["id": user.documentID, "email": email]) { (err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
}
print(session.session?.uid)
test.setData(["id:": test.documentID, "msg": "samwell Tarly", "uuid": session.session?.uid]) { (err) in
print("ummmmm test data?")
if err != nil {
print((err?.localizedDescription)!)
return
}
}
}
The Firebase APIs are asynchronous, simply because they access a remote system, across the internet, which takes a little time. The same applies for accessing the local disk, by the way. This blog post explains this in more detail.
Consequently, session.signUp is an asynchronous process. I.e. the call to print(session.session?.uid) is executed before session.signUp returns. Thus, session.session?.uid is still nil.
To work around this, you can nest your calls like this:
session.signUp(email: email, password: password) { (result, error) in
if let error = error {
self.error = error.localizedDescription
print("This is the error \(error)")
return
}
else {
self.email = ""
self.password = ""
user.setData(["id": user.documentID, "email": email]) { (err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
}
}
}
Generally speaking, I would strongly recommend to not perform so much logic in your views, but instead keep your views as anaemic as possible - meaning: put all your logic into view models, and bind the view to the view models by using Combine. This will make your code much cleaner, easier to test, and maintainable.
See https://peterfriese.dev/replicating-reminder-swiftui-firebase-part2/ for how to do this.
I would like to download from firebase:
data issued from a group profile (Firebase realtime DB)
including...
data issued from the group admin profile (Firebase realtime DB)
a group profile image (Firebase Storage)
Then I can instantiate a group object with its data and its image
First approach, I used 3 nested closures that allowed me to get data, and then to get the image.
It did work, but it was quite long to get sequentially all that stuffs from firebase.
So I tried to use GCD in order to push my 2 latest Firebase queries (user data + group image) at the same time (rather than one after the other), and to wait for the last callback to start instantiating my group.
Is it a correct approach ?
If yes, I find some difficulties to implement it...
My issue : returnedUser and returnedGroupImage are always nil
Here is my bunch of code :
static func getGroup(_ groupID:String, completionBlock: #escaping (_ group: Group?) -> ()) {
dataRef.child("data").child("groups").child(groupID).observe(.value, with: { (snapshot) in
if let snapshotValue = snapshot.value {
guard let name = (snapshotValue as AnyObject).object(forKey: "name") as? String else
{
completionBlock(nil)
return
}
guard let adminID = (snapshotValue as AnyObject).object(forKey: "adminID") as? String else
{
completionBlock(nil)
return
}
let queue = DispatchQueue(label: "asyncQueue", attributes: .concurrent, target: .main)
let dispatch_group = DispatchGroup()
var returnedUser: User?
var returnedGroupImage: UIImage?
queue.async (group: dispatch_group) {
FireBaseHelper.getUser(adminID, completionBlock: { (user) in
if user != nil {
returnedUser = user
}
})
}
queue.async (group: dispatch_group) {
FireBaseHelper.getGroupImage(groupID, completionBlock: { (image) in
if image != nil {
returnedGroupImage = image
}
})
}
dispatch_group.notify(queue: DispatchQueue.main) {
// Single callback that is supposed to be executed after all tasks are complete.
if (returnedUser == nil) || (returnedGroupImage == nil) {
// always true !
return
}
let returnedGroup = Group(knownID: (snapshotValue as AnyObject).key, named: name, createdByUser: currentUser!)
returnedGroup.groupImage = returnedGroupImage
completionBlock(returnedGroup)
}
}
})
}
Thanks for your help !
I believe that the way you are using DispatchGroups are not correct.
let dispatch_group = DispatchGroup()
var returnedUser: User?
var returnedGroupImage: UIImage?
dispatch_group.enter()
FireBaseHelper.getUser(adminID, completionBlock: { (user) in
if user != nil {
returnedUser = user
}
dispatch_group.leave()
})
dispatch_group.enter()
FireBaseHelper.getGroupImage(groupID, completionBlock: { (image) in
if image != nil {
returnedGroupImage = image
}
dispatch_group.leave()
})
dispatch_group.notify(queue: DispatchQueue.main) {
// Single callback that is supposed to be executed after all tasks are complete.
if (returnedUser == nil) || (returnedGroupImage == nil) {
// always true !
return
}
let returnedGroup = Group(knownID: (snapshotValue as AnyObject).key, named: name, createdByUser: currentUser!)
returnedGroup.groupImage = returnedGroupImage
completionBlock(returnedGroup)
}