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 5aa64191e2a5..f0ad4921df94 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.2 + +* Adds the `identifier` and `type` fields to the `SKProductDiscountWrapper` to reflect the changes in the [SKProductDiscount](https://developer.apple.com/documentation/storekit/skproductdiscount?language=objc) in iOS 12.2. + ## 0.3.1+1 * Fixes avoid_redundant_argument_values lint warnings and minor typos. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m index e4277d3edd59..f5e44d78b157 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m @@ -31,6 +31,10 @@ - (instancetype)initWithMap:(NSDictionary *)map { [[SKProductSubscriptionPeriodStub alloc] initWithMap:map[@"subscriptionPeriod"]]; [self setValue:subscriptionPeriodSub forKey:@"subscriptionPeriod"]; [self setValue:map[@"paymentMode"] ?: @(0) forKey:@"paymentMode"]; + if (@available(iOS 12.2, *)) { + [self setValue:map[@"identifier"] ?: [NSNull null] forKey:@"identifier"]; + [self setValue:map[@"type"] ?: @(0) forKey:@"type"]; + } } return self; } 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 c4e1ac1d059d..ed302d61d9b0 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 @@ -10,7 +10,8 @@ @interface TranslatorTest : XCTestCase @property(strong, nonatomic) NSDictionary *periodMap; -@property(strong, nonatomic) NSDictionary *discountMap; +@property(strong, nonatomic) NSMutableDictionary *discountMap; +@property(strong, nonatomic) NSMutableDictionary *discountMissingIdentifierMap; @property(strong, nonatomic) NSMutableDictionary *productMap; @property(strong, nonatomic) NSDictionary *productResponseMap; @property(strong, nonatomic) NSDictionary *paymentMap; @@ -27,13 +28,27 @@ @implementation TranslatorTest - (void)setUp { self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; - self.discountMap = @{ + + self.discountMap = [[NSMutableDictionary alloc] initWithDictionary:@{ @"price" : @"1", @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], @"numberOfPeriods" : @1, @"subscriptionPeriod" : self.periodMap, - @"paymentMode" : @1 - }; + @"paymentMode" : @1, + }]; + if (@available(iOS 12.2, *)) { + self.discountMap[@"identifier"] = @"test offer id"; + self.discountMap[@"type"] = @(SKProductDiscountTypeIntroductory); + } + self.discountMissingIdentifierMap = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"price" : @"1", + @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], + @"numberOfPeriods" : @1, + @"subscriptionPeriod" : self.periodMap, + @"paymentMode" : @1, + @"identifier" : [NSNull null], + @"type" : @0, + }]; self.productMap = [[NSMutableDictionary alloc] initWithDictionary:@{ @"price" : @"1", @@ -274,6 +289,15 @@ - (void)testSKPaymentDiscountFromMapMissingIdentifier { } } +- (void)testGetMapFromSKProductDiscountMissingIdentifier { + if (@available(iOS 12.2, *)) { + SKProductDiscountStub *discount = + [[SKProductDiscountStub alloc] initWithMap:self.discountMissingIdentifierMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductDiscount:discount]; + XCTAssertEqualObjects(map, self.discountMissingIdentifierMap); + } +} + - (void)testSKPaymentDiscountFromMapMissingKeyIdentifier { if (@available(iOS 12.2, *)) { NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; 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 5d87a68de67c..d01eb9becf3d 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 @@ -74,8 +74,12 @@ + (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount { @"subscriptionPeriod" : [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:discount.subscriptionPeriod] ?: [NSNull null], - @"paymentMode" : @(discount.paymentMode) + @"paymentMode" : @(discount.paymentMode), }]; + if (@available(iOS 12.2, *)) { + [map setObject:discount.identifier ?: [NSNull null] forKey:@"identifier"]; + [map setObject:@(discount.type) forKey:@"type"]; + } // TODO(cyanglaz): NSLocale is a complex object, want to see the actual need of getting this // expanded to a map. Matching android to only get the currencySymbol for now. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.dart index 1c2bee5a069a..1fdbfebd6ec5 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.dart @@ -117,3 +117,27 @@ class _SerializedEnums { late SKSubscriptionPeriodUnit unit; late SKProductDiscountPaymentMode discountPaymentMode; } + +/// Serializer for [SKProductDiscountType]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SKProductDiscountTypeConverter()`. +class SKProductDiscountTypeConverter + implements JsonConverter { + /// Default const constructor. + const SKProductDiscountTypeConverter(); + + @override + SKProductDiscountType fromJson(int? json) { + if (json == null) { + return SKProductDiscountType.introductory; + } + return $enumDecode( + _$SKProductDiscountTypeEnumMap.cast(), + json); + } + + @override + int toJson(SKProductDiscountType object) => + _$SKProductDiscountTypeEnumMap[object]!; +} diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.g.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.g.dart index 0d05720dc7ae..dc6c17276c1c 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.g.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.g.dart @@ -35,3 +35,8 @@ const _$SKProductDiscountPaymentModeEnumMap = { SKProductDiscountPaymentMode.freeTrail: 2, SKProductDiscountPaymentMode.unspecified: -1, }; + +const _$SKProductDiscountTypeEnumMap = { + SKProductDiscountType.introductory: 0, + SKProductDiscountType.subscription: 1, +}; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart index 2354563261fc..5eace6fda69e 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -167,6 +167,24 @@ enum SKProductDiscountPaymentMode { unspecified, } +/// Dart wrapper around StoreKit's [SKProductDiscountType] +/// (https://developer.apple.com/documentation/storekit/skproductdiscounttype?language=objc) +/// +/// This is used as a property in the [SKProductDiscountWrapper]. +/// The values of the enum options are matching the [SKProductDiscountType]'s +/// values. +/// +/// Values representing the types of discount offers an app can present. +enum SKProductDiscountType { + /// A constant indicating the discount type is an introductory offer. + @JsonValue(0) + introductory, + + /// A constant indicating the discount type is a promotional offer. + @JsonValue(1) + subscription, +} + /// Dart wrapper around StoreKit's [SKProductDiscount](https://developer.apple.com/documentation/storekit/skproductdiscount?language=objc). /// /// It is used as a property in [SKProductWrapper]. @@ -182,7 +200,9 @@ class SKProductDiscountWrapper { required this.priceLocale, required this.numberOfPeriods, required this.paymentMode, - required this.subscriptionPeriod}); + required this.subscriptionPeriod, + required this.identifier, + required this.type}); /// Constructing an instance from a map from the Objective-C layer. /// @@ -214,6 +234,16 @@ class SKProductDiscountWrapper { /// and their units and duration do not have to be matched. final SKProductSubscriptionPeriodWrapper subscriptionPeriod; + /// A string used to uniquely identify a discount offer for a product. + /// + /// You set up offers and their identifiers in App Store Connect. + @JsonKey(defaultValue: null) + final String? identifier; + + /// Values representing the types of discount offers an app can present. + @SKProductDiscountTypeConverter() + final SKProductDiscountType type; + @override bool operator ==(Object other) { if (identical(other, this)) { @@ -227,12 +257,14 @@ class SKProductDiscountWrapper { other.priceLocale == priceLocale && other.numberOfPeriods == numberOfPeriods && other.paymentMode == paymentMode && - other.subscriptionPeriod == subscriptionPeriod; + other.subscriptionPeriod == subscriptionPeriod && + other.identifier == identifier && + other.type == type; } @override - int get hashCode => Object.hash( - price, priceLocale, numberOfPeriods, paymentMode, subscriptionPeriod); + int get hashCode => Object.hash(price, priceLocale, numberOfPeriods, + paymentMode, subscriptionPeriod, identifier, type); } /// Dart wrapper around StoreKit's [SKProduct](https://developer.apple.com/documentation/storekit/skproduct?language=objc). diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart index 6eea3ff34da0..9e891e75b497 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -42,6 +42,9 @@ SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) => (json['subscriptionPeriod'] as Map?)?.map( (k, e) => MapEntry(k as String, e), )), + identifier: json['identifier'] as String? ?? null, + type: + const SKProductDiscountTypeConverter().fromJson(json['type'] as int?), ); SKProductWrapper _$SKProductWrapperFromJson(Map json) => SKProductWrapper( 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 c037cddf147d..f131a411baed 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 @@ name: in_app_purchase_storekit 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.1+1 +version: 0.3.2 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart index 12fb21436ace..de61268e4009 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart @@ -38,6 +38,16 @@ void main() { expect(wrapper, equals(dummyDiscount)); }); + test( + 'SKProductDiscountWrapper missing identifier and type should have ' + 'property values consistent with map', () { + final SKProductDiscountWrapper wrapper = + SKProductDiscountWrapper.fromJson( + buildDiscountMapMissingIdentifierAndType( + dummyDiscountMissingIdentifierAndType)); + expect(wrapper, equals(dummyDiscountMissingIdentifierAndType)); + }); + test( 'SKProductDiscountWrapper should have properties to be default if map is empty', () { diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart index e4ef2e3ef432..946fbc81b74c 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart @@ -58,6 +58,19 @@ final SKProductDiscountWrapper dummyDiscount = SKProductDiscountWrapper( numberOfPeriods: 1, paymentMode: SKProductDiscountPaymentMode.payUpFront, subscriptionPeriod: dummySubscription, + identifier: 'id', + type: SKProductDiscountType.subscription, +); + +final SKProductDiscountWrapper dummyDiscountMissingIdentifierAndType = + SKProductDiscountWrapper( + price: '1.0', + priceLocale: dollarLocale, + numberOfPeriods: 1, + paymentMode: SKProductDiscountPaymentMode.payUpFront, + subscriptionPeriod: dummySubscription, + identifier: null, + type: SKProductDiscountType.introductory, ); final SKProductWrapper dummyProductWrapper = SKProductWrapper( @@ -106,6 +119,21 @@ Map buildDiscountMap(SKProductDiscountWrapper discount) { SKProductDiscountPaymentMode.values.indexOf(discount.paymentMode), 'subscriptionPeriod': buildSubscriptionPeriodMap(discount.subscriptionPeriod), + 'identifier': discount.identifier, + 'type': SKProductDiscountType.values.indexOf(discount.type) + }; +} + +Map buildDiscountMapMissingIdentifierAndType( + SKProductDiscountWrapper discount) { + return { + 'price': discount.price, + 'priceLocale': buildLocaleMap(discount.priceLocale), + 'numberOfPeriods': discount.numberOfPeriods, + 'paymentMode': + SKProductDiscountPaymentMode.values.indexOf(discount.paymentMode), + 'subscriptionPeriod': + buildSubscriptionPeriodMap(discount.subscriptionPeriod) }; }