Swift - Google Firebase Authentication with Email - ios

I am trying to run the example of Google Firebase Authentication with Email. As I tried the email example of https://github.com/firebase/quickstart-ios/blob/master/authentication/AuthenticationExampleSwift/EmailViewController.swift I get errors in the project.
My Code looks like this:
#IBAction func loginButtonTapped(_ sender: AnyObject) {
if let email = self.userEmailTextField.text, let password = self.userPasswordTextField.text {
showSpinner({
// [START headless_email_auth]
FIRAuth.auth()?.signIn(withEmail: email, password: password) { (user, error) in
// [START_EXCLUDE]
self.hideSpinner({
if let error = error {
self.showMessagePrompt(error.localizedDescription)
return
}
self.navigationController!.popViewController(animated: true)
})
// [END_EXCLUDE]
}
// [END headless_email_auth]
})
} else {
self.showMessagePrompt("email/password can't be empty")
}
}
I get an error on showSpinner({...}) and at the very end on self.showMessagePrompt("email/password can't be empty"):
However, the error from the very end showMessagePrompt does not appear on self.showMessagePrompt few lines before. Maybe it has to do with my Swift Version, I tried to convert to 3, but my complete project was broken after that.

Because in that quickstart-ios, they uses bridging-header of UIViewController+Alerts.h file which is not implemented by you and not added by you in your project.
So one solution is to use UIViewController+Alerts.h as bridging header in your view controller or remove/modify your code something like this code..
#IBAction func loginButtonTapped(_ sender: AnyObject) {
if let email = self.userEmailTextField.text, let password = self.userPasswordTextField.text {
// [START headless_email_auth]
FIRAuth.auth()?.signIn(withEmail: email, password: password) { (user, error) in
// [START_EXCLUDE]
if let error = error {
print(error.localizedDescription)
//show alert
return
}
self.navigationController!.popViewController(animated: true)
// [END_EXCLUDE]
}
// [END headless_email_auth]
} else {
print("email/password can't be empty")
//show alert
}
}
You can find UIViewController+Alerts.h and UIViewController+Alerts.m files here

Related

Firebase iOS Authentication - Performing segue

I'm currently adding Firebase Authentication to my iOS application. I can sign up and sign in users, however, I'm struggling to find where I can add a segue to move on to the next screen.
func signInUser(email: String, password: String){
// creates user with the firebase autenthication platform
Auth.auth().signIn(withEmail: email, password: password) { [weak self] authResult, error in
guard let strongSelf = self else { return }
}
// would the segue go here?
}
You should execute your code inside the signIn function completion handler:
func signInUser(email: String, password: String){
//creates user with the firebase autenthication platform
Auth.auth().signIn(withEmail: email, password: password) { [weak self] authResult, error in
guard let strongSelf = self else { return }
if let error = error as? NSError {
// Handle sign in error
switch AuthErrorCode(error.code) {
...
}
} else {
// No errors: Perform segue here...
}
}
}

Firebase Passwordless Email Authentication doesn't open app on iOS

