[in_app_purchase] Add support for promotional offers through Store-Kit wrappers (#4458)

diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m
index b51f622..84e6f71 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m
+++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m
@@ -111,7 +111,7 @@
   XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed);
 }
 
-- (void)testAddPaymentSuccessWithMockQueue {
+- (void)testAddPaymentSuccessWithoutPaymentDiscount {
   XCTestExpectation* expectation =
       [self expectationWithDescription:@"result should return success state"];
   FlutterMethodCall* call =
@@ -129,6 +129,9 @@
         SKPaymentTransaction* transaction = transactions[0];
         if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
           transactionForUpdateBlock = transaction;
+          if (@available(iOS 12.2, *)) {
+            XCTAssertNil(transaction.payment.paymentDiscount);
+          }
           [expectation fulfill];
         }
       }
@@ -147,6 +150,93 @@
   XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased);
 }
 
+- (void)testAddPaymentSuccessWithPaymentDiscount {
+  XCTestExpectation* expectation =
+      [self expectationWithDescription:@"result should return success state"];
+  FlutterMethodCall* call =
+      [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
+                                        arguments:@{
+                                          @"productIdentifier" : @"123",
+                                          @"quantity" : @(1),
+                                          @"simulatesAskToBuyInSandbox" : @YES,
+                                          @"paymentDiscount" : @{
+                                            @"identifier" : @"test_identifier",
+                                            @"keyIdentifier" : @"test_key_identifier",
+                                            @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003",
+                                            @"signature" : @"test_signature",
+                                            @"timestamp" : @(1635847102),
+                                          }
+                                        }];
+  SKPaymentQueueStub* queue = [SKPaymentQueueStub new];
+  queue.testState = SKPaymentTransactionStatePurchased;
+  __block SKPaymentTransaction* transactionForUpdateBlock;
+  self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
+      transactionsUpdated:^(NSArray<SKPaymentTransaction*>* _Nonnull transactions) {
+        SKPaymentTransaction* transaction = transactions[0];
+        if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
+          transactionForUpdateBlock = transaction;
+          if (@available(iOS 12.2, *)) {
+            SKPaymentDiscount* paymentDiscount = transaction.payment.paymentDiscount;
+            XCTAssertEqual(paymentDiscount.identifier, @"test_identifier");
+            XCTAssertEqual(paymentDiscount.keyIdentifier, @"test_key_identifier");
+            XCTAssertEqualObjects(
+                paymentDiscount.nonce,
+                [[NSUUID alloc] initWithUUIDString:@"4a11a9cc-3bc3-11ec-8d3d-0242ac130003"]);
+            XCTAssertEqual(paymentDiscount.signature, @"test_signature");
+            XCTAssertEqual(paymentDiscount.timestamp, @(1635847102));
+          }
+          [expectation fulfill];
+        }
+      }
+      transactionRemoved:nil
+      restoreTransactionFailed:nil
+      restoreCompletedTransactionsFinished:nil
+      shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) {
+        return YES;
+      }
+      updatedDownloads:nil];
+  [queue addTransactionObserver:self.plugin.paymentQueueHandler];
+  [self.plugin handleMethodCall:call
+                         result:^(id r){
+                         }];
+  [self waitForExpectations:@[ expectation ] timeout:5];
+  XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased);
+}
+
+- (void)testAddPaymentFailureWithInvalidPaymentDiscount {
+  XCTestExpectation* expectation =
+      [self expectationWithDescription:@"result should return success state"];
+  FlutterMethodCall* call =
+      [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
+                                        arguments:@{
+                                          @"productIdentifier" : @"123",
+                                          @"quantity" : @(1),
+                                          @"simulatesAskToBuyInSandbox" : @YES,
+                                          @"paymentDiscount" : @{
+                                            @"keyIdentifier" : @"test_key_identifier",
+                                            @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003",
+                                            @"signature" : @"test_signature",
+                                            @"timestamp" : @(1635847102),
+                                          }
+                                        }];
+
+  [self.plugin
+      handleMethodCall:call
+                result:^(id r) {
+                  XCTAssertTrue([r isKindOfClass:FlutterError.class]);
+                  FlutterError* result = r;
+                  XCTAssertEqualObjects(result.code, @"storekit_invalid_payment_discount_object");
+                  XCTAssertEqualObjects(result.message,
+                                        @"You have requested a payment and specified a payment "
+                                        @"discount with invalid properties. When specifying a "
+                                        @"payment discount the 'identifier' field is mandatory.");
+                  XCTAssertEqualObjects(result.details, call.arguments);
+                  [expectation fulfill];
+                }];
+
+  [self waitForExpectations:@[ expectation ] timeout:5];
+}
+
 - (void)testAddPaymentWithNullSandboxArgument {
   XCTestExpectation* expectation =
       [self expectationWithDescription:@"result should return success state"];
diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m
index 364505d..e4277d3 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m
+++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m
@@ -61,6 +61,14 @@
       [self setValue:map[@"subscriptionGroupIdentifier"] ?: [NSNull null]
               forKey:@"subscriptionGroupIdentifier"];
     }
