2
votes

please help me finding correct solution in the following situation.

I am developing ios app with swift which will use Firebase as a backend.

Users should be able to login into firebase with email/password or/and with facebook. Maybe google will be added later.

It is important for me to have one firebase account for each real user as user will have reward points and it won't make sense if on one devise(with email login) he will have X points and on other device(with facebook) he will have different points.

Here is code which I am using to login with email:

  //EMAIL REGISTER  
    @IBAction func registerAction(_ sender: Any) {

        if self.emailTextField.text == "" || self.passwordTextField.text == "" {

            Print( "Please enter email and password.")

        } else {

            FIRAuth.auth()?.createUser(withEmail: self.emailTextField.text!, password: self.passwordTextField.text!, completion: { (user, error) in

                if error == nil {
                    self.logoutButton.isHidden = false
                    self.usernameLabel.text = user!.email
                    self.emailTextField.text = ""
                    self.passwordTextField.text = ""
                } else {
                    Print("\((error?.localizedDescription)!)")
                }

            } )

        }
    }

    //EMAIL LOGIN
    @IBAction func loginAction(_ sender: Any) {

        if self.emailTextField.text == "" || self.passwordTextField.text == "" {
            print( "Please enter email and password.")
        } else {

            FIRAuth.auth()?.signIn(withEmail: self.emailTextField.text!, password: self.passwordTextField.text!, completion: { (user, error) in

                if error == nil {
                    self.logoutButton.isHidden = false
                    self.usernameLabel.text = user!.email
                    self.emailTextField.text = ""
                    self.passwordTextField.text = ""

                } else {

                    Print("\((error?.localizedDescription)!)")

                }
            })
        }
    }

Here is code which I am using to login with facebook:

  func loginButton(_ loginButton: FBSDKLoginButton!, didCompleteWith result: FBSDKLoginManagerLoginResult!, error: Error!) {

        self.fbLoginButton.isHidden = true

        if error != nil {

            self.fbLoginButton.isHidden = false
            print(error.localizedDescription)
            return

        } else if (result.isCancelled) {

            print("canceled")
            self.fbLoginButton.isHidden = false

        } else {

            let credential = FIRFacebookAuthProvider.credential(withAccessToken: FBSDKAccessToken.current().tokenString)

            FIRAuth.auth()?.signIn(with: credential) { (user, error) in
                if let error = error {
                    print(error.localizedDescription)
                    return
                }
            }
        }
    }

Both methods work fine, but they create two separate accounts.

It is set up to allow multiple users with the same email in settings: enter image description here

and I end up with the following result:

enter image description here

Obviously I want these two accounts to be merged automatically.

Documentation describes the way how to link auth providers, but user should be logged in with one method and only after that it will be possible to link accounts. linkWithCredential method is used if you already have the oauth credential. In case user was created on one device with email it wont work if he will decide to login again on other device with facebook.


After loggin in with any of these ways the FIRAuth.auth()?.addStateDidChangeListener service is showing second view controller where I am working with database.

override func viewDidLoad() { super.viewDidLoad()

let user = FIRAuth.auth()?.currentUser
let dat = user?.providerData

let email = user?.providerData[0].email
let name = user?.displayName

if email != nil {
self.ref.child("user_profile").child("\(user!.uid)/email").setValue(email)
}

if name != {
self.ref.child("user_profile").child("\(user!.uid)/name").setValue(name)
}

}

Let say we loggin with facebook, maybe it is possible to find UID of user with same email and run updates? Not very good idea in case there are a lot of users.

Other idea is to use email as id like this:

self.ref.child("user_profile").child("\(email)/name").setValue(name)

Not sure if it is a good option?

thanks for your replies.

1

1 Answers

1
votes

How to sign in a user from different device after he signed in using he's email and password on the first device without anonymous users.

After creating the user you need to save the email and password to NSUbiquitousKeyValueStore this way the other device will have access to email and password because you will need to sign in the user first before linking their account to Facebook as you will see below.

In FB console
1. Account Email address settings:Prevent creation of multiple accounts with the same email address This way the account doesn't get created again if the user already signed in with email address.

2.If the user choose email address as first login:

 var iCloudKeyStore: NSUbiquitousKeyValueStore = NSUbiquitousKeyValueStore()

FIRAuth.auth()?.createUser(withEmail: email, password: password) { (user, error) in

           // User created
            if error == nil{

                 // signed in successfully

                 //Save the password and the email. Or choose your way to save them
                 iCloudKeyStore.set(password, forKey: email)


            }else{

                //Erorr
            }
        }
  1. Now the user has an account in Firebase and will look like that:

enter image description here 4. Now on the other device the user decided to login with Facebook (different provider)

After you have signed in the user with Facebook and have fb's token: Usually inside the function's listener of FBSDKAccessTokenDidChange Notification, you sign in the user with Firebase with Facebook's token.

let credential = FIRFacebookAuthProvider.credential(withAccessToken: FBSDKAccessToken.current().tokenString)

    FIRAuth.auth()?.signIn(with: credential) { (user, error) in
        // ...
        if error != nil {
            // SETP 5. HERE

            let nsError = (error as! NSError)
            // Error: 17007 The email address is already in use by another account. ERROR_EMAIL_ALREADY_IN_USE

             if nsError.code == 17007{


                    print(error,user?.uid,nsError.code,"facebook user error")

                    let email = nsError.userInfo["FIRAuthErrorUserInfoEmailKey"] as? String
                    self.signInUserWithEmail(email: email!)
                    print("email",email)
                }


        }else{


            print(user!.uid,"facebook user login")
    }
  1. Since the user already have an account with this email address which he's now trying to sign in with Facebook, so an error will occurs: as you see in step 4, now you need to sign in the user before you link is account to Facebook:

    func signInUserWithEmail(email:String){
    
        let password = iCloudKeyStore.string(forKey: email)
    
    
        FIRAuth.auth()?.signIn(withEmail: email, password: password, completion: { (user:FIRUser?, error:Error?) in
    
    
            if user != nil{
    
                self.linkAccountWihtFacebook()
                print(user?.uid)
    
            }else{
    
                print(error?.localizedDescription)
            }
        })
    
    }
    

    func linkAccountWihtFacebook(){

        let credential = FIRFacebookAuthProvider.credential(withAccessToken: FBSDKAccessToken.current().tokenString)
    
            FIRAuth.auth()?.currentUser?.link(with: credential, completion: { (user:FIRUser?, error:Error?) in
    
                if let LinkedUser = user{
    
                    print("NEW USER:",LinkedUser.uid)
    
                }
    
                if let error = error as? NSError{
    
                    //Indicates an attempt to link a provider of a type already linked to this account.
                    if error.code == FIRAuthErrorCode.errorCodeProviderAlreadyLinked.rawValue{
                        print("FIRAuthErrorCode.errorCodeProviderAlreadyLinked")
                    }
    
                    //This credential is already associated with a different user account.
                    if error.code == 17025{
    
                    }
    
                    print("MyError",error)
                }
    
            })
    }
    
  2. This way you will have the following result in Firebase console:

enter image description here

Firebase documentation: https://firebase.google.com/docs/auth/ios/account-linking

Link auth provider credentials to a user account

To link auth provider credentials to an existing user account:

1. Sign in the user using any authentication provider or method.

  1. Complete the sign-in flow for the new authentication provider up to, but not including, calling one of the FIRAuth.signInWith methods. For example, get the user's Google ID token, Facebook access token, or email and password.
  2. Get a FIRAuthCredential for the new authentication provider