Bug while error handling Firebase Authentication with swiftUI - swift

This is my AuthViewModel:
#Published var userSession: User?
#Published var currentUser: AppUser?
#Published var signupError: Error?
#Published var loginError: Error?
static let shared = AuthViewModel()
init() {
self.userSession = Auth.auth().currentUser
fetchUser()
}
func fetchUser() {
guard let uid = userSession?.uid else { return }
USER_COLLECTION.document(uid).getDocument { snapshot, _ in
guard let user = try? snapshot?.data(as: AppUser.self) else { return }
self.currentUser = user
}
}
func registerUser(withEmail email: String, password: String) {
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if let error = error {
self.signupError = error
print("error")
return
}
guard let user = result?.user else { return }
let data: [String: Any] = ["uid": user.uid, "email": user.email ?? ""]
USER_COLLECTION.document(user.uid).setData(data) { err in
self.userSession = user
self.fetchUser()
}
}
}
func login(withEmail email: String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if let error = error {
self.loginError = error
return
}
guard let user = result?.user else { return }
self.userSession = user
self.fetchUser()
}
}
I have 2 published variables which are updated every time there is an error in their respective functions. However, when I click the 'sign up' button the first time with invalid credentials, I don't get an alert. I have to click it the second time to see the alert. Attached below is the code from the sign up view SwiftUI button with action and label. The same applies to the login view.
Button {
authViewModel.registerUser(withEmail: email, password: password)
print("called")
if let error = authViewModel.signupError {
alertTitle = "Error signing up!"
alertMessage = error.localizedDescription
alertShowing = true
print(alertShowing)
}
print(alertShowing)
} label: {
CustomAuthButton(text: "Sign Up")
}
Below is the code attached to the sign up view (navigation view):
.alert(alertTitle, isPresented: $alertShowing) {
Button("OK") {
authViewModel.signupError = nil
}
} message: {
Text(alertMessage)
}
Attached below is the code for registering a user. I'll put the result of those print statements below.
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if let error = error {
DispatchQueue.main.async {
self.signupError = error
print("error")
return
}
}
called
false
2022-07-06 12:59:54.424510+0530 InfoMax[62528:2751506] [boringssl] boringssl_metrics_log_metric_block_invoke(153) Failed to log metrics
error
As you can see, error is being printed late. When I tap the button the second time, it is printed before, so I get the alert.

Related

Firebase sign in not working after I sign out

So the first sign in into the Swift UI app works fine. I sign in and the view changes to the home view from the sign in view. However, after I log out, the sign in doesn't cause my view to change, so I assume it doesn't work.
Here is my AuthViewModel where I handle the sign in and sign out.
import FirebaseAuth
class AuthViewModel: ObservableObject {
static let shared = AuthViewModel()
private(set) var authSubscription: AuthStateDidChangeListenerHandle?
#Published var currentUser: User?
#Published var error: Error?
init() {
authSubscription = Auth.auth().addStateDidChangeListener { [weak self] (auth, user) in
self?.currentUser = user
}
}
func signIn(email: String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) { [weak self] (authResult, error) in
if let error = error {
self?.error = error
} else {
self?.error = nil
}
}
}
func signUp(email: String, password: String) {
Auth.auth().createUser(withEmail: email, password: password) { [weak self] (authResult, error) in
if let error = error {
self?.error = error
} else {
self?.error = nil
}
}
}
func signOut() {
do {
try Auth.auth().signOut()
} catch let error {
self.error = error
}
}
}
Also here is how I am switching from my sign up view to the home view
struct ContentView: View {
#State var screenOn = 0
#EnvironmentObject var authViewModel: AuthViewModel
var body: some View {
if let user = authViewModel.currentUser {
if screenOn == 0 {
someView(screenOn: $screenOn)
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
.onOpenURL { url in
let link = url.absoluteString
}
} else if screenOn == 1 {
someView(screenOn: $screenOn)
.onAppear {
authViewModel.signOut()
}
} else if screenOn == 3 {
someView(screenOn: $screenOn)
} else if screenOn == 4 {
someView(screenOn: $screenOn)
}
} else {
Loginpage()
}
}
}

Why is my swift code executing out of order? Firebase authentication

I am calling Authenticator.loginUser() with the expectation that the method will log in a user then call getCurrentUser(). Based on printed output, getCurrentUser() is executing first. Is there a way to force it to execute in order?
class Authenticator: ObservableObject {
#Published var currentUser: UserProfile = UserProfile()
#Published var user: String = ""
#Published var documentId: String = ""
func loginUser (email: String, password: String, viewModel: UsersViewModel) {
FirebaseAuth.Auth.auth().signIn(withEmail: email, password: password, completion: { result, error in
guard error == nil else {
print ("error: \(error!)")
return
}
print ("user signed in")
})
self.user = self.getCurrentUser(viewModel: viewModel)
}
func getCurrentUser(viewModel: UsersViewModel) -> String {
guard let userID = Auth.auth().currentUser?.uid else {
return ""
}
viewModel.users.forEach { i in
if (i.userId == userID) {
currentUser = i
}
}
documentId = currentUser.documentId!
print("auth.documentId \(documentId)")
return userID
}
}
FirebaseAuth.Auth.auth().signIn is asynchronous - it runs in the background and not on the main thread. This means that signIn will be called some time in the future.
A solution is to put the call to getCurrentUser in the completion block:
func loginUser(email: String, password: String, viewModel: UsersViewModel) {
FirebaseAuth.Auth.auth().signIn(withEmail: email, password: password, completion: { result, error in
guard error == nil else {
print("error: \(error!)")
return
}
print("user signed in")
self.user = self.getCurrentUser(viewModel: viewModel) // move here
})
}