I'm trying and failing badly to implement the cool Firebase email link login feature. I successfully setup sending an email link. However, I can't get the email link to open up the app. It just opens up the preview page like it can't open the app.
I've tested the dynamic link I setup and I can get it to open up the app in a device. I just can't get the email link to do the same.
Code in my app:
func sendFirebaseEmailLink() {
let actionCodeSettings = ActionCodeSettings.init()
// userEmail comes from a textField
let email = userEmail
actionCodeSettings.url = URL.init(string: String(format: "https://<myappname>.firebaseapp.com/?email=%#", email))
// The sign-in operation has to always be completed in the app.
actionCodeSettings.handleCodeInApp = true
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
Auth.auth().sendSignInLink(toEmail: email,
actionCodeSettings: actionCodeSettings) { error in
if let error = error {
print(error.localizedDescription)
return
}
else {
UserDefaults.standard.set(email, forKey: "Email")
print("email sent to user")
}
}
}
When I say I've successfully gotten my dynamic link to open the app what I mean is when I follow the link I created (mylinkname.page.link/emaillogin) on a device that has the app installed, it opens the app. Because of that and [this helpful Firebase video][1] on setting up a dynamic link it seems like I've got those details correct and the issue is with the code, but I'm new to this so I'm not sure.
I've spend few days going around in circles to figure this out, and trying to parse the dense Firebase documentation, so any ideas are greatly appreciated.
I finally figured it out. The code was fine. It was an issue related to the dynamic link. I had a couple links setup in Firebase because I had to create a new Bundle ID at one point. When I deleted out the old one in Firebase the email link started working.
It shows up in my app association site like this, and oddly still does even though I deleted out the old link, but at least it works now!
{"applinks":{"apps":[],"details":[{"appID":"TEAMID.com.OLDBUNDLEIDENTIFIER.APPNAME","paths":["NOT //*","/*"]},{"appID":"TEAMID.com.NEWBUNDLEIDENTIFIER.APPNAME","paths":["NOT //","/"]}]}}
UPDATE: My full code to implement passwordless email login is below. It was painful for me to piece together using the documentation so hopefully this saves you the trouble.
Key steps assuming you understand the basics of Firebase Setup.
1) Setup a Dynamic Link Using the Firebase Video tutorial.
2) Code in View Controller:
var userEmail: String?
var link: String?
func sendFirebaseEmailLink() {
let actionCodeSettings = ActionCodeSettings.init()
let email = userEmail
actionCodeSettings.url = URL.init(string: String(format: "https://<myappname>.page.link/emaillogin/?email=%#", email!))
// The sign-in operation has to always be completed in the app.
actionCodeSettings.handleCodeInApp = true
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
Auth.auth().sendSignInLink(toEmail: email!,
actionCodeSettings: actionCodeSettings) { error in
if let error = error {
print(error.localizedDescription)
return
}
else {
UserDefaults.standard.set(email, forKey: "Email")
print("email sent to user")
}
// TODO: Notify user to check email and click the link.
}
}
// Sign in user after they clicked email link called from AppDelegate
#objc func signInUserAfterEmailLinkClick() {
// Get link url string from the dynamic link captured in AppDelegate.
if let link = UserDefaults.standard.value(forKey: "Link") as? String {
self.link = link
}
// Sign user in with the link and email.
Auth.auth().signIn(withEmail: userEmail!, link: link!) { (result, error) in
if error == nil && result != nil {
if (Auth.auth().currentUser?.isEmailVerified)! {
print("User verified with passwordless email")
// TODO: Do something after user verified like present a new View Controller
}
else {
print("User NOT verified by passwordless email")
}
}
else {
print("Error with passwordless email verfification: \(error?.localizedDescription ?? "Strangely, no error avaialble.")")
}
}
}
3) Code in AppDelegate
// For Passwordless Email Login to Handle Dynamic Link after User Clicks Email Link
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if let incomingURL = userActivity.webpageURL {
print("Incoming URL is \(incomingURL)")
// Parse incoming
let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else {
print("Found an error: \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handleIncomingDynamicLink(dynamicLink)
}
}
if linkHandled {
return true
}
else {
// Maybe do other things with dynamic links in future?
return false
}
}
return false
}
// Handles the link and saves it to userDefaults to assist with login.
func handleIncomingDynamicLink(_ dynamicLink: DynamicLink) {
guard let url = dynamicLink.url else {
print("My dynamic link object has no url")
return
}
print("Incoming link parameter is \(url.absoluteString)")
let link = url.absoluteString
if Auth.auth().isSignIn(withEmailLink: link) {
// Save link to userDefaults to help finalize login.
UserDefaults.standard.set(link, forKey: "Link")
// Send notification to ViewController to push the First Time Login VC
NotificationCenter.default.post(
name: Notification.Name("SuccessfulPasswordlessEmailNotification"), object: nil, userInfo: nil)
}
}
For anyone using SwiftUI with AppDelegate and SceneDelegate files instead of UIKit, here's what I've done:
Create a function to send a link to the user's email
func sendSignLink(email: String) async throws {
do {
let actionCodeSettings = ActionCodeSettings()
actionCodeSettings.url = URL(string: "*enter your Firebase Dynamic link here*")
actionCodeSettings.handleCodeInApp = true
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
try await Auth.auth().sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings)
UserDefaults.standard.set(email, forKey: "email")
}
catch {
throw error
}
}
In the SceneDelegate file, import FirebaseDynamicLinks and add the below code
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if let incomingURL = userActivity.webpageURL {
print("\n \nIncoming URL is \(incomingURL)")
_ = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else {
print("\n \nError with handling incoming URL: \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
guard let url = dynamicLink.url else {
print("\n \nDynamic link object has no url")
return
}
print("\n \nIncoming link parameter is \(url.absoluteString)")
let link = url.absoluteString
if Auth.auth().isSignIn(withEmailLink: link) {
// Send notification to trigger the rest of the sign in sequence
NotificationCenter.default.post(name: Notification.Name("Success"), object: nil, userInfo: ["link": link])
} else {
// Send error notification
NotificationCenter.default.post(name: Notification.Name("Error"), object: nil, userInfo: nil)
}
}
}
}
}
Create a function to handle the sign in after the user has clicked on the link in their email
func signInWithEmail(link: String) async throws {
do {
let email = UserDefaults.standard.value(forKey: "email")
try await Auth.auth().signIn(withEmail: email, link: link)
}
catch {
throw error
}
}
In a relevant view, handle the notifications which get posted
struct MyView: View {
var body: some View {
VStack {
Text("View")
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("Success"))) { notificationInfo in
if let userInfo = notificationInfo.userInfo {
if let link = userInfo["link"] as? String {
Task.init {
do {
try await signInWithEmail(link: link)
} catch {
print(error)
}
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("Error"))) { _ in
//do something with error
}
}
}

How to update Email Address in Firebase Authentication in Swift 4

i just want to update authenticate email address of current user. i have tried lot's of solution like updateEmail method of firebase but it not work !! if any one know then please tell me how can i achieved this Thanks in advance !!
#IBAction func btnResetEmailClick(_ sender: UIButton) {
let auth = Auth.auth()
guard let email = self.txtEmailAddress.text ?? auth.currentUser?.email else { return }
// email that i have to update with current user email
auth.currentUser?.updateEmail(to: (auth.currentUser?.email)!, completion: { (error) in
if error == nil{
}else{
}
})
}
To change the email address the user has to be logged in recently i would suggest doing this:
var credential: AuthCredential
#IBAction func changeEmail() {
if let user = Auth.auth().currentUser {
// re authenticate the user
user.reauthenticate(with: credential) { error in
if let error = error {
// An error happened.
} else {
// User re-authenticated.
user.updateEmail(to: "email") { (error) in
// email updated
}
}
}
}
}
This is effective method to solve it.
let user = Auth.auth().currentUser
user?.updateEmail(to: "email") { error in
if error != nil {
// An error happened
} else {
// Email updated.
}
}

