I have implemented firebase phone auth to verify phone number in my project and its working fine for me, But not able to update phone number. Like if a user had logged in with phone number A and now he wants to update this to phone number B. How will it be solved?
I have found an answer where you log in with your email account and update the mobile number into the same account. You may use the solution to log in with phone and update the phone number into the same account and see if it works. Anyway I will be working on this exact solution down in my project and update the answer then. But until then you can try to see if this works. Follow the regular firebase phone auth procedure as given here : https://firebase.google.com/docs/auth/ios/phone-auth
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationID, error) in
if let error = error {
self.showMessagePrompt(error.localizedDescription)
return
}
// Sign in using the verificationID and the code sent to the user
// ...
}
let credential = PhoneAuthProvider.provider().credential(
withVerificationID: verificationID,
verificationCode: verificationCode)
Then do not use the following code
// Sign In The User
Auth.auth().signInAndRetrieveData(with: credential) { _, error in
}
But use this code
Auth.auth().currentUser?.linkAndRetrieveData(with: credential, completion: { _, error in
if error == nil {
print("Whopdee doo")
} else {
print("Aargh!!!")
}
})
Related
Updated question
I am trying to manually check if the user is has to be reauthenticated or not. This is what I've come up with:
//MARK: updateEmail
static func updateEmail(email: String, finished: #escaping (_ done: Bool, _ hasToReauthenticate: Bool) -> Void) {
let currentUser = Auth.auth().currentUser
currentUser?.updateEmail(to: email) { err in
if err != nil {
if let errCode = AuthErrorCode(rawValue: err!._code) {
switch errCode {
case .userTokenExpired:
print("expired")
finished(true, true)
break
default:
Utilities.showErrorPopUp(labelContent: "Fehler", description: err!.localizedDescription)
finished(false, false)
}
}
} else {
finished(true, false)
}
}
}
But this is never going through the .userTokenExpired case even when it should.. What am I missing here ?
There is no API in Firebase Authentication that returns when the user has last authenticated, or whether that was recently. The only built-in functionality is that Firebase automatically checks for recent authentication for certain sensitive operations, but that seems to be of no use to you here.
But since your application is making API calls when the user authenticates, you can also record the time when they do so, and then check whether that was recent enough for your use-case.
If you need to check if user is authenicated - is same as reauthenication. Firebase will do their work to do some lower levels like tokens, etc. We don't have to worry about it.
guard let currentUser = Auth.auth().currentUser else {
//authenicate the user.
}
if you want to update the email address in user, the logic should be
check if the user is not nil, then update the email address.
If it is nil, then log in (anonymous or regular workflow to sign in), then update the email address.
I use this similar logic to check if the user is signed in, then do something. Otherwise, sign in as anonymous, then do same something.
The issue was quite simple: I caught the wrong error:
The error I have to catch in my case is .requiresRecentLogin . With that, everything is working fine.
After many tests I decided to create a new xCode project to better understand Firebase authentication with multiple providers.
I set up in Firebase -> SignIn Methods -> An account per email address
An account per email address
Prevents users from creating multiple
accounts using the same email address with different authentication
providers
At this point I have implemented, carefully following the Firebase guide, the login with Facebook and with Google .. Everything seems to work perfectly but I always find myself with the same error that I can't manage:
When my user creates a Firebase account via Google he is no longer able to log in if he decides to use Facebook.
Facebook returns its error when it completes its authentication flow with Firebase:
Firebase Error With Facebook Provider: An account already exists with the same email address but different sign-in credentials. Sign in using a provider associated with this email address.
Continuing to follow the documentation step by step I stopped here (firebase explains how to handle this error)
I have also implemented error handling but after calling Auth.auth().fetchSignInMethods Firebase says I should authenticate the user with the existing provider, at this point how do I get the credentials for authentication with the existing provider?
I wouldn't want to reopen the existing provider controller to get new credentials
Am I obliged to ask the user to log in with the existing provider and show another access controller again (in this case that of Google)?
How should I handle this situation?
override func viewDidLoad() {
super.viewDidLoad()
facebookSetup()
}
func facebookSetup() {
let loginButton = FBLoginButton(permissions: [ .publicProfile, .email ])
loginButton.center = view.center
loginButton.delegate = self
view.addSubview(loginButton)
}
//MARK: - FACEBOOK Delegate
func loginButton(_ loginButton: FBLoginButton, didCompleteWith result: LoginManagerLoginResult?, error: Error?) {
if let error = error {
print(error.localizedDescription)
return
}
let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print("\n FIREBASE: ",error.localizedDescription)
// An account with the same email already exists.
if (error as NSError?)?.code == AuthErrorCode.accountExistsWithDifferentCredential.rawValue {
// Get pending credential and email of existing account.
let existingAcctEmail = (error as NSError).userInfo[AuthErrorUserInfoEmailKey] as! String
let pendingCred = (error as NSError).userInfo[AuthErrorUserInfoUpdatedCredentialKey] as! AuthCredential
// Lookup existing account identifier by the email.
Auth.auth().fetchSignInMethods(forEmail: existingAcctEmail) { providers, error in
if (providers?.contains(GoogleAuthProviderID))! {
// Sign in with existing account.
Auth.auth().signIn(with: "? ? ? ?") { user, error in
// Successfully signed in.
if user != nil {
// Link pending credential to account.
Auth.auth().currentUser?.link(with: pendingCred) { result, error in
// Link Facebook to Google Account
}
}
}
}
}
}
}
}
I was working on the user profile page of my app and I am allowing the user to change their email address. If the user email address is changed successfully, the data in the firebase database of the particular user will be updated. Also, After successfully changing the email address, firebase will send an email to the user's previous email address (the user email address before it was changed to the new one) asking if it was the actual owner of the account who changed the email address and there will be a link to reset their email. If the user chooses to reset the email (for whatever reason), the user's new email will be changed to the previous email. But the problem is that the data in the database will not be updated, how can I detect this change (email reset) and update the database?
authenticateUserAlert.addAction(UIAlertAction(title: "Done", style: .default, handler: { [weak authenticateUserAlert] (_) in
// Print the user email
let emailTextField = authenticateUserAlert?.textFields![0]
print("Email: \(emailTextField!.text!)")
// Print the user password
let passwordTextField = authenticateUserAlert?.textFields![1]
print("Password: \(passwordTextField!.text!)")
// Re-authenticate the user
let user = Auth.auth().currentUser
let credential = EmailAuthProvider.credential(withEmail: emailTextField!.text!, password: passwordTextField!.text!)
user?.reauthenticate(with: credential, completion: { (result, error) in
if error != nil {
// Alert: What ever the error
print(error!.localizedDescription)
Alerts.errorAlert(on: vc, error: error!.localizedDescription, dismissAlert: false)
} else {
print(result!)
let editProfilePage = EditUserProfile()
editProfilePage.updateUserInfo()
}
})
}))
Here is what I tried according to an answer
Auth.auth().currentUser?.reload(completion: { (Error) in
//Completion handler
if let email = Auth.auth().currentUser?.email {
UserDataRetrieval.userEmail = email
self.emailLabel.text = email
print(Auth.auth().currentUser?.email)
}
})
It depends on the type of authentication you are using but honestly you should just use the email that is part of the authenticated account and then you don't need to worry about updating it in the database.
You can always just get the users email by using Auth.auth().currentUser.email
Update
Found a workaround to the issue of the credential data, try using
Auth.auth().currentUser?.reload(completion: { (Error) in
if (Error != nil) {
//Do something with error
} else {
//Do something with success or do nothing
}
})
Just call update credentials at the start of the app if you want to always have to most up to date credentials
You can always build your own custom handler for the email change revocation landing page. In that landing page you can update your database.
Check the official docs on how to do that.
I am trying to link Facebook and Google. So, the scenario is this:
I have already authenticated with Google. So, now I am logging in Facebook, having same email id which was used earlier with Google. So, I get the error of account Exists with a different credential. And, I did this:
func fetchUserInfo()
{
Auth.auth().signInAndRetrieveData(with:FacebookAuthProvider.credential(withAccessToken: (FBSDKAccessToken.current().tokenString)!), completion: { (result, error) in
if let error = AuthErrorCode.init(rawValue: error!._code)
{
switch error
{
case .accountExistsWithDifferentCredential :
let credential = FacebookAuthProvider.credential(withAccessToken: (FBSDKAccessToken.current()?.tokenString)!)
Auth.auth().currentUser?.linkAndRetrieveData(with: credential, completion: { (result, error) in
if let error = error
{
print("Unable to link Facebook Account", error.localizedDescription)
}
else
{
NavigationHelper.shared.moveToHome(fromVC: self)
}
})
default: break
}
}
else
{
GeneralHelper.shared.keepLoggedIn()
if let currentUser = Auth.auth().currentUser
{
print(currentUser.email!)
}
NavigationHelper.shared.moveToHome(fromVC: self)
}
})
}
Here Firebase Documentation says that we need to just link the currentUser and retrieve data. But, the issue I am facing is that the currentUser is always nil. So, how can I get the current user? I have already tried this months ago and then I was able to link Facebook, Google and Email. Do, I need to signInAndRetrieve the data from Google in order to get the currentUser?
The Error "account Exists with a different credential" is because, by default, Firebase do not allow to use the same email address for two (or more) different Sing In methods. You need to enable this option.
1 - Go to Authentication > Sign-in method
2 - Scroll down to Advanced: Multiple accounts per email address
3 - Change the option to Allow creation of multiple accounts with the same email address
FYI: You need to do whole login process for each Sign In method in your app. Each method has is own credentials.
Hope this helps.
When a user logs into the app using Facebook, I am able to capture and display their full name; however, neither email nor phone number is coming across. I have tried both the "One account per email address" as well as "Multiple accounts per email". I have tested it with an account whose email address is definitely not already registered in Firebase. What am I missing such that email/phone are not being captured? This all does work with Google accounts.
let name = Auth.auth().currentUser?.displayName // works!
let email = Auth.auth().currentUser?.email // nil- why?
let phone = Auth.auth().currentUser?.phoneNumber // nil -why?
The login process, which is standard Firebase w/ Facebook, looks like this:
func loginButton(_ loginButton: FBSDKLoginButton!, didCompleteWith result: FBSDKLoginManagerLoginResult!, error: Error!)
{
if let error = error
{
print(error.localizedDescription)
}
else
{
if FBSDKAccessToken.current() != nil
{
let credential = FacebookAuthProvider.credential(withAccessToken: FBSDKAccessToken.current().tokenString)
Auth.auth().signIn(with: credential) { (user, error) in
if let error = error
{
print (error.localizedDescription)
}
}
}
}
}
To update based on comments below. The following also produces a nil email and phone when inspecting the contents of userInfo. I understand the phone might just be that way, but it seems the email was supposed to work.
let userInfo = Auth.auth().currentUser?.providerData
The top level phoneNumber currentUser.phoneNumber is only for Firebase verified phone numbers. If you have that Facebook phone number, you can use the currentUser.updatePhoneNumber API to verify that number (you will need to go through the whole flow). Otherwise, you can wait for the upcoming Admin node.js API to set phone numbers with Admin privileges on existing users: https://github.com/firebase/firebase-admin-node/commit/68563c4b2c8128fbc45fc65bad3f6730d320b539
As for the email, in the case of "multiple accounts per email" you need to set it yourself via currentUser.updateEmail. You can get the Facebook email from currentUser.providerData which contains the Facebook provider data.