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

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

Merged
merged 12 commits into from
Nov 15, 2021

Conversation

mvanbeusekom
Copy link
Contributor

@mvanbeusekom mvanbeusekom commented Oct 29, 2021

This PR adds support for promotional offers on iOS by exposing the SKProduct.discounts (via the SKProductWrapper Dart class) and the SKMutablePayment.paymentDiscount (via the SKPaymentWrapper 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

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the relevant style guides and ran the auto-formatter. (Note that unlike the flutter/flutter repo, the flutter/plugins repo does use dart format.)
  • I signed the CLA.
  • The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. [shared_preferences]
  • I listed at least one issue that this PR fixes in the description above.
  • I updated pubspec.yaml with an appropriate new version according to the pub versioning philosophy.
  • I updated CHANGELOG.md to add a description of the change.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Copy link
Contributor

@cyanglaz cyanglaz left a 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];
Copy link
Contributor

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

Copy link
Contributor Author

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;
Copy link
Contributor

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)

Copy link
Contributor Author

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits: dictionary

Suggested change
// Creates an instance of the SKPaymentDiscount class based on the supplied disctionary.
// Creates an instance of the SKPaymentDiscount class based on the supplied dictionary.

Copy link
Contributor Author

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"];

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits: revert

Copy link
Contributor Author

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];
Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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"
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 540 to 543
/// 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.
Copy link
Contributor

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.

Copy link
Contributor Author

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
List<SKProductDiscountWrapper>? discounts,
this.discounts = <SKProductDiscountWrapper>[],

And remove the next line.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits:

Suggested change
/// 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@mvanbeusekom mvanbeusekom requested a review from cyanglaz November 2, 2021 10:56
@mvanbeusekom mvanbeusekom changed the title [webview_flutter] Add support for promotional offers through Store-Kit wrappers [in_app_purchase] Add support for promotional offers through Store-Kit wrappers Nov 3, 2021
return nil;
}

NSString *identifier = map[@"identifier"];
Copy link
Contributor

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?

Copy link
Contributor Author

@mvanbeusekom mvanbeusekom Nov 3, 2021

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.

Copy link
Contributor

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.

Copy link
Contributor

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,
Copy link
Contributor

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?

@mvanbeusekom mvanbeusekom requested a review from cyanglaz November 4, 2021 10:42
return nil;
}

NSString *identifier = map[@"identifier"];
Copy link
Contributor

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.

@mvanbeusekom mvanbeusekom requested a review from cyanglaz November 4, 2021 16:34
@mvanbeusekom
Copy link
Contributor Author

@cyanglaz thanks for the review. I have added NSAssert statements to validate if all required paymentDiscount fields are supplied. I have also added additional unit tests to ensure the asserts are there (so somebody removing them will be warned).

NSString *signature = map[@"signature"];
NSNumber *timestamp = map[@"timestamp"];

SKPaymentDiscount *discount = [[SKPaymentDiscount alloc] initWithIdentifier:identifier
Copy link
Contributor

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?

Copy link
Contributor Author

@mvanbeusekom mvanbeusekom Nov 8, 2021

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"]];
Copy link
Contributor

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?

Copy link
Contributor Author

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"];
Copy link
Contributor

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?

@mvanbeusekom mvanbeusekom requested a review from cyanglaz November 8, 2021 20:34
@mvanbeusekom
Copy link
Contributor Author

Hi @cyanglaz,

Last week I processed your feedback but forgot to request another review. Would you mind having another look? Many thanks in advance.

Copy link
Contributor

@cyanglaz cyanglaz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@cyanglaz cyanglaz added the waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land. label Nov 15, 2021
@fluttergithubbot fluttergithubbot merged commit 45f87b9 into flutter:master Nov 15, 2021
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Nov 16, 2021
@mvanbeusekom mvanbeusekom deleted the issue/92231 branch November 17, 2021 14:00
amantoux pushed a commit to amantoux/plugins that referenced this pull request Dec 11, 2021
KyleFin pushed a commit to KyleFin/plugins that referenced this pull request Dec 21, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes p: in_app_purchase platform-ios waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implementation of iOS Promotional Offers
3 participants