Link multiple auth providers on firebase

Currently I allow users to "Sign In with Facebook":
#objc func handleFBLogin() {
FBSDKLoginManager().logIn(withReadPermissions: ["email", "public_profile"], from: self) { (result, error) in
if error != nil {
print(error as Any)
return
}
self.handleFBAccessToken()
}
}
func handleFBAccessToken() {
let accessToken = FBSDKAccessToken.current()
guard let accessTokenString = accessToken?.tokenString else { return }
let credentials = FacebookAuthProvider.credential(withAccessToken: accessTokenString)
Auth.auth().signIn(with: credentials) { (user, error) in
if error != nil {
// I assume I handle the errors here
print(error as Any)
return
}
// successfully logged in user
self.instantiateTabVC()
}
FBSDKGraphRequest(graphPath: "/me", parameters: ["fields": "id, name, email"]).start { (connection, result, error) in
if error != nil {
print(error as Any)
}
}
}
I'd like to add it so that if they've already made an account via email/password, the two accounts will be automatically linked/merged (or vice versa). In "I assume I handle the errors here", I added
let providers = Auth.auth().fetchProviders(forEmail: AuthErrorUserInfoEmailKey)
// sign in with existing account
// call linkWithCredential:completion:
to fetch the email that already exists. I am basing this off of this and this (both similar questions). I understand that the premise is to
use fetchProvidersForEmail with that email which will lookup the provider IDs associated with that email. You then sign in the user with one of those providers. After you finish sign-in with the existing account, you call linkWithCredential:completion: with the original credential that caused the error to occur
However, I am new to Swift and extremely confused about how to go about that. Any sample code would be extremely beneficial. I've also tried reading the documentation but that hasn't helped either (as I probably have not learned to properly understand the documentation)
I hope that I am moving in the right direction to solve this problem, however, if you have any other suggestions, I'd be open to that. I appreciate any help
Try this Approach this will doesn't create a new user if exists.
func facebook() {
let loginManager: FBSDKLoginManager = FBSDKLoginManager()
loginManager.logIn(withReadPermissions: ["email"], from: self, handler: { result, error in
if let error = error {
self.showMessagePrompt(error.localizedDescription)
} else if result!.isCancelled {
print("FBLogin cancelled")
} else {
// [START headless_facebook_auth]
let credential = FacebookAuthProvider.credential(withAccessToken: FBSDKAccessToken.current().tokenString)
// [END headless_facebook_auth]
self.firebaseLogin(credential)
}
})
}
func firebaseLogin(_ credential: AuthCredential) {
if let user = Auth.auth().currentUser {
// [START link_credential]
user.link(with: credential) { _, error in
// [START_EXCLUDE]
if let error = error {
self.showMessagePrompt(error.localizedDescription)
return
}
// [END_EXCLUDE]
}
// [END link_credential]
} else {
// [START signin_credential]
Auth.auth().signIn(with: credential) { _, error in
// [START_EXCLUDE silent]
self.hideSpinner {
// [END_EXCLUDE]
if let error = error {
// [START_EXCLUDE]
self.showMessagePrompt(error.localizedDescription)
// [END_EXCLUDE]
return
}
// User is signed in
// [START_EXCLUDE]
// Merge prevUser and currentUser accounts and data
// ...
// [END_EXCLUDE]
}
}
// [END signin_credential]
}
}

