[in_app_purchase][iOS] fix iOS promotional offers (SKPaymentDiscountWrapper was not used properly) (#6541)

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 52f59ef..324e060 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,8 @@
+## 0.3.3
+
+* Supports adding discount information to AppStorePurchaseParam. 
+* Fixes iOS Promotional Offers bug which prevents them from working.
+
 ## 0.3.2+2
 
 * Updates imports for `prefer_relative_imports`.
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 ed302d6..34d6867 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
@@ -390,4 +390,27 @@
   }
 }
 
+- (void)testSKPaymentDiscountFromMapOverflowingTimestamp {
+  if (@available(iOS 12.2, *)) {
+    NSDictionary *discountMap = @{
+      @"identifier" : @"payment_discount_identifier",
+      @"keyIdentifier" : @"payment_discount_key_identifier",
+      @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52",
+      @"signature" : @"this is a encrypted signature",
+      @"timestamp" : @1665044583595,  // timestamp 2022 Oct
+    };
+    NSString *error = nil;
+    SKPaymentDiscount *paymentDiscount =
+        [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error];
+    XCTAssertNil(error);
+    XCTAssertNotNil(paymentDiscount);
+    XCTAssertEqual(paymentDiscount.identifier, discountMap[@"identifier"]);
+    XCTAssertEqual(paymentDiscount.keyIdentifier, discountMap[@"keyIdentifier"]);
+    XCTAssertEqualObjects(paymentDiscount.nonce,
+                          [[NSUUID alloc] initWithUUIDString:discountMap[@"nonce"]]);
+    XCTAssertEqual(paymentDiscount.signature, discountMap[@"signature"]);
+    XCTAssertEqual(paymentDiscount.timestamp, discountMap[@"timestamp"]);
+  }
+}
+
 @end
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 d01eb9b..c656b58 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
@@ -277,7 +277,7 @@
     return nil;
   }
 
-  if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp intValue] <= 0) {
+  if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp longLongValue] <= 0) {
     if (error) {
       *error = @"When specifying a payment discount the 'timestamp' field is mandatory.";
     }
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m
index d64c245..bfc90ea 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m
+++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m
@@ -200,10 +200,11 @@
                                            : [simulatesAskToBuyInSandbox boolValue];
 
   if (@available(iOS 12.2, *)) {
+    NSDictionary *paymentDiscountMap = [self getNonNullValueFromDictionary:paymentMap
+                                                                    forKey:@"paymentDiscount"];
     NSString *error = nil;
-    SKPaymentDiscount *paymentDiscount = [FIAObjectTranslator
-        getSKPaymentDiscountFromMap:[paymentMap objectForKey:@"paymentDiscount"]
-                          withError:&error];
+    SKPaymentDiscount *paymentDiscount =
+        [FIAObjectTranslator getSKPaymentDiscountFromMap:paymentDiscountMap withError:&error];
 
     if (error) {
       result([FlutterError
@@ -367,6 +368,11 @@
   result(nil);
 }
 
+- (id)getNonNullValueFromDictionary:(NSDictionary *)dictionary forKey:(NSString *)key {
+  id value = dictionary[key];
+  return [value isKindOfClass:[NSNull class]] ? nil : value;
+}
+
 #pragma mark - transaction observer:
 
 - (void)handleTransactionsUpdated:(NSArray<SKPaymentTransaction *> *)transactions {
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart
index c03f15f..0e5e420 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart
@@ -75,7 +75,10 @@
             purchaseParam is AppStorePurchaseParam ? purchaseParam.quantity : 1,
         applicationUsername: purchaseParam.applicationUserName,
         simulatesAskToBuyInSandbox: purchaseParam is AppStorePurchaseParam &&
-            purchaseParam.simulatesAskToBuyInSandbox));
+            purchaseParam.simulatesAskToBuyInSandbox,
+        paymentDiscount: purchaseParam is AppStorePurchaseParam
+            ? purchaseParam.discount
+            : null));
 
     return true; // There's no error feedback from iOS here to return.
   }
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart
index 78e16e2..d360a2d 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart
@@ -405,7 +405,8 @@
       'applicationUsername': applicationUsername,
       'requestData': requestData,
       'quantity': quantity,
-      'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox
+      'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox,
+      'paymentDiscount': paymentDiscount?.toMap(),
     };
   }
 
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart
index 168ef5c..0e7e241 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart
@@ -14,6 +14,7 @@
     String? applicationUserName,
     this.quantity = 1,
     this.simulatesAskToBuyInSandbox = false,
+    this.discount,
   }) : super(
           productDetails: productDetails,
           applicationUserName: applicationUserName,
@@ -32,4 +33,7 @@
 
   /// Quantity of the product user requested to buy.
   final int quantity;
+
+  /// Discount applied to the product. The value is `null` when the product does not have a discount.
+  final SKPaymentDiscountWrapper? discount;
 }
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 f2193e5..0b6e21a 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.2+2
+version: 0.3.3
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
index 08b9c85..e64876d 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
@@ -30,6 +30,7 @@
   PlatformException? restoreException;
   SKError? testRestoredError;
   bool queueIsActive = false;