How to use Firebase Auth + Combine to Sign-In and prevent Sign-In

So I've been following Log-In SwiftUI tutorials for Firebase and it's doing what it's supposed to for the most part: An error pops up when one or more of the input fields are left blank.
The problem occurs however, when I fill in the username + password fields with random gibberish. The view changes rather than popping up an error saying that the username is invalid (which I see in my console).
I've done some research and found that the problem might be due to the asynchronous behavior of Firebase and I haven't necessarily connected the error toggle to the sign-In result. But as a noob, I don't know how to implement the trailing closure in my code, and unsure where to go from here.
What change do I need to make exactly to ensure that upon failure of signing in with firebase, the view does not change and error pops up?
Here's my Sign-In function:
func signIn(){
error = false
session.signIn(email: user_account, password: password){
(result, error) in
if let errornew = error {
self.inputerror = errornew.localizedDescription
print("\(String(describing:error))")
self.error = true
self.alert.toggle()
} else {
self.user_account = ""
self.password = ""
}
}
}
Parent View:
struct ContentView: View {
#EnvironmentObject var session:SessionStore
#State var setUp = false
func getUser(){
session.listen()
}
var body: some View {
Group{
if(session.session != nil){
Text("App Home Page")
Text("Welcome")
Text("Email: \(session.session?.email ?? "")")
} else {
OpeningView() // Sign-In function is in a child view under OpeningView()
}
}.onAppear(perform: self.getUser)
}
Session Class:
class SessionStore: ObservableObject{
#Published var isSetUp:Bool?
var didChange = PassthroughSubject<SessionStore, Never>()
var session: User? {didSet {self.didChange.send(self)}}
var handle: AuthStateDidChangeListenerHandle?
let user = Auth.auth().currentUser
//checks to see whether or not we have a user
func listen(){
// monitor authentication changes using firebase
handle = Auth.auth().addStateDidChangeListener({ (auth, user) in
if let user = user {
//if we have a user, create a new user model
print("Got the user: \(user)")
self.session = User(uid: user.uid, email: user.email!)
} else {
//if not, then session is nil
self.session = nil
}
})
}
func signUp(email: String, password: String, handler: #escaping AuthDataResultCallback){
Auth.auth().createUser(withEmail: email, password: password, completion: handler)
}
func signIn(email: String, password: String, handler: #escaping AuthDataResultCallback){
Auth.auth().signIn(withEmail: email, password: password, completion: handler)
}
func login(withEmail email: String, password: String, _ callback: ((Error?) ->())? = nil){
Auth.auth().signIn(withEmail: email, password: password){(user, error) in
if let e = error{
callback?(e)
return
}
callback?(nil)
print("Login Successful")
}
}
func signOut(){
do{
try Auth.auth().signOut()
self.session = nil
} catch {
print("Error Signing Out.")
}
}
func unbind(){
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit{
unbind()
}
}
struct User {
var uid : String
var email : String?
init(uid: String, email: String?){
self.uid = uid
self.email = email
}
}
Your View needs something like this on top to check whether you are logged in or not:
var body: some View {
ZStack {
if Auth.auth().currentUser != nil {
// Your View when you are logged in.
else {
SignInView()
} }

"Cannot assign value of type 'AuthDataResult?' to type 'User?' and Value of type 'AuthDataResult' has no member 'uid'

I have problem in linecurrentUser = user which is cannot assgin value of type AuthDataResult?' to type User
currentUserId = (user?.uid)! -> Value of type AuthDataResult has no member uid
I could not figure how to implement the AuthDataResult in it. Please help
Thanks
class AuthFirebase: NSObject {
//This is instance of FIRDatabase to read and write data from Firebase database
static let dataBase = Database.database().reference()
static var currentUserID:String = ""
static var currentUser: User? = nil
//Create Function to Log In
static func LogIn(email:String, password:String, completion: #escaping(_ success: Bool ) ->
Void) {
Auth.auth().signIn(withEmail: email, password: password, completion:{ (user,error) in
if let error = error {
print(error.localizedDescription)
completion(false)
}
else {
currentUser = user
currentUserID = (user?.uid)!
completion(true) }
})
}
}
You have understood it wrong, the completion handler is returning AuthDataResult? and Error. To get the value of user, you have to do this:
Auth.auth().signIn(withEmail: collectionTF[0].text!, password: collectionTF[1].text!, completion: { (authDataResult, error) in
if let error = error
{
print(error)
}
else
{
if let user = authDataResult?.user { //This is the user
currentUser = user
currentUserId = user.uid
completion(true)
}
}
}
This is how you can get the User from AuthDataResult.
Last time I modified the code and it worked fine. However, Xcode run error on this issue again at currentUser=user line. as. Cannot assign value of Type User to type User?
static func LogIn(email:String, password:String, completion: #escaping(_ success: Bool ) ->
Void) {
Auth.auth().signIn (withEmail: email, password: password, completion:{ (AuthDataResult,error) in
if let error = error {
print(error.localizedDescription)
completion(false)
}
else {
if let user = AuthDataResult?.user {
currentUser = user << cannot assign value of type"User to type"User?'
currentUserID = user.uid
completion(true) }
}
}
)
}
}

How do I update UILabels synchronously with Firestore data?

I'm currently building an iOS app that will synchronize account information from Firestore. I have the login/register process hooked up and working. However, I need help understanding how to update my logInOutBtn, fullNameTxt and emailTxt in my MenuVC automatically when an user logs in/out. Currently, it will update whenever I close then reopen the menu, but what should I use to automatically update it without having to close the menu? Thanks!
// MenuVC
override func viewDidAppear(_ animated: Bool) {
if let user = Auth.auth().currentUser , !user.isAnonymous {
// We are logged in
logInOutBtn.setTitle("Logout", for: .normal)
if UserService.userListener == nil {
UserService.getCurrentUser {
self.fullNameTxt.text = UserService.user.fullName
self.emailTxt.text = UserService.user.email
}
}
} else {
logInOutBtn.setTitle("Login", for: .normal)
self.fullNameTxt.text = "Sign in or create an account"
self.emailTxt.text = "to continue."
}
}
fileprivate func presentLoginController() {
let storyboard = UIStoryboard(name: Storyboard.LoginStoryboard, bundle: nil)
if #available(iOS 13.0, *) {
let controller = storyboard.instantiateViewController(identifier: StoryboardId.LoginVC)
present(controller, animated: true, completion: nil)
} else {
// Fallback on earlier versions
}
}
#IBAction func logInOutClicked(_ sender: Any) {
guard let user = Auth.auth().currentUser else { return }
if user.isAnonymous {
presentLoginController()
} else {
do {
try Auth.auth().signOut()
UserService.logoutUser()
Auth.auth().signInAnonymously { (result, error) in
if let error = error {
debugPrint(error)
Auth.auth().handleFireAuthError(error: error, vc: self)
}
self.presentLoginController()
}
} catch {
debugPrint(error)
Auth.auth().handleFireAuthError(error: error, vc: self)
}
}
}
// UserService
func getCurrentUser(completion: #escaping () -> ()) {
guard let authUser = auth.currentUser else { return }
let userRef = db.collection("users").document(authUser.uid)
userListener = userRef.addSnapshotListener({ (snap, error) in
if let error = error {
debugPrint(error.localizedDescription)
return
}
guard let data = snap?.data() else { return }
self.user = User.init(data: data)
completion()
})
// User Model
struct User {
var fullName: String
var address: String
var id: String
var email: String
var stripeId: String
init(fullName: String = "",
address: String = "",
id: String = "",
email: String = "",
stripeId: String = "") {
self.fullName = fullName
self.address = address
self.id = id
self.email = email
self.stripeId = stripeId
}
init(data: [String : Any]) {
fullName = data["fullName"] as? String ?? ""
address = data["address"] as? String ?? ""
id = data["id"] as? String ?? ""
email = data["email"] as? String ?? ""
stripeId = data["stripeId"] as? String ?? ""
}
static func modelToData(user: User) -> [String : Any] {
let data : [String : Any] = [
"fullName" : user.fullName,
"address" : user.address,
"id" : user.id,
"email" : user.email,
"stripeId" : user.stripeId
]
return data
}
}
// My app menu
The signout process is pretty straightforward and is marked as throws so if it fails, it will generate an error that can be handled by a catch. It is not asynchronous so it won't have (or need) a closure.
So simply stated
func signOut() {
let firebaseAuth = Auth.auth()
do {
try firebaseAuth.signOut()
print("successful signout")
self.logInOutBtn.setTitle("Log In", for: .normal)
self.fullNameTxt.text = ""
self.emailTxt.text = ""
} catch let signOutError as NSError {
print ("Error signing out: %#", signOutError)
//present the error to the user/handle the error
}
}
The signIn function is asynchronous with a closure so when the user signs in successfully, the code in the closure will fire and that's the perfect place to update the UI.
Auth.auth().signIn(withEmail: email, password: password) { [weak self] authResult, error in
guard let strongSelf = self else { return }
// update the UI here.
}
You can also just monitor the authState with an observer and have it react to users logging in/out
self.authListener = Auth.auth()?.addAuthStateDidChangeListener { auth, user in
if let theUser = user {
print("User logged in \(theUser)") // User is signed in.
self.dismissViewControllerAnimated(true, completion: nil)
} else {
print("Need to login.") // No user is signed in.
//present login view controller
}
}
If you no longer want to observe the auth state, you can remove it with
Auth.auth()?.removeAuthStateDidChangeListener(self.authListener)