iOS SWIFT: Unable to delete user from Firebase Database

I want to delete my current user from Firebase. The authenticated user gets deleted however, I am unable to delete the data for that user in the database. What am i doing wrong?
This is my delete user method....
FIRAuth.auth()?.signIn(withEmail: (emailTextField?.text)! , password: (passwordTextField?.text)!, completion: { (user, error) in
if error == nil {
print("User Authenticate!!!")
let user = FIRAuth.auth()?.currentUser
user?.delete(completion: { (error) in
if error != nil {
print("Error unable to delete user")
} else {
DataService.ds.deleteCurrentFirebaseDBUser()
KeychainWrapper.standard.removeObject(forKey: KEY_UID)
self.performSegue(withIdentifier: "goToLogin", sender: nil)
}
})
} else {
//Password was wrong, unable to authenicate user. Data is not updated
print("!!!ALERT!!! Unable to authenticate user")
let alert = UIAlertController(title: "Incorrect Password", message: "Please re-enter your password", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
})
Firebase Rules:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
Database:
App
-> users
->
4erkjkl543jfe46
->name
->email
ERRORS:
2017-01-21 21:33:10.321704 APP[11582:4102711] [FirebaseDatabase] setValue: or removeValue: at /users/4erkjkl543jfe46 failed: permission_denied
Optional(Error Domain=com.firebase Code=1 "Permission denied" UserInfo={NSLocalizedDescription=Permission denied})
I'm having the same issue. You are not able to make use of your function deleteCurrentFirebaseDBUser() because the Firebase delete function (if successful) removes the user auth object.
As a result user is not authenticated anymore at the time you want to delete user's data in database with deleteCurrentFirebaseDBUser().
Currently I delete user's data in database before Firebase delete function which is not the ideal solution.
We can delete user from both side authentication and database.But before that we need to reauthenticate user first then we get latest token to delete the user.
Here is the pretty code:
let user = Auth.auth().currentUser
user?.reauthenticate(with:credential) { error in
if let error = error {
// An error happened.
showAlertWithErrorMessage(message: error.localizedDescription)
} else {
// User re-authenticated.
user?.delete { error in
if let error = error {
// An error happened.
showAlertWithErrorMessage(message: error.localizedDescription)
} else {
// Account deleted.
let userID = HelperFunction.helper.FetchFromUserDefault(name: kUID)
Database.database().reference(fromURL: kFirebaseLink).child(kUser).child(userID).removeValue()
try! Auth.auth().signOut()
showAlertWithErrorMessage(message: "Your account deleted successfully...")
return
}
}
}
}
100% working in my project and well tested
for just to delete a child from Firebase use "removeValue()"
var db: DatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
db = Database.database().reference()
deleteByID()
}
func deleteByID(){
db.child("YOURID").removeValue()
}
Swift 5 | Firebase 8.11.0
As #SvshX said, deleting the user data before deleting the actual user is the only available solution.
The problem with this method is that deleting the user might give an error like AuthErrorCode.requiresRecentLogin, then the data will be deleted but the user will not.
This error is given when the last authentication of the user was more than 5 minuets ago (from Firebase Docs)
So, fixing both of the issues can be achieved by using DispatchGroup and checking the lastSignInDate.
This is my final solution (just call deleteUserProcess()):
let deleteDataGroup = DispatchGroup()
func deleteUserProcess() {
guard let currentUser = Auth.auth().currentUser else { return }
deleteUserData(user: currentUser)
// Call deleteUser only when all data has been deleted
deleteDataGroup.notify(queue: .main) {
self.deleteUser(user: currentUser)
}
}
/// Remove data from Database & Storage
func deleteUserData(user currentUser: User) {
// Check if `currentUser.delete()` won't require re-authentication
if let lastSignInDate = currentUser.metadata.lastSignInDate,
lastSignInDate.minutes(from: Date()) >= -5 {
deleteDataGroup.enter()
Database.database().reference().child(userId).removeValue { error, _ in
if let error = error { print(error) }
self.deleteDataGroup.leave()
}
// Delete folders from Storage isn't possible,
// so list and run over all files to delete each one independently
deleteDataGroup.enter()
Storage.storage().reference().child(userId).listAll { list, error in
if let error = error { print(error) }
list.items.forEach({ file in
self.deleteDataGroup.enter()
file.delete { error in
if let error = error { print(error) }
self.deleteDataGroup.leave()
}
})
deleteDataGroup.leave()
}
}
}
/// Delete user
func deleteUser(user currentUser: User) {
currentUser.delete { error in
if let error = error {
if AuthErrorCode(rawValue: error._code) == .requiresRecentLogin {
reauthenticate()
} else {
// Another error occurred
}
return
}
// Logout properly
try? Auth.auth().signOut()
GIDSignIn.sharedInstance.signOut()
LoginManager().logOut()
// The user has been deleted successfully
// TODO: Redirect to the login UI
}
}
func reauthenticate() {
// TODO: Display some UI to get credential from the user
let credential = ... // Complete from https://stackoverflow.com/a/38253448/8157190
Auth.auth().currentUser?.reauthenticate(with: credential) { _, error in
if let error = error {
print(error)
return
}
// Reload user (to update metadata.lastSignInDate)
Auth.auth().currentUser?.reload { error in
if let error = error {
print(error)
return
}
// TODO: Dismiss UI
// Call `deleteUserProcess()` again, this time it will delete the user
deleteUserProcess()
}
}
}
The minuets function can be added in an extension to Date (thanks to Leo Dabus):
extension Date {
/// Returns the amount of minutes from another date
func minutes(from date: Date) -> Int {
return Calendar.current.dateComponents([.minute], from: date, to: self).minute ?? 0
}
}

Resources