I am now currently working on an IOS app. So I want to delete the user account during the registration when app get killed such as crash, or users force to close.
So I create NSNotification (appWillTerminate). As you can see from the code below, whenever app is closed at this page, the notification will get triggered, because I can see my print statement "hello world" in the console. I do not think there are any syntax error or anything. But it just can't delete the user from Authentication.
#objc func deleteAccount(notification: NSNotification){
print("hello world")
guard let userID = Auth.auth().currentUser?.uid else {return}
Database.database().reference().child("users").child(userID).removeValue(completionBlock: { (error, ref) in
if(error != nil){
print(error)
}
let user = Auth.auth().currentUser
user?.delete { error in
if let error = error {
print(error.localizedDescription)
// An error happened.
} else {
print("delete success")
do{
try Auth.auth().signOut()
}catch let logoutError{
print(logoutError)
}
}
}
})
}
Related
So my goal is to have the correct user sign up and be shown the correct segue as well as the user info be written to Firestore. So I have a basic sign up function that gets triggered when the sign up button is pressed:
#IBAction func schoolSignupPressed(_ sender: UIButton) {
let validationError = validateFields()
let schoolName = schoolNameTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let schoolEmail = schoolEmailTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let schoolPassword = schoolPasswordTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let schoolID = schoolIDTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let schoolDistrict = schoolDistrictTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let dateCreated = Date()
if validationError != nil {
return
}
Auth.auth().createUser(withEmail: schoolEmail, password: schoolPassword) { (result, error) in
guard let signUpError = error?.localizedDescription else { return }
guard error == nil else {
self.showAlert(title: "Error Signing Up", message: "There was an error creating the user. \(signUpError)")
return
}
let db = Firestore.firestore()
guard let result = result else { return }
db.document("school_users/\(result.user.uid)").setData(["school_name":schoolName,
"school_id":schoolID,
"emailAddress": result.user.email ?? schoolEmail,
"remindersPushNotificationsOn": true,
"updatesPushNotificationsOn": true,
"schoolDistrict":schoolDistrict,
"time_created":dateCreated,
"userID": result.user.uid],
merge: true) { (error) in
guard let databaseError = error?.localizedDescription else { return }
guard error == nil else {
self.showAlert(title: "Error Adding User Info", message: "There was an error adding the user info. \(databaseError)")
return
}
}
let changeRequest = result.user.createProfileChangeRequest()
changeRequest.displayName = schoolName
changeRequest.commitChanges { (error) in
guard error == nil else {
return
}
print("School Name Saved!")
}
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
self.performSegue(withIdentifier: Constants.Segues.fromSchoolSignUpToSchoolDashboard, sender: self)
}
}
}
This is the sign up function for the 'school' user, but the 'student' user is essentially the same thing just different fields and of course a different segue destination. Now maybe like a day ago or 2, I was testing this function out and it was working completely fine the user was succesfully signed up, the user info was written to firestore, the correct view controller was displayed, the only difference was I had some DispatchGroup blocks within the function because when i was running the method in TestFlight, there would be a couple of bugs that would crash the app.
So I figured since everything was working fine in the simulator, I archive the build, upload it to TestFlight and wait for it to be approved. It got approved last night and I ended up testing it out on my phone this morning to see it again, now when I try to sign up as either a school user or a student user, it segues to the wrong view controller every time and no info gets written to firestore, the user just gets saved in Firebase Auth and that is not the outcome I expect in my app.
I've checked the segue identifiers, I've checked the connections tab, and even though it was working amazing 24 hours ago, I still checked it all. I'm trying my best to really appreciate what Apple does for developers but I'm really starting to grow a hatred towards TestFlight, everything I do and run in the simulator works fantastic on Xcode, as soon as I run it in TestFlight, everything just goes out the window. I hate these types of bugs because you genuinely don't know where the issue is stemming from simply because you've used, if not very similar, the exact same method in every other previous situation.
The login process works fine on both student and school user, I'll show an example of the school user login method:
#IBAction func loginPressed(_ sender: UIButton) {
let validationError = validateFields()
let email = schoolEmailTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let password = schoolPasswordTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
if validationError != nil {
return
} else {
Auth.auth().signIn(withEmail: email, password: password) { (result, error) in
guard let signInError = error?.localizedDescription else { return }
let group = DispatchGroup()
group.enter()
guard error == nil else {
self.showAlert(title: "Error Signing In", message: "There was an issue trying to sign the user in. \(signInError)")
return
}
group.leave()
group.notify(queue: .main) {
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
self.performSegue(withIdentifier: Constants.Segues.fromSchoolLoginToSchoolEvents, sender: self)
}
}
}
}
}
Pretty much the same for student users. If anyone can point out possible issues for this bug in the first code snippet that would be amazing. Thanks in advance.
Although it is helpful, removing the error.localizedDescription line brought everything back to normal.
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
}
}
}
I am trying to implement a function to delete current user's account on iOS. Account deletion works properly but the problem is that I cannot delete the account's data from Database and Storage when deleting an account.
"currentUser.delete" deletes the account but I think there is no authentication to delete its data from Database and Storage. Permission denied message shows up in the log. After running this function, I get to see the account is gone in Firebase Console Authentication page but data from Database and Storage persists.
Is this the correct way to delete an account?
I tried to delete data from Database and Storage before deleting the account. However, Firebase asks for re-authentication if session is more than 5 minutes old. Re-login shows empty data to the user before performing account deletion again so this is misleading and very confusing.
Please let me know how to remove data when deleting an account.
private func deleteAccount() {
guard let currentUser = Auth.auth().currentUser else {
return print("user not logged in")
}
currentUser.delete { error in
if error == nil {
// 1. Delete currentUser's data from Database. Permission denied
// 2. Delete currentUser's data from Storage. Permission denied
// present login screen (welcome page)
self.presentLoginScreen()
} else {
guard let errorCode = AuthErrorCode(rawValue: error!._code) else { return }
if errorCode == AuthErrorCode.requiresRecentLogin {
self.showMessage("Please re-authenticate to delete your account.", type: .error)
do {
try Auth.auth().signOut()
self.presentLoginScreen()
} catch {
print("There was a problem logging out")
}
}
}
}
}
Swift 5 | Firebase 8.11.0
To solve the problems that you've mentioned (delete the data before deleting the actual user and potentially get the AuthErrorCode.requiresRecentLogin error), you may use DispatchGroup and check the lastSignInDate, like this (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
}
}
you can first make your specific user deleted and and its value through its UID then you can deleted user and take him to root view controller or login screen after deleting it.
// removing user data from firebase and its specific user id
let user = Auth.auth().currentUser
user?.delete { error in
if let error = error {
// An error happened.
print(error.localizedDescription)
} else {
Database.database().reference().child("users").child(user?.uid ?? "").removeValue()
self.navigationController?.popToRootViewController(animated: true)
// Account deleted and logout user
// do {
// try Auth.auth().signOut()
// take you to root
// self.navigationController?.popToRootViewController(animated: true)
}
So in my app I just made it so that the user can choose to delete their account, and that works out beautifully. However after the account is deleted the app crashes. I think this is because it is trying to search for a user but it's not there.
Here is my code:
let loginController = LoginController()
func deleteAccount() {
let user = Auth.auth().currentUser
let userId = Auth.auth().currentUser?.uid
let databaseUser = Database.database().reference().child("users").child(userId!)
user?.delete { error in
if let error = error {
print(error)
} else {
self.present(self.loginController, animated: true, completion: nil)
}
}
databaseUser.removeValue(completionBlock: { (error, ref) in
if error != nil {
print(error)
} else {
self.present(self.loginController, animated: true, completion: nil)
} //Without doing this the user's account only gets deleted in the Authentication, not the whole database. I think this is the problem here?
})
}
Thank you so much in advance!
First you need to delete the users database as to do that you would need the let userId = Auth.auth().currentUser?.uid which is only active if the user itself is on your backend, Then you go on to delete the auth.
func deleteAccount() {
let user = Auth.auth().currentUser
let userId = Auth.auth().currentUser!.uid
let databaseUser = Database.database().reference().child("users").child(userId)
databaseUser.removeValue(completionBlock: { (error, ref) in
if error != nil {
print(error)
} else {
user?.delete { error in
if let error = error {
print(error)
} else {
self.present(self.loginController, animated: true, completion: nil)
}
}
})
}
If this still doesn't work track down the lifecycle of the user using debugging tools....
You are implicitly unwrapping an optional in this line with !:
let databaseUser = Database.database().reference().child("users").child(userId!)
You should check whether it's nil in the first place with a guard statement:
guard let userId = Auth.auth().currentUser?.uid else {
return
}
let databaseUser = Database.database().reference().child("users").child(userId)
[...]
Moreover, your code logic is likely to be wrong, as you are getting nil for the userId before you can work with it.
The code order is not ideal because deleting the user also logs them out. So the code may be trying to access the users node after the user was logged out.
Also remember that Firebase is asynchronous and the only way to know a function has completed is when the code inside the closure executes i.e. in this case the databaseUser.removeValue may be firing before the delete user or sometimes it may not.
Code is faster than the internet so it's best to leverage the closures so you know when it's safe to proceed.
Try this sequence; noting that we don't try to delete the Firebase user until we know for sure the data in the users node was deleted. There could use more error checking but you get the idea.
let userRef = self.ref.child("users").child(uid)
userRef.setValue(nil, withCompletionBlock: { snapshot in
Auth.auth().currentUser?.delete(completion: { err in
if err != nil {
print(err?.localizedDescription)
}
})
})
In watchOS2 I am getting the sessionToken from the iOS app with WCSession. That works great but when I try to call become in background it never returns. The console logs 1 and 2 but never makes it to print("3"). Any reason becomeInBackground wouldn't respond at all? Or is there a completely different way I should be going about this?
The sessionToken is coming from user.sessionToken in iOS.
print("1")
if let sessionToken = reply["sessionToken"] as? String {
print("2")
PFUser.becomeInBackground(sessionToken, block: { (user, error) in
print("3")
if let user = user as? User where error == nil {
print("success")
} else {
print(error)
}
})
}