Repository: flutter
Issue: [in_app_purchase] please add a simple way to determine the exact purchase error type on iOS
Labels: platform-ios,p: in_app_purchase,package,c: proposal,P3,team-ios,triaged-ios

<!-- Thank you for using Flutter!

     If you are looking for support, please check out our documentation
     or consider asking a question on Stack Overflow:
      * https://flutter.dev/
      * https://api.flutter.dev/
      * https://stackoverflow.com/questions/tagged/flutter?sort=frequent

     If you have found a bug or if our documentation doesn't have an answer
     to what you're looking for, then fill out the template below. Please read
     our guide to filing a bug first: https://flutter.dev/docs/resources/bug-reports
-->

## Use case

Users can cancel the payment by closing the payment sheet by pressing the "Cancel" button in the top right corner or tapping outside the sheet, see the screenshot.

<img src="https://user-images.githubusercontent.com/73189330/123043751-93de7500-d41a-11eb-897c-a356aaff17ac.jpg" width="300">

When this happens, the purchase object is pushed into the `purchaseStream` with its `error` field containing the error details. However, these error details are way too abstract to easily figure out what exactly has happened.

Here's an example of `IAPError` field values when user cancels the sheet:

```
code = "purchase_error"
message = "SKErrorDomain"
source = "app_store"
details = {NSUnderlyingError: {domain: ASDErrorDomain, userInfo: {NSLocalizedDescription: Unhandled exception, NSLocalizedFailureReason: An unknown error occurred, NSUnderlyingError: {domain: AMSErrorDomain, userInfo: {NSLocalizedDescription: Payment Sheet Failed, NSLocalizedFailureReason: Payment sheet cancelled}, code: 6}}, code: 907}}
```

Apparently, one can figure out the exact error only by parsing the `details` map which, when converted to JSON, contains the following data.

```json
{
    "NSUnderlyingError":{
       "domain":"ASDErrorDomain",
       "userInfo":{
          "NSLocalizedDescription":"Unhandled exception",
          "NSLocalizedFailureReason":"An unknown error occurred",
          "NSUnderlyingError":{
             "domain":"AMSErrorDomain",
             "userInfo":{
                "NSLocalizedDescription":"Payment Sheet Failed",
                "NSLocalizedFailureReason":"Payment sheet cancelled"
             },
             "code":6
          }
       },
       "code":907
    }
 }
```

Moreover, the `NSUnderlyingError.userInfo.code` field seems to contain an irrelevant value because the code for "user cancelled" error is `2`, not `6` (https://developer.apple.com/documentation/storekit/skerror/code/paymentcancelled). Although I'm no expert in iOS development so I can be wrong here.

It seems that the only way to find out whether the user has cancelled the payment sheet is doing

```dart
// Somehow get this value from IAPError.details.
final nsLocalizedFailureReason = "Payment sheet cancelled";

if (nsLocalizedFailureReason == "Payment sheet cancelled") {
  // handle user cancellation
}
```

(which is wrong because on different locales this value will be different because it's localized).

I find this situation quite silly. Either I'm completely wrong and missed something obvious, or there is indeed no simple way to determine the exact error type on iOS using the official `in_app_purchases` plugin.

On Android I get values like `BillingResponse.userCanceled` in the `IAPError.message` field (which is strange because the `message` field is supposed to contain human-readable error message, and `BillingResponse.userCanceled` is not very human-readable, but at least it works), while on iOS I have to parse arbitrary maps to find out which error it is.

## Proposal

Add some simple way to determine the kind of exact type of purchase error on iOS. It's fine if it's going to be just a string in the `IAPError.message` field like it is on Android as long as the developers don't have to parse multi-level maps.

Also it would be nice if there was some `enum` containing at least the common error to make filtering by error type simple, like

```dart
if (purchase.error.code == PurchaseErrorCode.userCancelled) {
  // Handle user cancellation.
}
```