-
Notifications
You must be signed in to change notification settings - Fork 9.8k
[in_app_purchase] Add support for promotional offers through Store-Kit wrappers #4458
Conversation
2659f4e
to
24b4f1e
Compare
24b4f1e
to
38c5dbc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the quick PR! I left some comments and questions.
@@ -61,6 +61,14 @@ - (instancetype)initWithMap:(NSDictionary *)map { | |||
[self setValue:map[@"subscriptionGroupIdentifier"] ?: [NSNull null] | |||
forKey:@"subscriptionGroupIdentifier"]; | |||
} | |||
if (@available(iOS 12.2, *)) { | |||
NSMutableArray *discounts = [NSMutableArray new]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nits: Do now use new per google objc style guide: https://google.github.io/styleguide/objcguide.html#do-not-use-new
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
@@ -14,6 +14,7 @@ @interface TranslatorTest : XCTestCase | |||
@property(strong, nonatomic) NSMutableDictionary *productMap; | |||
@property(strong, nonatomic) NSDictionary *productResponseMap; | |||
@property(strong, nonatomic) NSDictionary *paymentMap; | |||
@property(strong, nonatomic) NSDictionary *paymentDiscountMap; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NSDictionary should be copy: https://google.github.io/styleguide/objcguide.html#setters-copy-nsstrings
(I know some old codes use strong, which should also be updated to copy is possible, in a separate PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
@@ -47,6 +51,9 @@ NS_ASSUME_NONNULL_BEGIN | |||
andSKPaymentTransaction:(SKPaymentTransaction *)transaction | |||
API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); | |||
|
|||
// Creates an instance of the SKPaymentDiscount class based on the supplied disctionary. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nits: dictionary
// Creates an instance of the SKPaymentDiscount class based on the supplied disctionary. | |
// Creates an instance of the SKPaymentDiscount class based on the supplied dictionary. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
@@ -24,6 +24,7 @@ + (NSDictionary *)getMapFromSKProduct:(SKProduct *)product { | |||
// https://github.com/flutter/flutter/issues/26610 | |||
[map setObject:[FIAObjectTranslator getMapFromNSLocale:product.priceLocale] ?: [NSNull null] | |||
forKey:@"priceLocale"]; | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nits: revert
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
@@ -49,6 +54,17 @@ + (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPe | |||
return @{@"numberOfUnits" : @(period.numberOfUnits), @"unit" : @(period.unit)}; | |||
} | |||
|
|||
+ (nonnull NSArray *)getMapArrayFromSKProductDiscounts: | |||
(nonnull NSArray<SKProductDiscount *> *)productDiscounts { | |||
NSMutableArray *discountsMapArray = [NSMutableArray new]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use alloc] init];
instead of new
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
/// 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. The `map` parameter must not be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nits: "The map
parameter must not be null." is not necessary as it is a NNBD supported file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
|
||
/// A string identifying the key that is used to generate the signature. | ||
/// | ||
/// Keys are generated and downloaded from App Store Connect. See the "KEY ID" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a link to some official guide might help explain this better? I feel like this is hard to maintain. For example, if App Store Connect changed their UI, this has to be updated to match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
/// The unique nonce should be generated on your server when it creates the | ||
/// `signature` for the payment discount. The nonce can be used once, a new | ||
/// nonce should be created for each payment request. | ||
/// The string representation of the nonce must be lowercase. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an official guide on how to generate this from Apple, if so, we can add a link here too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes there is, I have added an extra link.
@@ -241,7 +241,8 @@ class SKProductWrapper { | |||
required this.price, | |||
this.subscriptionPeriod, | |||
this.introductoryPrice, | |||
}); | |||
List<SKProductDiscountWrapper>? discounts, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
List<SKProductDiscountWrapper>? discounts, | |
this.discounts = <SKProductDiscountWrapper>[], |
And remove the next line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember there's a way to indicate default value to be empty array in jsonSerializable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updating List<SKProductDiscountWrapper>? discounts,
to this.discounts = <SKProductDiscountWrapper>[],
will not work as the default value is not a constant. The following error is generated:
The default value of an optional parameter must be constant. dart(non_constant_default_value)
That is why I resolved it like this. The field is already configured to use an empty array as default value in jsonSerializable, however this is done on field level not in the constructor:
@JsonKey(defaultValue: <SKProductDiscountWrapper>[])
final List<SKProductDiscountWrapper> discounts;
I am not aware you can add additional attributes on constructor parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
now you've updated the filed to be none-optional, will this.discounts = const <SKProductDiscountWrapper>[],
not work? The parameter doesn't have to be optional, right?
/// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nits:
/// no promotional offers have been set up this field returns an empty list. | |
/// no promotional offers have been set up, this field returns an empty list. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
return nil; | ||
} | ||
|
||
NSString *identifier = map[@"identifier"]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what happens if the map
is empty, or some of the values are NSNull?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't added specific check for these cases because the fields are mandatory which is enforced on the Dart side. Since this is only called from Dart side I skipped doing additional tests.
If you feel it is better to do so I will expand the logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes let's add logic for these values, if any of the critical values is nil, we should return nil.
My rational is that if for some reason, the dart api changes so that these values are nullable, dart tests will get updated along with the change; however, there are no tests on the platform side to catch this.
We can also add an NSAssert
to make sure these values are non-null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! the asserts are for testings, for prod app, should we also prevent nil values?
@@ -241,7 +241,8 @@ class SKProductWrapper { | |||
required this.price, | |||
this.subscriptionPeriod, | |||
this.introductoryPrice, | |||
}); | |||
List<SKProductDiscountWrapper>? discounts, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
now you've updated the filed to be none-optional, will this.discounts = const <SKProductDiscountWrapper>[],
not work? The parameter doesn't have to be optional, right?
return nil; | ||
} | ||
|
||
NSString *identifier = map[@"identifier"]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes let's add logic for these values, if any of the critical values is nil, we should return nil.
My rational is that if for some reason, the dart api changes so that these values are nullable, dart tests will get updated along with the change; however, there are no tests on the platform side to catch this.
We can also add an NSAssert
to make sure these values are non-null.
@cyanglaz thanks for the review. I have added |
af8cf8c
to
ba6dfba
Compare
NSString *signature = map[@"signature"]; | ||
NSNumber *timestamp = map[@"timestamp"]; | ||
|
||
SKPaymentDiscount *discount = [[SKPaymentDiscount alloc] initWithIdentifier:identifier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what happens to the SKPaymentDiscount
object if any of the values are nil? Will it crash or throw some iap server error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the SKPaymentDiscount
object it self is nil
the payment will continue without discount. This is also documented as part of the Dart API.
The values of a SKPaymentDiscount
should never be nil
as this would indicate an invalid discount offer. Apples documentation is not very clear on this but it should result into a server error.
I have restructured the code and added additional validation to ensure empty and nil
values are detected and returns a PlatformError
in case invalid values are supplied. This to make it more clear to developers what went wrong.
P.S. I also removed the NSAssert lines since they are now obsolete.
|
||
NSString *identifier = map[@"identifier"]; | ||
NSString *keyIdentifier = map[@"keyIdentifier"]; | ||
NSUUID *nonce = [[NSUUID alloc] initWithUUIDString:map[@"nonce"]]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will initWithUUIDString
crash if map[@"nonce"] is nil?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to Apple's documentation the initWithUUIDString
method returns nil
for invalid strings. I have made a quick test and confirmed this also includes nil
values passed in.
I did restructure the code to improve error handling (see earlier comment).
return nil; | ||
} | ||
|
||
NSString *identifier = map[@"identifier"]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! the asserts are for testings, for prod app, should we also prevent nil values?
108cb53
to
1867ff4
Compare
Hi @cyanglaz, Last week I processed your feedback but forgot to request another review. Would you mind having another look? Many thanks in advance. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
…Store-Kit wrappers (flutter/plugins#4458)
This PR adds support for promotional offers on iOS by exposing the
SKProduct.discounts
(via theSKProductWrapper
Dart class) and theSKMutablePayment.paymentDiscount
(via theSKPaymentWrapper
Dart class) properties.Resolves flutter/flutter#92231
If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.
Pre-launch Checklist
dart format
.)[shared_preferences]
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.