[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"