I have multiple login options for the user to choose from: email, google, and facebook. If Firebase already has the email stored as a user (ie. the user previously signed up with test#gmail.com), an alert that tells the user that an account with that email already exists. This works perfectly for Google sign in, as shown in this screenshot. However, nothing visually happens when the user clicks the Facebook button (doesn't even switch screens), and I get this error in the debugger:
Error Domain=FIRAuthErrorDomain Code=17012 "An account already exists with
the same email address but different sign-in credentials. Sign in using a
provider associated with this email address." UserInfo={FIRAuthErrorUserInfoNameKey=ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL,
FIRAuthErrorUserInfoEmailKey=318junkjabr#gmail.com,
FIRAuthErrorUserInfoUpdatedCredentialKey=<FIROAuthCredential:
0x6000005f3200>, NSLocalizedDescription=An account already exists with the
same email address but different sign-in credentials. Sign in using a
provider associated with this email address.}
This is my code for the Log In View Controller:
override func viewDidLoad() {
super.viewDidLoad()
setUpFBButton()
setUpGoogleButton()
setUpEmailButton()
GIDSignIn.sharedInstance()?.presentingViewController = self
}
// MARK: - SIGN UP WITH GOOGLE
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
if let err = error {
print("Failed to log into Google: ", err)
return
}
print("Successfully logged into Google")
guard let authentication = user.authentication else { return }
let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken, accessToken: authentication.accessToken)
// sign user in with Firebase
Auth.auth().signIn(with: credential, completion: { (user, error) in
let firstName = user?.user.displayName
let email = user?.user.email
let lastName = ""
let uid = user?.user.uid
if let err = error {
print("Failed to create a Firebase User with Google account: ", err)
return
} else {
// Successfully logged in
print("Successfully logged into Firebase with Google email: ", email ?? "", "Now add user to Firestore if user is new.")
// check if user already exists
self.addUserToFirestore(firstName ?? "", lastName, email ?? "", uid ?? "", "Google")
}
})
}
fileprivate func setUpGoogleButton() {
Utilities.styleLightFilledButton(signInGoogleButton)
signInGoogleButton!.addTarget(self, action:
#selector(handleCustomGoogleSignIn), for: .touchUpInside)
GIDSignIn.sharedInstance()?.delegate = self
}
#objc func handleCustomGoogleSignIn() {
GIDSignIn.sharedInstance().signIn()
}
// MARK: - SIGN UP WITH FACEBOOK
// design the facebook button and assign #selector to facebook button actions
fileprivate func setUpFBButton() {
Utilities.styleHollowButton(signInFacebookButton)
signInFacebookButton.addTarget(self, action: #selector(handleCustomFBButton), for: .touchUpInside)
}
// handle the facebook button actions
#objc func handleCustomFBButton() {
LoginManager().logIn(permissions: ["email", "public_profile"], from: self) { (result, err) in
if err != nil {
print("Custom FB login failed:", err!)
return
}
self.getUserInfo()
}
}
// grab id, name, and email of user
func getUserInfo() {
print("Successfully logged in with facebook...")
GraphRequest(graphPath: "/me", parameters: ["fields": "id, name, email"]).start {
(connection, result, err) in
guard let Info = result as? [String: Any] else { return }
let name = Info["name"] as? String
let email = Info["email"] as? String
let uid = Info["id"] as? String
if err != nil {
print("Failed to start graph request:", err!)
return
}
print(result!)
self.signIntoFirebase(name ?? "", email ?? "", uid ?? "")
}
}
// connect the user to firebase
func signIntoFirebase(_ name:String, _ email:String, _ uid:String) {
let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString)
Auth.auth().signIn(with: credential) { (user, error) in
if let err = error {
print(err)
return
} else {
print("Facebook user successfully authenticated with Firebase. Now run through Firestore.")
// check if user already exists. if user exists, go to chats screen. if it does not exist, create a new user and redirect to chat screen.
self.addUserToFirestore(name, "", email, uid, "Facebook")
}
}
}
func loginButtonDidLogOut(_ loginButton: FBLoginButton) {
print("Logged out of facebook")
}
// MARK: - Other functions
func addUserToFirestore(_ firstName:String, _ lastName:String, _ email:String, _ uid:String, _ signInMethod:String) {
let db = Firestore.firestore()
let docRef = db.collection("users").document(uid)
// check if user exists in firestore
docRef.getDocument { (document, error) in
if let document = document {
if document.exists {
let message = "Good news! You already have a Coal account that uses " + email + ".\nPlease sign in to your existing account. Then you will be able to link your " + signInMethod + " profile from your Account Settings page."
// user exists. send to chats screen.
print("User already exists. Document data: \(String(describing: document.data()))")
self.showError("You're already a member!", message)
} else {
// user does not exist. create a new user
print("Document does not exist. Create new user.")
docRef.setData(["firstname":firstName, "lastname":lastName, "email":email]) { err in
if err != nil {
// Show error message
print("Error saving user data to Firestore")
} else {
print("New user created in Firestore")
self.transitionToConvo()
}
}
}
}
}
}
func showError(_ title:String, _ message:String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertAction.Style.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
func transitionToConvo() {
let tabBarC = self.storyboard?.instantiateViewController(withIdentifier: "mainTabBarController") as! TabBarController
tabBarC.modalPresentationStyle = .fullScreen
self.present(tabBarC, animated: true, completion: nil)
print("Switched to TabBarController")
}
func setUpEmailButton() {
Utilities.styleDarkFilledButton(signInEmailButton)
}
} // end
I think the reason why it's not getting an alert is because the Facebook uid doesn't match the email uid saved in Firestore (while Google uid does match). The way I'm getting the alert to show up is if the uid matches that in Firestore, so consequently, the alert doesn't show. Does anyone know how I can get the alert to show an how to not get this error?
I know my code is a bit messy, so please let me know if you need further explanation. Any help is appreciated!!
After hours of trying to figure out my problem, I decided to post a question... but almost immediately after, found the answer (i know right :/).
Since one account per email is already enabled in the project settings, Auth.auth().signIn automatically scans if a user exists. If a user exists, it'll return an error after if error != nil. Because it returned an error before my addUserToFirestore function was called (to check if the user exists and if not, add user to Firestore), the alert was never shown.
Now that we know what if error != nil means, we can just insert the alert there:
Auth.auth().signIn(with: credential) { (user, error) in
if error != nil {
let message = "Good news! You already have a Coal account that uses " + email + ".\nPlease sign in to your existing account. Then you will be able to link your Facebook profile from your Account Settings page."
// user exists. send to chats screen.
print("User already exists. Let user know.")
self.showError("You're already a member!", message)
return
}
I'm not sure why it worked for Google Auth, but it ended up that this is what worked for Facebook Auth.
I have some problems with changing email address in firebase authentication.
My code looks like this now:
func changeEmail(withEmail email: String, completion: #escaping ((Bool) -> Void)) {
guard let currentUser = Auth.auth().currentUser, let email = mail else { return }
currentUser.updateEmail(to: email) { [weak self]
error in
guard let self = self else { return }
let title: String
let message: String
if let error = error {
title = "alert.error.title".localized()
message = error.localizedDescription
} else {
title = email
message = "auth.confirm.email.popup".localized()
currentUser.sendEmailVerification()
}
self.navigator.showAlert(title: title,
message: message,
bottomLeftTitle: "general.got.it".localized(),
bottomLeftHandler: { completion(error == nil)
})
}
}
So it is okey, and working, and user can actually change email.
But problem occurs when user stayed too long and needs to re-login. Everyone knows that it is disturbing user experience in app.
Auth.auth().reload() //not working in this situation.
So how to change email, without asking user to logout and login again?
There is a reauthenticate method exactly for this purpose.
https://firebase.google.com/docs/auth/ios/manage-users#re-authenticate_a_user
What you need to do is ask the user for its login credentials again. No logout - login needed.
Possible code for that:
if (self.newPassword == self.newPasswordConfirm) && (!(self.newPassword.isEmpty) || !(self.newUserName.isEmpty)) {
reauthenticate(email: self.accountEmail, password: self.oldPassword) { isSucceeded in
//Successfully authenticated
if isSucceeded == true {
if !self.newUserName.isEmpty {
// update username
}
Auth.auth().currentUser?.updatePassword(to: self.newPassword) { (error) in
// Alert user that it didn't work
}
self.editProfile.toggle()
}
// Failed to reauthenticate
else if isSucceeded == false {
// Alert User
}
}
}
In my SettingsViewModel I have the following:
class SettingsViewModel: ObservableObject {
func deleteUser(){
let userId = Auth.auth().currentUser!.uid
Firestore.firestore().collection("users").document(userId).delete() { err in
if let err = err {
print("error: \(err)")
} else {
print("Deleted user in db users")
Storage.storage().reference(forURL: "gs://myapp.appspot.com").child("avatar").child(userId).delete() { err in
if let err = err {
print("error: \(err)")
} else {
print("Deleted User image")
Auth.auth().currentUser!.delete { error in
if let error = error {
print("error deleting user - \(error)")
} else {
print("Account deleted")
}
}
}
}
}
}
}
}
In my setting view im calling the function in a button like so:
#ObservedObject var settingsViewModel = SettingsViewModel()
func logout() {
session.logout()
}
Button(action: {
self.showActionSheet = true
}) {
Text("Delete Account").foregroundColor(.white).padding()
}.background(Color.red)
.cornerRadius(10)
.padding(.top, 35)
.actionSheet(isPresented: self.$showActionSheet) {
ActionSheet(title: Text("Delete"), message: Text("Are you sure you want to delete your account?"),
buttons: [
.default(Text("Yes, delete my account."), action: {
self.deleteUser()
self.session.logout()
self.showActionSheet.toggle()
}),.cancel()
])
}
This doesn't work correctly as it deletes the account:
Auth.auth().currentUser!.delete { error in
if let error = error {
print("error deleting user - \(error)")
} else {
print("Account deleted")
}
}
And then signs out, leaving the other data, but if i remove:
Auth.auth().currentUser!.delete { error in
if let error = error {
print("error deleting user - \(error)")
} else {
print("Account deleted")
}
}
then it deletes the user data, but then doesn't delete the storage:
Storage.storage().reference(forURL: "gs://myapp.appspot.com").child("avatar").child(userId).delete()
Im trying to get it to flow so it deletes the user data, then the image, then the auth data and then logout out the app. all the functions work, but putting them together is causing an issue.
The recommended way for doing this is:
Write a Cloud Function that triggers when the user account is deleted in Firebase Auth (see user.onDelete())
In this function, delete all the user's data in Cloud Firestore, Cloud Storage
Trigger the function by deleting the user by calling user.delete()
You might want to check out the Delete User Data Extension, which covers step 1 and 2 for you.
At the moment, I am able to successfully ask the user for their permission to access their Contact information. I am handling this through a switch statement like so:
func requestContactPermissions() {
let store = CNContactStore()
var authStatus = CNContactStore.authorizationStatus(for: .contacts)
switch authStatus {
case .restricted:
print("User cannot grant permission, e.g. parental controls in force.")
exit(1)
case .denied:
print("User has explicitly denied permission.")
print("They have to grant it via Preferences app if they change their mind.")
exit(1)
case .notDetermined:
print("You need to request authorization via the API now.")
store.requestAccess(for: .contacts) { success, error in
if let error = error {
print("Not authorized to access contacts. Error = \(String(describing: error))")
exit(1)
}
if success {
print("Access granted")
}
}
case .authorized:
print("You are already authorized.")
#unknown default:
print("unknown case")
}
}
In the .notDetermined case, this is opening the dialog where I can either click no or yes, granting or denying the application access. This is fine and expected.
What I am looking to do, is change the view if the user clicks yes. Right now, I have the requestContactPermissions function within a button like so:
Button(action: {
withAnimation {
// TODO: Screen should not change until access is successfully given.
requestContactPermissions()
// This is where the view change is occurring.
self.loginSignupScreen = .findFriendsResults
}
})
How might I add in the logic to have the view change once the user has granted the application access to their contacts?
add a completion to the requestContactPermissions function something like this (I trimmed the irrelevant parts for the answer):
func requestContactPermissions(completion: #escaping (Bool) -> ()) {
let store = CNContactStore()
var authStatus = CNContactStore.authorizationStatus(for: .contacts)
switch authStatus {
case .notDetermined:
print("You need to request authorization via the API now.")
store.requestAccess(for: .contacts) { success, error in
if let error = error {
print("Not authorized to access contacts. Error = \(String(describing: error))")
exit(1)
//call completion for failure
completion(false)
}
if success {
//call completion for success
completion(true)
print("Access granted")
}
}
}
}
and then you can determine inside the closure whether the user granted permission or not:
Button(action: {
withAnimation {
// TODO: Screen should not change until access is successfully given.
requestContactPermissions { didGrantPermission in
if didGrantPermission {
//this is the part where you know if the user granted permission:
// This is where the view change is occurring.
self.loginSignupScreen = .findFriendsResults
}
}
}
})
I'm setting up a shareExtension in iOS and want to use the FirebaseSDK to upload data direct instead of using AppGroups. This works as expected, but after 1 hour the UserToken get's invalidated and i can't reach the Firestore Backend anymore.
I'm using the FirebaseSDK (6.2.0) and enabled Keychain sharing to access the current signedIn User. I have the same Google-Plist in the MainApp and the shareExtension. The data gets also uploaded correctly from the shareExtension and was also updated via the snapshotListener in the MainApp.
Relevant code in the MainApp
lazy var db = Firestore.firestore()
//TEAMID form the Apple Developer Portal
let accessGroup = "TEAMID.de.debug.fireAuthExample"
override func viewDidLoad() {
super.viewDidLoad()
do {
try Auth.auth().useUserAccessGroup("\(accessGroup)")
} catch let error as NSError {
print("Error changing user access group: %#", error)
}
guard let user = Auth.auth().currentUser else {
self.statusLabel.text = "user get's lost"
return
}
statusLabel.text = "UserID: \(user.uid)"
// Do any additional setup after loading the view.
db.collection("DummyCollection").addSnapshotListener { (querySnapshot, error) in
if let err = error {
print(err.localizedDescription)
}
guard let snapshot = querySnapshot else {
return
}
DispatchQueue.main.async {
self.dbCountLabel.text = "\(snapshot.count)"
}
}
}
func signIN(){
// https://firebase.google.com/docs/auth/ios/single-sign-on
do {
try Auth.auth().useUserAccessGroup("\(accessGroup)")
} catch let error as NSError {
print("Error changing user access group: %#", error)
}
Auth.auth().signInAnonymously { (result, error) in
if let err = error{
print(err.localizedDescription)
return
}
print("UserID: \(Auth.auth().currentUser!.uid)")
}
}
}
}
Code in the shareExtension:
override func viewDidLoad() {
super.viewDidLoad()
if FirebaseApp.app() == nil {
FirebaseApp.configure()
}
do {
try Auth.auth().useUserAccessGroup(accessGroup)
} catch let error as NSError {
print("Error changing user access group: %#", error)
}
tempUser = Auth.auth().currentUser
if tempUser != nil {
userIDLabel.text = "UserID: \(tempUser!.uid)"
doneButton.isEnabled = true
db.collection("DummyCollection").addSnapshotListener { (querySnapshot, error) in
if let err = error {
print(err.localizedDescription)
}
guard let snapshot = querySnapshot else {
return
}
DispatchQueue.main.async {
self.dataCountLabel.text = "\(snapshot.count)"
}
}
} else {
// No user exists in the access group
self.navigationItem.title = "No User"
}
}
I expect that this should be possible, but the Token gets somehow invalid in the MainApp and i could not reach the Firestore backend.
6.2.0 - [Firebase/Auth][I-AUT000003] Token auto-refresh re-scheduled in 01:00 because of error on previous refresh attempt.
6.2.0 - [Firebase/Firestore][I-FST000001] Could not reach Cloud Firestore backend. Connection failed 1 times. Most recent error: An internal error has occurred, print and inspect the error details for more information.
Answering my own question: This should be fixed in the next release (Firebase 6.4.0) Details can be found in this PR 3239.