Apple Pay, Error Reporting and Crash

Apple Pay, Error Reporting and Crash

Working on the Apple platform brings a new kind of challenge and excitement. The environment where it will never cease to amaze you with exciting and never-seen-before circumstances. Today I ran into similar problem which consumed 75% of my day and it wasn't all bad. It gave me insight into previously unknown realm.

While working on app, we found that it was crashing while trying to handle failure case during placing an order with ApplePay. Now this was problematic for two reasons.

  1. Crash - for obvious reasons
  2. No error reporting - This was bad that user might just get away as they may never know what caused the crash or unsuccessful purchase

I started digging into the issue by placing exception breakpoints where app is supposed to halt execution at the starting point of crash. Then I found a code similar to the one listed below

func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {
    myAPI.makePayment().fail { error in
        if error.message == "Shipping Error" {
            let error = NSError(domain: "Payment Error", code: 100, userInfo: ["messgae": "Invalid Shipping Address"])
            completion(PKPaymentAuthorizationResult(status: .failure, errors: [error]))
        }
    }
}

For more context, func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) is the delegate method associated with PKPaymentAuthorizationViewControllerDelegate protocol which is used to check if given payment method associated with ApplePay has succeeded in authorization or not.

Now, we were getting error from server and based on that info we were creating our own NSError object which was used to create an instance of PKPaymentAuthorizationResult which was then passed as a parameter to completion block. So far so good. But I was stumped as to why app would crash on following line?

completion(PKPaymentAuthorizationResult(status: .failure, errors: [error]))

This led me to the dead end as it was leading into Apple's own private implementation and I could not jump into it without incorporating significant sorcery. It dawned to me that I should probably look at some other examples on how PKPaymentAuthorizationResult is used for error handling.

And Boom......

I was surprised how PKPaymentAuthorizationResult pretends taking one of the argument as of type Error, but it does not accept all kind of errors. Based on my further observations and results it looked like if you want to send an error back, you must create an error object using APIs involved with PKPaymentRequest. Following options are possible based on what failed while processing payment.

PKPaymentRequest.paymentBillingAddressInvalidError(withKey: "", localizedDescription: "")
PKPaymentRequest.paymentShippingAddressInvalidError(withKey: "", localizedDescription: "")
PKPaymentRequest.paymentContactInvalidError(withContactField: PKContactField(rawValue: ""), localizedDescription: "")
PKPaymentRequest.paymentShippingAddressUnserviceableError(withLocalizedDescription: "")

You may customize them the way you want, but they must confirm to these types before being returned to completion block in the form of PKPaymentAuthorizationResult object.

Now back to our original problem - we have to fix the crash associated with creating a custom NSError object instead of using one of those provided by Apple. Let's demonstrate and  fix the problem using the same example above,

func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {
    myAPI.makePayment().fail { error in
        if error.message == "Shipping Error" {
            let error = PKPaymentRequest.paymentShippingAddressInvalidError(withKey: CNPostalAddressPostalCodeKey, localizedDescription: "Invalid Shipping Address")
            completion(PKPaymentAuthorizationResult(status: .failure, errors: [error]))
        }
    }
}

And now it should work fine. I wonder why Apple didn't make it so obvious that only specific type of Error objects must be passed to this method? It's confusing and I don't blame developers who may pass arbitrary Error objects to it inadvertently causing a crash.

Another thing is when it crashes, there is not much clue why it may have crashed. All I see is completion handler and PKPaymentAuthorizationResult object passed to it. In spite of being an iOS developer for many years, I could not say what was wrong with this seemingly innocuous piece of code. Hopefully, in the future, Apple will enforce us to pass only certain types of error objects to PKPaymentAuthorizationResult or at least give more clear and verbose error message explaining what might have caused the crash.