[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!)],
};
}