0

So my goal is to delete all user's guests if the guest count for a purchased ticket is over 1 when a user is deleting their account.

Currently I have this function to try to accomplish this:

func deleteUserGuests(completion: @escaping (_ done: Bool) -> Void) {
    var retries = 0
    guard let user = Auth.auth().currentUser else { return  }
    
    func checkForGuestsAndDeleteIfAny() {
        db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
            if let snapshot = querySnapshot {
                if snapshot.isEmpty {
                    completion(true)
                    // done, nothing left to delete
                } else {
                    // delete the documents using a dispatch group or a Firestore batch delete
                    for document in querySnapshot!.documents {
                        let docID = document.documentID
                        self.db.collection("student_users/\(user.uid)/events_bought/\(docID)/guests").getDocuments { (querySnap, error) in
                            guard querySnap?.isEmpty == false else {
                                print("The user being deleted has no guests with his purchases.")
                                return
                            }
                            let group = DispatchGroup()
                            for doc in querySnap!.documents {
                                let guest = doc.documentID
                                group.enter()
                                self.db.document("student_users/\(user.uid)/events_bought/\(docID)/guests/\(guest)").delete { (error) in
                                    guard error == nil else {
                                        print("Error deleting guests while deleting user.")
                                        return
                                    }
                                    print("Guests deleted while deleting user!")
                                    group.leave()
                                }
                            }
                        }
                    }
                 
                    checkForGuestsAndDeleteIfAny()// call task again when this finishes
                           // because this function only exits when there is nothing left to delete
                           // or there have been too many failed attempts
                }
            } else {
                if let error = error {
                    print(error)
                }
                retries += 1 // increment retries
                run() // retry
            }
        }
    }
    
   
    func run() {
        guard retries < 30 else {
            completion(false) // 5 failed attempts, exit function
            return
        }
        if retries == 0 {
            checkForGuestsAndDeleteIfAny()

        } else { // the more failures, the longer we wait until retrying
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(retries)) {
                checkForGuestsAndDeleteIfAny()
            }
        }
    }
    
    run()
}

I upped the retry limit, to see if that was the issue, but it still doesn't delete the guests if there are more than one.

I call it in an alert action when the user successfully reauthenticates before deleting their account:

let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
            
            
            self.deleteButton.isHidden = true
            self.loadingToDelete.alpha = 1
            self.loadingToDelete.startAnimating()
            
            self.deleteUserGuests { (response) in
                if response == false {
                    return
                }
            }
            
            self.deleteUserPurchases { (purchase) in
                if purchase == false {
                    return
                }
            }
            self.deleteUserOutOfFirestore { (removed) in
                if removed == false {
                    return
                }
            }
            
            user.delete(completion: { (error) in
                guard error == nil else {
                    print("There was an error deleting user from the system.")
                    return
                }
                print("User Deleted.")
                
            })
            
            self.loadingToDelete.stopAnimating()
            self.performSegue(withIdentifier: Constants.Segues.studentUserDeletedAccount, sender: self)
            
            
            
            
        }

This is the result in the database:

residual data

Everything else gets deleted fine in correct order, purchases, the user itself, and then the user out of Firebase auth, but the guests never get deleted if it is over 1 guest. Is there something I did wrong or left out in the deleteUserGuests method that is causing this issue?

dante
  • 105
  • 11
  • Why aren't you nesting these tasks so that one starts after the other and then in the final task you delete the user object and then in that completion handler you stop animating the spinner and segue the user? Or is that not how you want to do it? – trndjc Apr 27 '21 at 19:27
  • Welll, will that fix this issue? If it does then sure I'll do it like that, but if not, I don't really see the use. The only thing I need to figure out is how to make the for loop pick up on all the guests and delete them as well. Sounds simple but for the life of me can't figure out why. @bxod – dante Apr 27 '21 at 19:31
  • Is `deleteUserGuests` the only function you're having a problem with? – trndjc Apr 27 '21 at 19:33
  • yes literally the only issue, that is why it is so frustrating to me, everything else deletes fine. I searched up nested for loops with dispatch groups, came across [this question](https://stackoverflow.com/questions/59785632/dispatch-group-for-nested-loop), tried to implement the answer, still no luck. @bxod – dante Apr 27 '21 at 19:39

1 Answers1

0

As I've said a number of times, I'd approach this entire task differently--I'd do this sort of cleanup on the server side, perform the deletes atomically using a batch or transaction operation, and have robust recursion throughout. However, to fix your immediate problem of why you can't delete the documents in this subcollection, this will do it.

func deleteUserGuests(completion: @escaping (_ done: Bool) -> Void) {
    guard let user = Auth.auth().currentUser else {
        return
    }
    var retries = 0
    
    func task() {
        db.collection("student_users/\(user.uid)/events_bought").getDocuments { (snapshot, error) in
            if let snapshot = snapshot {
                if snapshot.isEmpty {
                    completion(true)
                } else {
                    let dispatchEvents = DispatchGroup()
                    var errors = false
                    
                    for doc in snapshot.documents {
                        dispatchEvents.enter()

                        self.db.collection("student_users/\(user.uid)/events_bought/\(doc.documentID)/guests").getDocuments { (snapshot, error) in
                            if let snapshot = snapshot {
                                if snapshot.isEmpty {
                                    dispatchEvents.leave()
                                } else {
                                    let dispatchGuests = DispatchGroup()
                                    
                                    for doc in snapshot.documents {
                                        dispatchGuests.enter()

                                        doc.reference.delete { (error) in
                                            if let error = error {
                                                print(error)
                                                errors = true
                                            }
                                            
                                            dispatchGuests.leave()
                                        }
                                    }
                                    
                                    dispatchGuests.notify(queue: .main) {
                                        dispatchEvents.leave()
                                    }
                                }
                            } else {
                                if let error = error {
                                    print(error)
                                }
                                errors = true
                                dispatchEvents.leave()
                            }
                        }
                    }
                    
                    dispatchEvents.notify(queue: .main) {
                        if errors {
                            retries += 1
                            run()
                        } else {
                            completion(true)
                        }
                    }
                }
            } else {
                if let error = error {
                    print(error)
                }
                retries += 1
                run()
            }
        }
    }
    
    
    func run() {
        guard retries < 30 else {
            completion(false)
            return
        }
        if retries == 0 {
            task()
        } else {
            let delay = Double(retries)
            
            DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
                task()
            }
        }
    }
    
    run()
}
trndjc
  • 11,654
  • 3
  • 38
  • 51
  • just tried it, it still leaves one guest behind in the database. Listen man, I would definitely give what you're recommending a try, thing is, I have absolutely no idea how to or where to even start with what you recommended. You've been doing this much longer than I have so it's not as easy as it sounds when you explain it lol. @bxod – dante Apr 27 '21 at 20:22
  • There is no reason a guest document should remain if all of the other ones are being deleted by this function and it's not throwing any errors. If this function doesn't produce any errors and it's deleting all but one guest document, then something is wrong with your data or your console. Basically, your problem is not in this code. – trndjc Apr 27 '21 at 20:26
  • Welp, guess I'll just have to give up on this for now. I'll just prompt users to refund all their purchases before they delete their account. I genuinely have no clue how to fix this, regardless of where the issue stems from. Thanks anyways. @bxod – dante Apr 27 '21 at 21:12
  • Just create a dummy collection with dummy documents and a dummy sub-collection and write a loop to delete them. This code right here will do that. Like I said before, start from the beginning and take it step by step and test along the way. – trndjc Apr 27 '21 at 21:28