[in_app_purchase] Make sure unsupported userInfo doesn't crash App (#5111)

diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md
index 087c6e3..d614baa 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md
+++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.3.0+4
+
+* Ensures that `NSError` instances with an unexpected value for the `userInfo` field don't crash the app, but send an explanatory message instead.
+
 ## 0.3.0+3
 
 * Implements transaction caching for StoreKit ensuring transactions are delivered to the Flutter client.
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m
index 0f689f6..c4e1ac1 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m
+++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m
@@ -158,6 +158,56 @@
   XCTAssertEqualObjects(map, self.errorMap);
 }
 
+- (void)testErrorWithNSNumberAsUserInfo {
+  NSError *error = [NSError errorWithDomain:SKErrorDomain code:3 userInfo:@{@"key" : @42}];
+  NSDictionary *expectedMap =
+      @{@"domain" : SKErrorDomain, @"code" : @3, @"userInfo" : @{@"key" : @42}};
+  NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error];
+  XCTAssertEqualObjects(expectedMap, map);
+}
+
+- (void)testErrorWithMultipleUnderlyingErrors {
+  NSError *underlyingErrorOne = [NSError errorWithDomain:SKErrorDomain code:2 userInfo:nil];
+  NSError *underlyingErrorTwo = [NSError errorWithDomain:SKErrorDomain code:1 userInfo:nil];
+  NSError *mainError = [NSError
+      errorWithDomain:SKErrorDomain
+                 code:3
+             userInfo:@{@"underlyingErrors" : @[ underlyingErrorOne, underlyingErrorTwo ]}];
+  NSDictionary *expectedMap = @{
+    @"domain" : SKErrorDomain,
+    @"code" : @3,
+    @"userInfo" : @{
+      @"underlyingErrors" : @[
+        @{@"domain" : SKErrorDomain, @"code" : @2, @"userInfo" : @{}},
+        @{@"domain" : SKErrorDomain, @"code" : @1, @"userInfo" : @{}}
+      ]
+    }
+  };
+  NSDictionary *map = [FIAObjectTranslator getMapFromNSError:mainError];
+  XCTAssertEqualObjects(expectedMap, map);
+}
+
+- (void)testErrorWithUnsupportedUserInfo {
+  NSError *error = [NSError errorWithDomain:SKErrorDomain
+                                       code:3
+                                   userInfo:@{@"user_info" : [[NSObject alloc] init]}];
+  NSDictionary *expectedMap = @{
+    @"domain" : SKErrorDomain,
+    @"code" : @3,
+    @"userInfo" : @{
+      @"user_info" : [NSString
+          stringWithFormat:
+              @"Unable to encode native userInfo object of type %@ to map. Please submit an "
+              @"issue at https://github.com/flutter/flutter/issues/new with the title "
+              @"\"[in_app_purchase_storekit] Unable to encode userInfo of type %@\" and add "
+              @"reproduction steps and the error details in the description field.",
+              [NSObject class], [NSObject class]]
+    }
+  };
+  NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error];
+  XCTAssertEqualObjects(expectedMap, map);
+}
+
 - (void)testLocaleToMap {
   if (@available(iOS 10.0, *)) {
     NSLocale *system = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m
index 3ceb512..5d87a68 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m
+++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m
@@ -167,20 +167,43 @@
   if (!error) {
     return nil;
   }
+
   NSMutableDictionary *userInfo = [NSMutableDictionary new];
   for (NSErrorUserInfoKey key in error.userInfo) {
     id value = error.userInfo[key];
-    if ([value isKindOfClass:[NSError class]]) {
-      userInfo[key] = [FIAObjectTranslator getMapFromNSError:value];
-    } else if ([value isKindOfClass:[NSURL class]]) {
-      userInfo[key] = [value absoluteString];
-    } else {
-      userInfo[key] = value;
-    }
+    userInfo[key] = [FIAObjectTranslator encodeNSErrorUserInfo:value];
   }
   return @{@"code" : @(error.code), @"domain" : error.domain ?: @"", @"userInfo" : userInfo};
 }
 
++ (id)encodeNSErrorUserInfo:(id)value {
+  if ([value isKindOfClass:[NSError class]]) {
+    return [FIAObjectTranslator getMapFromNSError:value];
+  } else if ([value isKindOfClass:[NSURL class]]) {
+    return [value absoluteString];
+  } else if ([value isKindOfClass:[NSNumber class]]) {
+    return value;
+  } else if ([value isKindOfClass:[NSString class]]) {
+    return value;
+  } else if ([value isKindOfClass:[NSArray class]]) {
+    NSMutableArray *errors = [[NSMutableArray alloc] init];
+    for (id error in value) {
+      [errors addObject:[FIAObjectTranslator encodeNSErrorUserInfo:error]];
+    }
+    return errors;
+  } else {
+    return [NSString
+        stringWithFormat:
+            @"Unable to encode native userInfo object of type %@ to map. Please submit an issue at "
+            @"https://github.com/flutter/flutter/issues/new with the title "
+            @"\"[in_app_purchase_storekit] "
+            @"Unable to encode userInfo of type %@\" and add reproduction steps and the error "
+            @"details in "
+            @"the description field.",
+            [value class], [value class]];
+  }
+}
+
 + (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront {
   if (!storefront) {
     return nil;
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml
index 51b5ce7..512d2a6 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml
+++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml
@@ -2,7 +2,7 @@
 description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework.
 repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_storekit
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
-version: 0.3.0+3
+version: 0.3.0+4
 
 environment:
   sdk: ">=2.14.0 <3.0.0"