+    if (@available(iOS 12.2, *)) {
+      NSMutableArray *discounts = [[NSMutableArray alloc] init];
+      for (NSDictionary *discountMap in map[@"discounts"]) {
+        [discounts addObject:[[SKProductDiscountStub alloc] initWithMap:discountMap]];
+      }
+
+      [self setValue:discounts forKey:@"discounts"];
+    }
   }
   return self;
 }
diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m
index 89a7b2c..e014419 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m
+++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m
@@ -14,6 +14,7 @@
 @property(strong, nonatomic) NSMutableDictionary *productMap;
 @property(strong, nonatomic) NSDictionary *productResponseMap;
 @property(strong, nonatomic) NSDictionary *paymentMap;
+@property(copy, nonatomic) NSDictionary *paymentDiscountMap;
 @property(strong, nonatomic) NSDictionary *transactionMap;
 @property(strong, nonatomic) NSDictionary *errorMap;
 @property(strong, nonatomic) NSDictionary *localeMap;
@@ -45,6 +46,9 @@
     self.productMap[@"subscriptionPeriod"] = self.periodMap;
     self.productMap[@"introductoryPrice"] = self.discountMap;
   }
+  if (@available(iOS 12.2, *)) {
+    self.productMap[@"discounts"] = @[ self.discountMap ];
+  }
 
   if (@available(iOS 12.0, *)) {
     self.productMap[@"subscriptionGroupIdentifier"] = @"com.group";
@@ -59,6 +63,13 @@
     @"applicationUsername" : @"app user name",
     @"simulatesAskToBuyInSandbox" : @(NO)
   };
+  self.paymentDiscountMap = @{
+    @"identifier" : @"payment_discount_identifier",
+    @"keyIdentifier" : @"payment_discount_key_identifier",
+    @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52",
+    @"signature" : @"this is a encrypted signature",
+    @"timestamp" : @([NSDate date].timeIntervalSince1970),
+  };
   NSDictionary *originalTransactionMap = @{
     @"transactionIdentifier" : @"567",
     @"transactionState" : @(SKPaymentTransactionStatePurchasing),
@@ -175,4 +186,134 @@
   }
 }
 
