Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 020800b

Browse files
committed
Support promotional offers
1 parent 0d33094 commit 020800b

File tree

9 files changed

+182
-9
lines changed

9 files changed

+182
-9
lines changed

packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ @interface TranslatorTest : XCTestCase
1414
@property(strong, nonatomic) NSMutableDictionary *productMap;
1515
@property(strong, nonatomic) NSDictionary *productResponseMap;
1616
@property(strong, nonatomic) NSDictionary *paymentMap;
17+
@property(strong, nonatomic) NSDictionary *paymentDiscountMap;
1718
@property(strong, nonatomic) NSDictionary *transactionMap;
1819
@property(strong, nonatomic) NSDictionary *errorMap;
1920
@property(strong, nonatomic) NSDictionary *localeMap;
@@ -50,6 +51,10 @@ - (void)setUp {
5051
self.productMap[@"subscriptionGroupIdentifier"] = @"com.group";
5152
}
5253

54+
if (@available(iOS 11.2, *)) {
55+
self.productMap[@"discounts"] = @[ self.discountMap ];
56+
}
57+
5358
self.productResponseMap =
5459
@{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : @[]};
5560
self.paymentMap = @{

packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ NS_ASSUME_NONNULL_BEGIN
4747
andSKPaymentTransaction:(SKPaymentTransaction *)transaction
4848
API_AVAILABLE(ios(13), macos(10.15), watchos(6.2));
4949

50+
// Creates an instance of the SKPaymentDiscount class based on the supplied disctionary.
51+
+ (SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map API_AVAILABLE(ios(12.2));
52+
5053
@end
5154
;
5255

packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,24 @@ + (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront
193193
return map;
194194
}
195195

196+
+ (SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map {
197+
if (!map) {
198+
return nil;
199+
}
200+
201+
NSString *identifier = map[@"identifier"];
202+
NSString *keyIdentifier = map[@"keyIdentifier"];
203+
NSUUID *nonce = [[NSUUID alloc] initWithUUIDString:map[@"nonce"]];
204+
NSString *signature = map[@"signature"];
205+
NSNumber *timestamp = map[@"timestamp"];
206+
207+
SKPaymentDiscount *discount = [[SKPaymentDiscount alloc] initWithIdentifier:identifier
208+
keyIdentifier:keyIdentifier
209+
nonce:nonce
210+
signature:signature
211+
timestamp:timestamp];
212+
213+
return discount;
214+
}
215+
196216
@end

packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -378,12 +378,14 @@ class SKError {
378378
@JsonSerializable(createToJson: true)
379379
class SKPaymentWrapper {
380380
/// Creates a new [SKPaymentWrapper] with the provided information.
381-
const SKPaymentWrapper(
382-
{required this.productIdentifier,
383-
this.applicationUsername,
384-
this.requestData,
385-
this.quantity = 1,
386-
this.simulatesAskToBuyInSandbox = false});
381+
const SKPaymentWrapper({
382+
required this.productIdentifier,
383+
this.applicationUsername,
384+
this.requestData,
385+
this.quantity = 1,
386+
this.simulatesAskToBuyInSandbox = false,
387+
this.paymentDiscount,
388+
});
387389

388390
/// Constructs an instance of this from a key value map of data.
389391
///
@@ -450,6 +452,13 @@ class SKPaymentWrapper {
450452
/// testing.
451453
final bool simulatesAskToBuyInSandbox;
452454

455+
/// The details of optional discount that should be applied to the payment.
456+
///
457+
/// See [Setting Up Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/setting_up_promotional_offers?language=objc)
458+
/// for more information on generating keys and creating offers for
459+
/// auto-renewable subscriptions.
460+
final SKPaymentDiscountWrapper? paymentDiscount;
461+
453462
@override
454463
bool operator ==(Object other) {
455464
if (identical(other, this)) {
@@ -473,3 +482,99 @@ class SKPaymentWrapper {
473482
@override
474483
String toString() => _$SKPaymentWrapperToJson(this).toString();
475484
}
485+
486+
/// Dart wrapper around StoreKit's
487+
/// [SKPaymentDiscount](https://developer.apple.com/documentation/storekit/skpaymentdiscount?language=objc).
488+
///
489+
/// Used to indicate a discount is applicable to a payment. The
490+
/// [SKPaymentDiscountWrapper] instance should be assigned to the
491+
/// [SKPaymentWrapper] object to which the discount should be applied.
492+
/// Discount offers are set up in App Store Connect.
493+
@immutable
494+
@JsonSerializable(createToJson: true)
495+
class SKPaymentDiscountWrapper {
496+
/// Creates a new [SKPaymentDiscountWrapper] with the provided information.
497+
const SKPaymentDiscountWrapper({
498+
required this.identifier,
499+
required this.keyIdentifier,
500+
required this.nonce,
501+
required this.signature,
502+
required this.timestamp,
503+
});
504+
505+
/// Constructs an instance of this from a key value map of data.
506+
///
507+
/// The map needs to have named string keys with values matching the names and
508+
/// types of all of the members on this class. The `map` parameter must not be
509+
/// null.
510+
factory SKPaymentDiscountWrapper.fromJson(Map<String, dynamic> map) {
511+
assert(map != null);
512+
return _$SKPaymentDiscountWrapperFromJson(map);
513+
}
514+
515+
/// Creates a Map object describes the payment object.
516+
Map<String, dynamic> toMap() {
517+
return <String, dynamic>{
518+
'identifier': identifier,
519+
'keyIdentifier': keyIdentifier,
520+
'nonce': nonce,
521+
'signature': signature,
522+
'timestamp': timestamp,
523+
};
524+
}
525+
526+
/// The identifier of the discount offer.
527+
///
528+
/// The identifier must match one of the offers set up in App Store Connect.
529+
final String identifier;
530+
531+
/// A string identifying the key that is used to generate the signature.
532+
///
533+
/// Keys are generated and downloaded from App Store Connect. See the "KEY ID"
534+
/// column in the App Store Promotions section in App Store Connect to use as
535+
/// the keyIdentifier.
536+
final String keyIdentifier;
537+
538+
/// A universal unique identifier created together with the signature.
539+
///
540+
/// The unique nonce should be generated on your server when it creates the
541+
/// `signature` for the payment discount. The nonce can be used once, a new
542+
/// nonce should be created for each payment request.
543+
/// The string representation of the nonce must be lowercase.
544+
final String nonce;
545+
546+
/// A cryptographically signed string representing the to properties of the
547+
/// promotional offer.
548+
///
549+
/// The signature is string signed with a private key and contains all the
550+
/// properties of the promotional offer. To keep you private key secure the
551+
/// 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)
552+
/// for more information.
553+
final String signature;
554+
555+
/// The date and time the signature was created.
556+
///
557+
/// The timestamp should be formatted in Unix epoch time.
558+
final int timestamp;
559+
560+
@override
561+
bool operator ==(Object other) {
562+
if (identical(other, this)) {
563+
return true;
564+
}
565+
if (other.runtimeType != runtimeType) {
566+
return false;
567+
}
568+
final SKPaymentDiscountWrapper typedOther =
569+
other as SKPaymentDiscountWrapper;
570+
return typedOther.identifier == identifier &&
571+
typedOther.keyIdentifier == keyIdentifier &&
572+
typedOther.nonce == nonce &&
573+
typedOther.signature == signature &&
574+
typedOther.timestamp == timestamp;
575+
}
576+
577+
@override
578+
int get hashCode =>
579+
hashValues(identifier, keyIdentifier, nonce, signature, timestamp);
580+
}

packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ class SKProductWrapper {
241241
required this.price,
242242
this.subscriptionPeriod,
243243
this.introductoryPrice,
244-
});
244+
List<SKProductDiscountWrapper>? discounts,
245+
}) : this.discounts = discounts ?? <SKProductDiscountWrapper>[];
245246

246247
/// Constructing an instance from a map from the Objective-C layer.
247248
///
@@ -295,6 +296,16 @@ class SKProductWrapper {
295296
/// and their units and duration do not have to be matched.
296297
final SKProductDiscountWrapper? introductoryPrice;
297298

299+
/// An array of subscription offers available for the auto-renewable subscription (available on iOS 12.2 and higher).
300+
///
301+
/// This property lists all promotional offers set up in App Store Connect. If
302+
/// no promotional offers have been set up this field returns an empty list.
303+
/// Each [subscriptionPeriod] of individual discounts are independent of the
304+
/// product's [subscriptionPeriod] and their units and duration do not have to
305+
/// be matched.
306+
@JsonKey(defaultValue: <SKProductDiscountWrapper>[])
307+
final List<SKProductDiscountWrapper> discounts;
308+
298309
@override
299310
bool operator ==(Object other) {
300311
if (identical(other, this)) {
@@ -311,7 +322,8 @@ class SKProductWrapper {
311322
typedOther.subscriptionGroupIdentifier == subscriptionGroupIdentifier &&
312323
typedOther.price == price &&
313324
typedOther.subscriptionPeriod == subscriptionPeriod &&
314-
typedOther.introductoryPrice == introductoryPrice;
325+
typedOther.introductoryPrice == introductoryPrice &&
326+
DeepCollectionEquality().equals(typedOther.discounts, discounts);
315327
}
316328

317329
@override
@@ -323,7 +335,8 @@ class SKProductWrapper {
323335
this.subscriptionGroupIdentifier,
324336
this.price,
325337
this.subscriptionPeriod,
326-
this.introductoryPrice);
338+
this.introductoryPrice,
339+
this.discounts);
327340
}
328341

329342
/// Object that indicates the locale of the price

packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ void main() {
8484
expect(wrapper.subscriptionGroupIdentifier, null);
8585
expect(wrapper.price, '');
8686
expect(wrapper.subscriptionPeriod, null);
87+
expect(wrapper.discounts, <SKProductDiscountWrapper>[]);
8788
});
8889

8990
test('toProductDetails() should return correct Product object', () {

packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ final SKProductWrapper dummyProductWrapper = SKProductWrapper(
6868
price: '1.0',
6969
subscriptionPeriod: dummySubscription,
7070
introductoryPrice: dummyDiscount,
71+
discounts: [dummyDiscount],
7172
);
7273

7374
final SkProductResponseWrapper dummyProductResponseWrapper =
@@ -118,6 +119,7 @@ Map<String, dynamic> buildProductMap(SKProductWrapper product) {
118119
'subscriptionPeriod':
119120
buildSubscriptionPeriodMap(product.subscriptionPeriod),
120121
'introductoryPrice': buildDiscountMap(product.introductoryPrice!),
122+
'discounts': [buildDiscountMap(product.introductoryPrice!)],
121123
};
122124
}
123125

0 commit comments

Comments
 (0)