+  Map<String, dynamic> discountReceived = <String, dynamic>{};
 
   void reset() {
     transactions = <SKPaymentTransactionWrapper>[];
@@ -54,6 +55,7 @@
     restoreException = null;
     testRestoredError = null;
     queueIsActive = false;
+    discountReceived = <String, dynamic>{};
   }
 
   SKPaymentTransactionWrapper createPendingTransaction(String id,
@@ -169,6 +171,18 @@
       case '-[InAppPurchasePlugin addPayment:result:]':
         final String id = call.arguments['productIdentifier'] as String;
         final int quantity = call.arguments['quantity'] as int;
+
+        // Keep the received paymentDiscount parameter when testing payment with discount.
+        if (call.arguments['applicationUsername'] == 'userWithDiscount') {
+          if (call.arguments['paymentDiscount'] != null) {
+            final Map<dynamic, dynamic> discountArgument =
+                call.arguments['paymentDiscount'];
+            discountReceived = discountArgument.cast<String, dynamic>();
+          } else {
+            discountReceived = <String, dynamic>{};
+          }
+        }
+
         final SKPaymentTransactionWrapper transaction =
             createPendingTransaction(id, quantity: quantity);
         transactions.add(transaction);
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart
index 852599a..51ff2c2 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart
@@ -489,6 +489,38 @@
       expect(
           fakeStoreKitPlatform.finishedTransactions.first.payment.quantity, 5);
     });
+
+    test(
+        'buying non consumable with discount, should get purchase objects in the purchase update callback',
+        () async {
+      final List<PurchaseDetails> details = <PurchaseDetails>[];
+      final Completer<List<PurchaseDetails>> completer =
+          Completer<List<PurchaseDetails>>();
+      final Stream<List<PurchaseDetails>> stream =
+          iapStoreKitPlatform.purchaseStream;
+
+      late StreamSubscription<List<PurchaseDetails>> subscription;
+      subscription = stream.listen((List<PurchaseDetails> purchaseDetailsList) {
+        details.addAll(purchaseDetailsList);
+        if (purchaseDetailsList.first.status == PurchaseStatus.purchased) {
+          completer.complete(details);
+          subscription.cancel();
+        }
+      });
+      final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam(
+        productDetails:
+            AppStoreProductDetails.fromSKProduct(dummyProductWrapper),
+        applicationUserName: 'userWithDiscount',
+        discount: dummyPaymentDiscountWrapper,
+      );
+      await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam);
+
+      final List<PurchaseDetails> result = await completer.future;
+      expect(result.length, 2);
+      expect(result.first.productID, dummyProductWrapper.productIdentifier);
+      expect(fakeStoreKitPlatform.discountReceived,
+          dummyPaymentDiscountWrapper.toMap());
+    });
   });
 
   group('complete purchase', () {
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 de61268..b6de5e0 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
@@ -141,6 +141,21 @@
       expect(payment, equals(dummyPayment));
     });
 
+    test('SKPaymentWrapper should have propery values consistent with .toMap()',
+        () {
+      final Map<String, dynamic> mapResult = dummyPaymentWithDiscount.toMap();
+      expect(mapResult['productIdentifier'],
+          dummyPaymentWithDiscount.productIdentifier);
+      expect(mapResult['applicationUsername'],
+          dummyPaymentWithDiscount.applicationUsername);
+      expect(mapResult['requestData'], dummyPaymentWithDiscount.requestData);
+      expect(mapResult['quantity'], dummyPaymentWithDiscount.quantity);
+      expect(mapResult['simulatesAskToBuyInSandbox'],
+          dummyPaymentWithDiscount.simulatesAskToBuyInSandbox);
+      expect(mapResult['paymentDiscount'],
+          equals(dummyPaymentWithDiscount.paymentDiscount?.toMap()));
+    });
+
     test('Should construct correct SKError from json', () {
       final SKError error = SKError.fromJson(buildErrorMap(dummyError));
       expect(error, equals(dummyError));
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 946fbc8..6601a21 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
@@ -10,6 +10,15 @@
     requestData: 'fake-data-utf8',
     quantity: 2,
     simulatesAskToBuyInSandbox: true);
+
+final SKPaymentWrapper dummyPaymentWithDiscount = SKPaymentWrapper(
+    productIdentifier: 'prod-id',
+    applicationUsername: 'app-user-name',
+    requestData: 'fake-data-utf8',
+    quantity: 2,
+    simulatesAskToBuyInSandbox: true,
+    paymentDiscount: dummyPaymentDiscountWrapper);
+
 const SKError dummyError = SKError(
     code: 111,
     domain: 'dummy-domain',
@@ -186,3 +195,12 @@
   };
   return map;
 }
+
+final SKPaymentDiscountWrapper dummyPaymentDiscountWrapper =
+    SKPaymentDiscountWrapper.fromJson(const <String, dynamic>{
+  'identifier': 'dummy-discount-identifier',
+  'keyIdentifier': 'KEYIDTEST1',
+  'nonce': '00000000-0000-0000-0000-000000000000',
+  'signature': 'dummy-signature-string',
+  'timestamp': 1231231231,
+});