+- (void)testSKPaymentDiscountFromMap {
+  if (@available(iOS 12.2, *)) {
+    NSString *error = nil;
+    SKPaymentDiscount *paymentDiscount =
+        [FIAObjectTranslator getSKPaymentDiscountFromMap:self.paymentDiscountMap withError:&error];
+
+    XCTAssertEqual(paymentDiscount.identifier, self.paymentDiscountMap[@"identifier"]);
+    XCTAssertEqual(paymentDiscount.keyIdentifier, self.paymentDiscountMap[@"keyIdentifier"]);
+    XCTAssertEqualObjects(paymentDiscount.nonce,
+                          [[NSUUID alloc] initWithUUIDString:self.paymentDiscountMap[@"nonce"]]);
+    XCTAssertEqual(paymentDiscount.signature, self.paymentDiscountMap[@"signature"]);
+    XCTAssertEqual(paymentDiscount.timestamp, self.paymentDiscountMap[@"timestamp"]);
+  }
+}
+
+- (void)testSKPaymentDiscountFromMapMissingIdentifier {
+  if (@available(iOS 12.2, *)) {
+    NSArray *invalidValues = @[ [NSNull null], @(1), @"" ];
+
+    for (id value in invalidValues) {
+      NSDictionary *discountMap = @{
+        @"identifier" : value,
+        @"keyIdentifier" : @"payment_discount_key_identifier",
+        @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52",
+        @"signature" : @"this is a encrypted signature",
+        @"timestamp" : @([NSDate date].timeIntervalSince1970),
+      };
+
+      NSString *error = nil;
+      [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error];
+
+      XCTAssertNotNil(error);
+      XCTAssertEqualObjects(
+          error, @"When specifying a payment discount the 'identifier' field is mandatory.");
+    }
+  }
+}
+
+- (void)testSKPaymentDiscountFromMapMissingKeyIdentifier {
+  if (@available(iOS 12.2, *)) {
+    NSArray *invalidValues = @[ [NSNull null], @(1), @"" ];
+
+    for (id value in invalidValues) {
+      NSDictionary *discountMap = @{
+        @"identifier" : @"payment_discount_identifier",
+        @"keyIdentifier" : value,
+        @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52",
+        @"signature" : @"this is a encrypted signature",
+        @"timestamp" : @([NSDate date].timeIntervalSince1970),
+      };
+
+      NSString *error = nil;
+      [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error];
+
+      XCTAssertNotNil(error);
+      XCTAssertEqualObjects(
+          error, @"When specifying a payment discount the 'keyIdentifier' field is mandatory.");
+    }
+  }
+}
+
+- (void)testSKPaymentDiscountFromMapMissingNonce {
+  if (@available(iOS 12.2, *)) {
+    NSArray *invalidValues = @[ [NSNull null], @(1), @"" ];
+
+    for (id value in invalidValues) {
+      NSDictionary *discountMap = @{
+        @"identifier" : @"payment_discount_identifier",
+        @"keyIdentifier" : @"payment_discount_key_identifier",
+        @"nonce" : value,
+        @"signature" : @"this is a encrypted signature",
+        @"timestamp" : @([NSDate date].timeIntervalSince1970),
+      };
+
+      NSString *error = nil;
+      [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error];
+
+      XCTAssertNotNil(error);
+      XCTAssertEqualObjects(error,
+                            @"When specifying a payment discount the 'nonce' field is mandatory.");
+    }
+  }
+}
+
+- (void)testSKPaymentDiscountFromMapMissingSignature {
+  if (@available(iOS 12.2, *)) {
+    NSArray *invalidValues = @[ [NSNull null], @(1), @"" ];
+
+    for (id value in invalidValues) {
+      NSDictionary *discountMap = @{
+        @"identifier" : @"payment_discount_identifier",
+        @"keyIdentifier" : @"payment_discount_key_identifier",
+        @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52",
+        @"signature" : value,
+        @"timestamp" : @([NSDate date].timeIntervalSince1970),
+      };
+
+      NSString *error = nil;
+      [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error];
+
+      XCTAssertNotNil(error);
+      XCTAssertEqualObjects(
+          error, @"When specifying a payment discount the 'signature' field is mandatory.");
+    }
+  }
+}
+
+- (void)testSKPaymentDiscountFromMapMissingTimestamp {
+  if (@available(iOS 12.2, *)) {
+    NSArray *invalidValues = @[ [NSNull null], @"", @(-1) ];
+
+    for (id value in invalidValues) {
+      NSDictionary *discountMap = @{
+        @"identifier" : @"payment_discount_identifier",
+        @"keyIdentifier" : @"payment_discount_key_identifier",
+        @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52",
+        @"signature" : @"this is a encrypted signature",
+        @"timestamp" : value,
+      };
+
+      NSString *error = nil;
+      [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error];
+
+      XCTAssertNotNil(error);
+      XCTAssertEqualObjects(
+          error, @"When specifying a payment discount the 'timestamp' field is mandatory.");
+    }
+  }
+}
+
 @end
diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h
index 95a5edc..eb97ceb 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h
+++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h
@@ -20,6 +20,10 @@
 + (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount
     API_AVAILABLE(ios(11.2));
 
+// Converts an array of SKProductDiscount instances into an array of dictionaries.
++ (nonnull NSArray *)getMapArrayFromSKProductDiscounts:
+    (nonnull NSArray<SKProductDiscount *> *)productDiscounts API_AVAILABLE(ios(12.2));
+
 // Converts an instance of SKProductsResponse into a dictionary.
 + (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse;
 
@@ -47,6 +51,11 @@
                  andSKPaymentTransaction:(SKPaymentTransaction *)transaction
     API_AVAILABLE(ios(13), macos(10.15), watchos(6.2));
 
+// Creates an instance of the SKPaymentDiscount class based on the supplied dictionary.
++ (nullable SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map
+                                                  withError:(NSString *_Nullable *_Nullable)error
+    API_AVAILABLE(ios(12.2));
+
 @end
 ;
 
diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m
index 0125604..3ceb512 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m
+++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m
@@ -35,6 +35,10 @@
                        ?: [NSNull null]
             forKey:@"introductoryPrice"];
   }
+  if (@available(iOS 12.2, *)) {
+    [map setObject:[FIAObjectTranslator getMapArrayFromSKProductDiscounts:product.discounts]
+            forKey:@"discounts"];
+  }
   if (@available(iOS 12.0, *)) {
     [map setObject:product.subscriptionGroupIdentifier ?: [NSNull null]
             forKey:@"subscriptionGroupIdentifier"];
@@ -49,6 +53,17 @@
   return @{@"numberOfUnits" : @(period.numberOfUnits), @"unit" : @(period.unit)};
 }
 
++ (nonnull NSArray *)getMapArrayFromSKProductDiscounts:
+    (nonnull NSArray<SKProductDiscount *> *)productDiscounts {
+  NSMutableArray *discountsMapArray = [[NSMutableArray alloc] init];
+
+  for (SKProductDiscount *productDiscount in productDiscounts) {
+    [discountsMapArray addObject:[FIAObjectTranslator getMapFromSKProductDiscount:productDiscount]];
+  }
+
+  return discountsMapArray;
+}
+
 + (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount {
   if (!discount) {
     return nil;
@@ -193,4 +208,63 @@
   return map;
 }
 
++ (SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map
+                                         withError:(NSString **)error {
+  if (!map || map.count <= 0) {
+    return nil;
+  }
+
+  NSString *identifier = map[@"identifier"];
+  NSString *keyIdentifier = map[@"keyIdentifier"];
+  NSString *nonce = map[@"nonce"];
+  NSString *signature = map[@"signature"];
+  NSNumber *timestamp = map[@"timestamp"];
+
+  if (!identifier || ![identifier isKindOfClass:NSString.class] ||
+      [identifier isEqualToString:@""]) {
+    if (error) {
+      *error = @"When specifying a payment discount the 'identifier' field is mandatory.";
+    }
+    return nil;
+  }
+
+  if (!keyIdentifier || ![keyIdentifier isKindOfClass:NSString.class] ||
+      [keyIdentifier isEqualToString:@""]) {
+    if (error) {
+      *error = @"When specifying a payment discount the 'keyIdentifier' field is mandatory.";
+    }
+    return nil;
+  }
+
+  if (!nonce || ![nonce isKindOfClass:NSString.class] || [nonce isEqualToString:@""]) {
+    if (error) {
+      *error = @"When specifying a payment discount the 'nonce' field is mandatory.";
+    }
+    return nil;
+  }
+
+  if (!signature || ![signature isKindOfClass:NSString.class] || [signature isEqualToString:@""]) {
+    if (error) {
+      *error = @"When specifying a payment discount the 'signature' field is mandatory.";
+    }
+    return nil;
+  }
+
+  if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp intValue] <= 0) {
+    if (error) {
+      *error = @"When specifying a payment discount the 'timestamp' field is mandatory.";
+    }
+    return nil;
+  }
+
+  SKPaymentDiscount *discount =
+      [[SKPaymentDiscount alloc] initWithIdentifier:identifier
+                                      keyIdentifier:keyIdentifier
+                                              nonce:[[NSUUID alloc] initWithUUIDString:nonce]
+                                          signature:signature
+                                          timestamp:timestamp];
+
+  return discount;
+}
+
 @end
diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m
index 7e2d2ca..661f57f 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m
+++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m
@@ -203,6 +203,25 @@
                                            ? NO
                                            : [simulatesAskToBuyInSandbox boolValue];
 
+  if (@available(iOS 12.2, *)) {
+    NSString *error = nil;
+    SKPaymentDiscount *paymentDiscount = [FIAObjectTranslator
+        getSKPaymentDiscountFromMap:[paymentMap objectForKey:@"paymentDiscount"]
+                          withError:&error];
+
+    if (error) {
+      result([FlutterError
+          errorWithCode:@"storekit_invalid_payment_discount_object"
+                message:[NSString stringWithFormat:@"You have requested a payment and specified a "
+                                                   @"payment discount with invalid properties. %@",
+                                                   error]
+                details:call.arguments]);
+      return;
+    }
+
+    payment.paymentDiscount = paymentDiscount;
+  }
+
   if (![self.paymentQueueHandler addPayment:payment]) {
     result([FlutterError
         errorWithCode:@"storekit_duplicate_product_object"
diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart
index 3decba2..918eac0 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart
+++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart
@@ -378,12 +378,14 @@
 @JsonSerializable(createToJson: true)
 class SKPaymentWrapper {
   /// Creates a new [SKPaymentWrapper] with the provided information.
-  const SKPaymentWrapper(
-      {required this.productIdentifier,
-      this.applicationUsername,
-      this.requestData,
-      this.quantity = 1,
-      this.simulatesAskToBuyInSandbox = false});
+  const SKPaymentWrapper({
+    required this.productIdentifier,
+    this.applicationUsername,
+    this.requestData,
+    this.quantity = 1,
+    this.simulatesAskToBuyInSandbox = false,
+    this.paymentDiscount,
+  });
 
   /// Constructs an instance of this from a key value map of data.
   ///
@@ -450,6 +452,14 @@
   /// testing.
   final bool simulatesAskToBuyInSandbox;
 
+  /// The details of a discount that should be applied to the payment.
+  ///
+  /// See [Implementing Promotional Offers in Your App](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/implementing_promotional_offers_in_your_app?language=objc)
+  /// for more information on generating keys and creating offers for
+  /// auto-renewable subscriptions. If set to `null` no discount will be
+  /// applied to this payment.
+  final SKPaymentDiscountWrapper? paymentDiscount;
+
   @override
   bool operator ==(Object other) {
     if (identical(other, this)) {
@@ -473,3 +483,103 @@
   @override
   String toString() => _$SKPaymentWrapperToJson(this).toString();
 }
+
+/// Dart wrapper around StoreKit's
+/// [SKPaymentDiscount](https://developer.apple.com/documentation/storekit/skpaymentdiscount?language=objc).
+///
+/// Used to indicate a discount is applicable to a payment. The
+/// [SKPaymentDiscountWrapper] instance should be assigned to the
+/// [SKPaymentWrapper] object to which the discount should be applied.
+/// Discount offers are set up in App Store Connect. See [Implementing Promotional Offers in Your App](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/implementing_promotional_offers_in_your_app?language=objc)
+/// for more information.
+@immutable
+@JsonSerializable(createToJson: true)
+class SKPaymentDiscountWrapper {
+  /// Creates a new [SKPaymentDiscountWrapper] with the provided information.
+  const SKPaymentDiscountWrapper({
+    required this.identifier,
+    required this.keyIdentifier,
+    required this.nonce,
+    required this.signature,
+    required this.timestamp,
+  });
+
+  /// Constructs an instance of this from a key value map of data.
+  ///
+  /// The map needs to have named string keys with values matching the names and
+  /// types of all of the members on this class.
+  factory SKPaymentDiscountWrapper.fromJson(Map<String, dynamic> map) {
+    assert(map != null);
+    return _$SKPaymentDiscountWrapperFromJson(map);
+  }
+
+  /// Creates a Map object describes the payment object.
+  Map<String, dynamic> toMap() {
+    return <String, dynamic>{
+      'identifier': identifier,
+      'keyIdentifier': keyIdentifier,
+      'nonce': nonce,
+      'signature': signature,
+      'timestamp': timestamp,
+    };
+  }
+
+  /// The identifier of the discount offer.
+  ///
+  /// The identifier must match one of the offers set up in App Store Connect.
+  final String identifier;
+
+  /// A string identifying the key that is used to generate the signature.
+  ///
+  /// Keys are generated and downloaded from App Store Connect. See
+  /// [Generating a Signature for Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers?language=objc)
+  /// for more information.
+  final String keyIdentifier;
+
+  /// A universal unique identifier (UUID) created together with the signature.
+  ///
+  /// The UUID should be generated on your server when it creates the
+  /// `signature` for the payment discount. The UUID can be used once, a new
+  /// UUID should be created for each payment request. The string representation
+  /// of the UUID must be lowercase. See
+  /// [Generating a Signature for Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers?language=objc)
+  /// for more information.
+  final String nonce;
+
+  /// A cryptographically signed string representing the to properties of the
+  /// promotional offer.
+  ///
+  /// The signature is string signed with a private key and contains all the
+  /// properties of the promotional offer. To keep you private key secure the
+  /// signature should be created on a server. See [Generating a Signature for Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers?language=objc)
+  /// for more information.
+  final String signature;
+
+  /// The date and time the signature was created.
+  ///
+  /// The timestamp should be formatted in Unix epoch time. See
+  /// [Generating a Signature for Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers?language=objc)
+  /// for more information.
+  final int timestamp;
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(other, this)) {
+      return true;
+    }
+    if (other.runtimeType != runtimeType) {
+      return false;
+    }
+    final SKPaymentDiscountWrapper typedOther =
+        other as SKPaymentDiscountWrapper;
+    return typedOther.identifier == identifier &&
+        typedOther.keyIdentifier == keyIdentifier &&
+        typedOther.nonce == nonce &&
+        typedOther.signature == signature &&
+        typedOther.timestamp == timestamp;
+  }
+
+  @override
+  int get hashCode =>
+      hashValues(identifier, keyIdentifier, nonce, signature, timestamp);
+}
diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart
index 8c0e64b..f594ad4 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart
+++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart
@@ -32,3 +32,22 @@
       'quantity': instance.quantity,
       'simulatesAskToBuyInSandbox': instance.simulatesAskToBuyInSandbox,
     };
+
+SKPaymentDiscountWrapper _$SKPaymentDiscountWrapperFromJson(Map json) =>
+    SKPaymentDiscountWrapper(
+      identifier: json['identifier'] as String,
+      keyIdentifier: json['keyIdentifier'] as String,
+      nonce: json['nonce'] as String,
+      signature: json['signature'] as String,
+      timestamp: json['timestamp'] as int,
+    );
+
+Map<String, dynamic> _$SKPaymentDiscountWrapperToJson(
+        SKPaymentDiscountWrapper instance) =>
+    <String, dynamic>{
+      'identifier': instance.identifier,
+      'keyIdentifier': instance.keyIdentifier,
+      'nonce': instance.nonce,
+      'signature': instance.signature,
+      'timestamp': instance.timestamp,
+    };
diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart
index 1b681f2..4110754 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart
+++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart
@@ -241,6 +241,7 @@
     required this.price,
     this.subscriptionPeriod,
     this.introductoryPrice,
+    this.discounts = const <SKProductDiscountWrapper>[],
   });
 
   /// Constructing an instance from a map from the Objective-C layer.
@@ -295,6 +296,16 @@
   /// and their units and duration do not have to be matched.
   final SKProductDiscountWrapper? introductoryPrice;
 
+  /// An array of subscription offers available for the auto-renewable subscription (available on iOS 12.2 and higher).
+  ///
+  /// This property lists all promotional offers set up in App Store Connect. If
+  /// no promotional offers have been set up, this field returns an empty list.
+  /// Each [subscriptionPeriod] of individual discounts are independent of the
+  /// product's [subscriptionPeriod] and their units and duration do not have to
+  /// be matched.
+  @JsonKey(defaultValue: <SKProductDiscountWrapper>[])
+  final List<SKProductDiscountWrapper> discounts;
+
   @override
   bool operator ==(Object other) {
     if (identical(other, this)) {
@@ -311,7 +322,8 @@
         typedOther.subscriptionGroupIdentifier == subscriptionGroupIdentifier &&
         typedOther.price == price &&
         typedOther.subscriptionPeriod == subscriptionPeriod &&
-        typedOther.introductoryPrice == introductoryPrice;
+        typedOther.introductoryPrice == introductoryPrice &&
+        DeepCollectionEquality().equals(typedOther.discounts, discounts);
   }
 
   @override
@@ -323,7 +335,8 @@
       this.subscriptionGroupIdentifier,
       this.price,
       this.subscriptionPeriod,
-      this.introductoryPrice);
+      this.introductoryPrice,
+      this.discounts);
 }
 
 /// Object that indicates the locale of the price
diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart
index c9079ff..6eea3ff 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart
+++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart
@@ -65,6 +65,11 @@
           ? null
           : SKProductDiscountWrapper.fromJson(
               Map<String, dynamic>.from(json['introductoryPrice'] as Map)),
+      discounts: (json['discounts'] as List<dynamic>?)
+              ?.map((e) => SKProductDiscountWrapper.fromJson(
+                  Map<String, dynamic>.from(e as Map)))
+              .toList() ??
+          [],
     );
 
 SKPriceLocaleWrapper _$SKPriceLocaleWrapperFromJson(Map json) =>
diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart
index 6a33b75..4d010aa 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart
+++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart
@@ -84,6 +84,7 @@
       expect(wrapper.subscriptionGroupIdentifier, null);
       expect(wrapper.price, '');
       expect(wrapper.subscriptionPeriod, null);
+      expect(wrapper.discounts, <SKProductDiscountWrapper>[]);
     });
 
     test('toProductDetails() should return correct Product object', () {
diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart
index 595a074..a2cf2a0 100644
--- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart
+++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart
@@ -68,6 +68,7 @@
   price: '1.0',
   subscriptionPeriod: dummySubscription,
   introductoryPrice: dummyDiscount,
+  discounts: [dummyDiscount],
 );
 
 final SkProductResponseWrapper dummyProductResponseWrapper =
@@ -118,6 +119,7 @@
     'subscriptionPeriod':
         buildSubscriptionPeriodMap(product.subscriptionPeriod),
     'introductoryPrice': buildDiscountMap(product.introductoryPrice!),
+    'discounts': [buildDiscountMap(product.introductoryPrice!)],
   };
 }