Search code examples
swiftuiin-app-purchaseios17

Reporting In-App Purchase Refund Request Errors


Below is Apple's stock Request Refund Logic from the Food Truck Building a SwiftUI Multiplatform App that I am trying to modify to catch refund request errors. Apple's doesn't seem to think that it is important to show how to catch the errors associated with their sample logic making it difficult for people like me that are still learning how to handle errors.

I am seeing the errors below in result where the comment says catch refund errors here. The one case statement dismisses control if the refund was successful.

This is what I am seeing with my various print statements:

refund error Binding<Optional>(transaction: SwiftUI.Transaction(plist: []), location: SwiftUI.StoredLocation<Swift.Optional<Swift.Error>>, _value: nil) failure(StoreKit.Transaction.RefundRequestError.duplicateRequest) Transaction.RefundError RefundRequestError

So my question is how to get a localizedDescription of the error (in this case duplicateRequest) into the alert?

Part of the problem seems to be that the errors are presented after the successful case branches off without using a do / catch block. I have tried using error to store and use in the alert and gotError but I don't know the result type.

Thanks for your assistance!

struct RefundView: View {
    @State private var recentTransactions: [StoreKit.Transaction] = []
    @State private var selectedTransactionID: UInt64?
    @State private var refundSheetIsPresented = false
    
    @State private var error: Error?
    @State private var refundError: Bool = false
    @State var gotError: String = ""

    @Environment(\.dismiss) private var dismiss

    var body: some View {
            
            List(recentTransactions, selection: $selectedTransactionID) { transaction in
                TransactionRowView(transaction: transaction)
            }
            .safeAreaInset(edge: .bottom) {
                VStack(spacing: 0) {
                    Text("Select a purchase to refund")
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                        .padding()
                    Button {
                        refundSheetIsPresented = true
                    } label: {
                        Text("Request a refund")
                            .bold()
                            .padding(.vertical, 5)
                            .frame(maxWidth: .infinity)
                    }
                    .buttonStyle(.borderedProminent)
                    .padding([.horizontal, .bottom])
                    .disabled(selectedTransactionID == nil)
                }
            }
            
            .task { @MainActor in
                for await transaction in StoreKit.Transaction.all {
                    recentTransactions.append(transaction.unsafePayloadValue)
                }
            }
            .refundRequestSheet(
                for: selectedTransactionID ?? 0,
                isPresented: $refundSheetIsPresented
            ) { result in
                if case .success(.success) = result {
                    dismiss()
                }
                // ******* catch refund errors here *******
                self.error = error
                refundError.toggle()
              //  $gotError = result
                
                print("refund error \($error)")
                print(result)
                print("Transaction.RefundError \(Transaction.RefundRequestError)")
                
            }
        .navigationTitle("Refund Purchase")
        .alert("Refund error: \(gotError)", isPresented: $refundError, actions: {})
    }
}

Solution

  • The result in this case is Result<Transaction.RefundRequestStatus, Transaction.RefundRequestError> and your gotError variable is a String, so it's not equivalent. You also don't need the prefix $. This symbol is for read-write binding.

    if case .success(.success) = result {
        dismiss()
    }
    self.error = error
    refundError.toggle()
    

    By writing like above, you're saying that in any case: success or failure, the refundError property will always toggle, and that leads to:

    Part of the problem seems to be that the errors are presented after the successful case


    You can fix it by breaking result into two cases, as @lorem said in his comment. Then change refundError = true only, since it's a @State object and was linked with .alert(isPresented:). It will automatically return to false when the alert is dismissed.

    swift result {
    case .success(let state):
        if state == .success {
            dismiss()
        }
    case .failure(let storeKitError):
        self.error = storeKitError
        self.refundError = true
    }
    

    Finally, you don't have to use another @State var gotError: String here because you already stored error. So, I recommend using computed property to get the error localized description.

    private var gotError: String {
        error?.localizedDescription ?? ""
    }
    
    ...
    .alert("Refund error: \(gotError)", isPresented: $refundError, actions: {})