So my goal is to have congruent functionality both on the iOS simulator in Xcode and as well as a physical device on TestFlight. So currently, I have a function that handles refunds in my app. On the simulator the function runs perfectly fine in the order I expect it to, but the print statements execute in the wrong order which I'm assuming is the reason for misbehaviour on TestFlight simulations.
Here is the method:
@IBAction func cancelPurchasePressed(_ sender: UIButton) {
guard let nameOfEvent = selectedEventName else { return }
guard let user = Auth.auth().currentUser else { return }
let alertForCancel = UIAlertController(title: "Cancel Purchase", message: "Are you sure you want to cancel your purchase of a ticket to \(nameOfEvent)? You will receive full reimbursement of what you paid within 5 - 10 days.", preferredStyle: .alert)
let cancelPurchase = UIAlertAction(title: "Cancel Purchase", style: .default) { (purchaseCancel) in
self.viewPurchaseButton.isHidden = true
self.cancelPurchaseButton.isHidden = true
self.refundLoading.alpha = 1
self.refundLoading.startAnimating()
self.makeRefundRequest()
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
let group = DispatchGroup()
self.db.collection("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests").getDocuments { (querySnapshot, error) in
guard error == nil else {
print("The guests couldn't be fetched.")
return
}
guard querySnapshot?.isEmpty == false else {
print("The user did not bring any guests.")
return
}
for guest in querySnapshot!.documents {
let name = guest.documentID
group.enter()
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests/\(name)").delete { (error) in
guard error == nil else {
print("The guests couldn't be deleted.")
return
}
print("Guests deleted with purchase refund.")
group.leave()
}
}
}
group.notify(queue: .main) {
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)").delete { (error) in
guard error == nil else {
print("Error trying to delete the purchased event.")
return
}
print("The purchased event was succesfully removed from the database!")
}
self.refundLoading.stopAnimating()
self.refundLoading.alpha = 0
self.ticketFormButton.isHidden = false
self.cancelPurchaseButton.isHidden = true
self.viewPurchaseButton.isHidden = true
}
}
}
alertForCancel.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alertForCancel.addAction(cancelPurchase)
present(alertForCancel, animated: true, completion: nil)
}
Basically what I have going on is a simple refund request being made to Stripe and a second after I have an asyncAfter
code block with some database cleanup in it. I have to do the asyncAfter
or else the refund request gets beat out by the other async tasks by speed.
So I took my knowledge of DispatchGroups
and decided to implement it since I have an async task in a for loop that I need to be completed before every other task. So I expected this to work fine, despite the order of the print statements being incorrect, but when I ran the exact block of code on my phone via TestFlight, I made a refund and the cell was still showing up in the tableview, meaning the document wasn't deleted from the database properly.
I've been having some terrifying experience recently with DispatchGroups
and TestFlight and I just honestly hope to fix all this and have these problems come to an end temporarily. Any suggestions on how I can fix this method to prevent incorrect order on TestFlight?
UPDATE Decided to use a completion handler instead to do the same functionality:
func makeRefundRequest(refundMade: @escaping ((Bool) -> ())) {
let backendURLForRefund = "https://us-central1-xxxxxx-41f12.cloudfunctions.net/createRefund"
getStripePaymentIntentID { (paymentid) in
guard let id = paymentid else { return }
let url = URL(string: backendURLForRefund)!
let json: [String: Any] = [
"payment_intent": id
]
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: json)
let task = URLSession.shared.dataTask(with: request) { [weak self] (data, response, error) in
guard let taskError = error?.localizedDescription else { return }
guard let response = response as? HTTPURLResponse,
response.statusCode == 200,
let data = data,
let _ = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
self?.showAlert(title: "Refund Request Error", message: "There was an error making the refund request. \(taskError)")
refundMade(false)
return
}
}
task.resume()
refundMade(true)
}
}
And then I just slapped this method in the actual refund process method itself:
@IBAction func cancelPurchasePressed(_ sender: UIButton) {
guard let nameOfEvent = selectedEventName else { return }
guard let user = Auth.auth().currentUser else { return }
let alertForCancel = UIAlertController(title: "Cancel Purchase", message: "Are you sure you want to cancel your purchase of a ticket to \(nameOfEvent)? You will receive full reimbursement of what you paid within 5 - 10 days.", preferredStyle: .alert)
let cancelPurchase = UIAlertAction(title: "Cancel Purchase", style: .default) { (purchaseCancel) in
self.viewPurchaseButton.isHidden = true
self.cancelPurchaseButton.isHidden = true
self.refundLoading.alpha = 1
self.refundLoading.startAnimating()
self.makeRefundRequest { (response) in
if response == false {
return
} else {
let group = DispatchGroup()
self.db.collection("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests").getDocuments { (querySnapshot, error) in
guard error == nil else {
print("The guests couldn't be fetched.")
return
}
guard querySnapshot?.isEmpty == false else {
print("The user did not bring any guests.")
return
}
for guest in querySnapshot!.documents {
let name = guest.documentID
group.enter()
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests/\(name)").delete { (error) in
guard error == nil else {
print("The guests couldn't be deleted.")
return
}
print("Guests deleted with purchase refund.")
group.leave()
}
}
}
group.notify(queue: .main) {
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)").delete { (error) in
guard error == nil else {
print("Error trying to delete the purchased event.")
return
}
print("The purchased event was succesfully removed from the database!")
}
self.refundLoading.stopAnimating()
self.refundLoading.alpha = 0
self.ticketFormButton.isHidden = false
self.cancelPurchaseButton.isHidden = true
self.viewPurchaseButton.isHidden = true
}
}
}
}
alertForCancel.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alertForCancel.addAction(cancelPurchase)
present(alertForCancel, animated: true, completion: nil)
}
This actually does not work fine, yes the refund goes through on Stripe and the database is cleaned up for 3 minutes, but the print statements print in incorrect order and also the document magically reappears in the Firestore database 3 minutes after physically seeing it be deleted, how can I prevent this and make sure they print in correct order and execute in correct order to work properly on TestFlight? Is this an issue in my DispatchGroup
implementation? Or is it something completely different?
I think it's important that no matter what you end up doing you should learn how to do everything anyway. I would advise against this approach below but it's what you originally started so let's finish it regardless, so you know how dispatch grouping works. Once you've gotten a handle on this, refine it by replacing the dispatch group with a Firestore transaction or batch operation. The point of the transaction or batch operation is so all of the documents are deleted atomically, meaning they all go or none go. This simplifies things greatly and they are very basic! And the documentation for them is very clear.
The final thing I would suggest is perhaps integrating some recursion, meaning that if something fails it can retry automatically. Recursive functions are also very basic so just learn how to write one in Playground first and then apply it here. Just take it one step at a time and you'll get it down within a day or two. But this is the first step so carefully read what I wrote and understand why I did what I did.
func makeRefundRequest(refundMade: @escaping (_ done: Bool) -> Void) {
getStripePaymentIntentID { (paymentid) in
guard let id = paymentid,
let url = URL(string: "https://us-central1-xxxxxx-41f12.cloudfunctions.net/createRefund") else {
refundMade(false) // always call completion when you exit this function
return
}
let json: [String: Any] = ["payment_intent": id]
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: json)
let task = URLSession.shared.dataTask(with: request) { [weak self] (data, response, error) in
if let response = response as? HTTPURLResponse,
response.statusCode == 200,
let data = data,
let _ = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
refundMade(true) // completion: true
} else {
if let error = error {
print(error)
}
refundMade(false) // completion: false
}
}
task.resume()
// do not call the completion of this function here because you just made a network call
// so call the completion of this function in that call's completion handler
}
}
@IBAction func cancelPurchasePressed(_ sender: UIButton) {
guard let nameOfEvent = selectedEventName,
let user = Auth.auth().currentUser else {
return
}
let alertForCancel = UIAlertController(title: "Cancel Purchase", message: "Are you sure you want to cancel your purchase of a ticket to \(nameOfEvent)? You will receive full reimbursement of what you paid within 5 - 10 days.", preferredStyle: .alert)
let cancelPurchase = UIAlertAction(title: "Cancel Purchase", style: .default) { (purchaseCancel) in
// put the UI in a loading state
self.viewPurchaseButton.isHidden = true
self.cancelPurchaseButton.isHidden = true
self.refundLoading.alpha = 1
self.refundLoading.startAnimating()
self.makeRefundRequest { (done) in
if done {
self.db.collection("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests").getDocuments { (snapshot, error) in
guard let snapshot = snapshot,
!snapshot.isEmpty else {
if let error = error {
print(error)
}
return
}
let group = DispatchGroup() // instatiate the dispatch group outside the loop
for doc in snapshot.documents {
group.enter() // enter on each loop iteration
let name = doc.documentID
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests/\(name)").delete { (error) in
if let error = error {
print(error)
}
group.leave() // leave no matter what the outcome, error or not
// what do you do when this document didn't delete?
// by doing all your deleting in a transaction or batch
// you can ensure that they all delete or none delete
}
}
group.notify(queue: .main) { // done with loop, make final network call
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)").delete { (error) in
if let error = error {
print(error)
}
// put the UI back to normal state
self.refundLoading.stopAnimating()
self.refundLoading.alpha = 0
self.ticketFormButton.isHidden = false
self.cancelPurchaseButton.isHidden = true
self.viewPurchaseButton.isHidden = true
}
}
}
} else { // refund was not made, put the UI back into normal state
self.refundLoading.stopAnimating()
self.refundLoading.alpha = 0
self.ticketFormButton.isHidden = false
self.cancelPurchaseButton.isHidden = true
self.viewPurchaseButton.isHidden = true
}
}
}
alertForCancel.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alertForCancel.addAction(cancelPurchase)
present(alertForCancel, animated: true, completion: nil)
}