-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[in_app_purchase][iOS] fix iOS promotional offers (SKPaymentDiscountWrapper was not used properly) #6541
[in_app_purchase][iOS] fix iOS promotional offers (SKPaymentDiscountWrapper was not used properly) #6541
Conversation
| } | ||
|
|
||
| if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp intValue] <= 0) { | ||
| if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp longLongValue] <= 0) { |
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.
Nice catch. Can we add an XCUnitTest for this? Maybe have the timestamp to be a very big long long number?
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. I've added XCUnitTest using current timestamp number value. It's enough to cause the intValue overflow in negative number. If I use really big long long number, the intValue overflow goes positive again and passes the test. I'm not sure of the usefulness of this "not negative int" check in the original code.
| 'quantity': quantity, | ||
| 'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox | ||
| 'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox, | ||
| 'paymentDiscount': paymentDiscount?.toMap(), |
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.
We should have some test for the toMap method, can we update the test to include the discount? If we don't have an existing test, do you mind adding a test for the toMap method?
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. I've added a new test checking the values of the SKPaymentWrapper object and the result of the toMap method explicitly.
| simulatesAskToBuyInSandbox: purchaseParam is AppStorePurchaseParam && | ||
| purchaseParam.simulatesAskToBuyInSandbox)); | ||
| purchaseParam.simulatesAskToBuyInSandbox, | ||
| paymentDiscount: purchaseParam is AppStorePurchaseParam |
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.
We should test that the paymentDiscount is encoded and sent to method channel.
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 see your point, that in fact the bug was that the discount parameter was not sent trough the method channel. But I am not sure on how to construct such a test?
In my understanding: now the discount is send trough AppStorePurchaseParam in the buyNonConsumable method, which is not returning a useful result to test. This method constructs SKPaymentWrapper with a discount parameter and sends it to _skPaymentQueueWrapper.addPayment which turns the SKPaymentWrapper to a map (which we test in the newly created test for the toMap method) and sends it trough the method channel. But the channel is not returning useful result for testing either. The encoding of the paymentDiscount is in the toMap method which we test now.
Can you elaborate please?
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.
You might be able to intercept the method call in the FakeStoreKitPlatform and inspect the value?
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 got your idea. Thanks! Done in the last commit.
…r SKPaymentWrapper.toMap()
packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m
Show resolved
Hide resolved
|
@cyanglaz I don't understand while the last commit failed at the |
"submit-queue" is not related to this PR. It is an indication of the current tree's status. This CI is intended to block merging when tree is red (when the build is failing on master). Usually, if there is a "submit-queue" error, someone on the flutter team is working on a fix, the best action is to wait. |
cyanglaz
left a comment
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 mod nits, @stuartmorgan could you do a secondary review?
| /// Quantity of the product user requested to buy. | ||
| final int quantity; | ||
|
|
||
| /// Discount applied to the product (optional). |
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 "optional" keyword is probably not necessary as the nullability of this parameter already indicates it is optional.
However, it might be better to add an explanation what does it mean to be null, something like:
///
/// The value is `null` when the product does not have a discount.
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. Changed in the last commit.
| PlatformException? restoreException; | ||
| SKError? testRestoredError; | ||
| bool queueIsActive = false; | ||
| Map<String, dynamic>? discountReceived; |
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: Alternatively, we can make it non-nullable and initialized it to empty map, this can avoid many unwrapping.
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, collections should only be nullable if null and empty are conceptually different.
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. Changed in the last commit.
| PlatformException? restoreException; | ||
| SKError? testRestoredError; | ||
| bool queueIsActive = false; | ||
| Map<String, dynamic>? discountReceived; |
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, collections should only be nullable if null and empty are conceptually different.
| @@ -1,3 +1,7 @@ | |||
| ## 0.3.3 | |||
|
|
|||
| * Fixes `SKProductDiscountWrapper` with being sent trough the method channel as the expected parameter. | |||
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.
Typo: through
But higher level, the changelog should describe the issue from the client perspective, not the implementation details of the bug.
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.
Changed in the last commit.
| + (SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map | ||
| withError:(NSString **)error { | ||
| if (!map || map.count <= 0) { | ||
| if (!map || [map isKindOfClass:[NSNull class]] || map.count <= 0) { |
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.
This code is checking for a type violation; NSNull is not a NSDictionary, so if this happens the caller has made an invalid call. If you have a path where this is happening, it needs to be fixed at the original argument extraction point, not here.
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.
The original code never transferred through the method channel the discount parameter. So this check was stopped at the !map check, never trying the map.count <= 0 check and directly returning nil afterwards.
What happened when I transferred the discount key through the channel was that, when I was buying a product without discount, it was NULL but it did not stop at the !map it went to the next check map.count <= 0 and crashed.
So here I do not try to check for a type violation, but I am shielding the crashing check that comes next.
As for if it should be done here or in the caller. Why it is better in the caller?
Shouldn't the method take care of the argument's validation, as it does in the checks that come below?
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.
when I was buying a product without discount, it was
NULL
You mean it was NSNull, presumably, given the code you added. NULL is a typedef for 0, and is essentially the C equivalent of nil. NSNull is not nil/NULL, it is singleton class.
but it did not stop at the
!map
That is expected; a pointer that is pointing to an NSNull instance is not nil.
it went to the next check
map.count <= 0and crashed.
Presumably the crash was an unrecognized selector sent to instance exception, because count is not a method of NSNull.
So here I do not try to check for a type violation
Yes, that is exactly what you are checking for. You are taking a pointer of type NSDictionary and checking whether it contains an NSNull instance instead of an NSDictionary instance. Which would be a type violation.
As for if it should be done here or in the caller. Why it is better in the caller?
This method takes an NSDictionary*, not an id. It is better in the caller because it is a type violation to pass an argument to this method that is not an NSDictionary, and an instance of NSNull is not an NSDictionary.
The comparable Dart code is:
class NSNull {
}
void methodHandler(dynamic args) {
expectsAMap(args);
}
void expectsAMap(Map? map) {
if (map != null) {
print(map.length);
}
}
void main() {
NSNull nullArg = NSNull();
methodHandler(nullArg);
}
The bug is this Dart code is methodHandler passing args to expectsAMap without checking whether it's a Map, not when expectsAMap uses map as if it were its declared type.
Shouldn't the method take care of the argument's validation, as it does in the checks that come below?
Methods should not have to validate that they haven't been called with types that violate their declarations. Other validation (like whether it's nil), sure.
The code below this is also wrong; values with unknown types should be type checked before assigning to a variable declared as a specific type, not after. Either the values can only be NSStrings, in which case the checks are pointless, or (more likely) they could be NSStrings or NSNulls, in which case the variables should be declared id, not NSString*.
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 see your point. Thanks for the detailed explanation. Much appreciated! I've pushed a commit where I move the check out of there and into it's caller.
| 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 |
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.
This is a bugfix, so the version should be 0.3.2+3.
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.
This adds a new field to the AppStorePurchaseParam so it should be a new feature. The PR description or CHANGELOG should probably be something like "Supports adding discount information to AppStorePurchaseParam", instead of "fixing".
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 correction there; I didn't look closely enough at the details and thought it was just populating something that was previously added but not wired up.
| final String id = call.arguments['productIdentifier'] as String; | ||
| final int quantity = call.arguments['quantity'] as int; | ||
|
|
||
| // in case of testing paymentDiscount |
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.
Comments should be full grammatically correct sentences, with capitalization and punctuation.
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.
Changed in the last commit
| call.arguments['paymentDiscount']; | ||
| discountReceived = <String, dynamic>{}; | ||
| // can't cast directly the argument to Map<String,dynamic>, will receive: | ||
| // PlatformException(error, type '_InternalLinkedHashMap<Object?, Object?>' is not a subtype of type 'Map<String?, dynamic>' in type cast, null, 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.
This comment sounds like you attempted to cast with as instead of cast<...>.
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.
True. Changed it to a cast<> now. Thanks!
packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
Outdated
Show resolved
Hide resolved
0b58c4f to
badbd1d
Compare
|
@cyanglaz / @stuartmorgan should I squash all the commits before you accept the changes and decide to merge? |
That's not necessary, GitHub will squash when it merges. |
stuartmorgan-g
left a comment
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.
Looks good overall, just one more type issue.
| SKPaymentDiscount *paymentDiscount = [FIAObjectTranslator | ||
| getSKPaymentDiscountFromMap:[paymentMap objectForKey:@"paymentDiscount"] | ||
| withError:&error]; | ||
| NSDictionary *paymentDiscountMap = [paymentMap objectForKey:@"paymentDiscount"]; |
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.
This is still a type violation, it's just more local. If the right hand side of this assignment is NSNull, then it is incorrect to assign it to an NSDictionary*.
Either this should be an id, or you should use a helper like https://github.com/flutter/plugins/blob/main/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift#L198-L201 (written as ObjC of course) to do the extract/check/convert/assign in a single line here.
Doing the latter would allow you to remove the extra conditional you had to add at this layer since the called code already handles nil correctly.
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.
(@cyanglaz Any chance you could plan to convert this plugin to Pigeon sometime in the nearish term, like early 2023? It seems like there's a fair amount of this kind of problem in the plugin, and we could eliminate all of it with a Pigeon conversion.)
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. I did it with ObjC version of the helper that you've shown, one line without the condition check. Thanks! :)
stuartmorgan-g
left a comment
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, thanks for the iteration on the type issues!
…tDiscountWrapper was not used properly) (flutter/plugins#6541)
…rapper was not used properly) (flutter#6541)
…rapper was not used properly) (flutter#6541)
Make iOS Promotional Offers in in_app_purchase plugin usable.
SKPaymentDiscountWrapperwas not being sent trough the method channel.This is an update to an old (already closed) PR here
Resolves: [in-app-purchase][iOS] Promotional Offers in iOS not working
Pre-launch Checklist
dart format.)[shared_preferences]pubspec.yamlwith an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.CHANGELOG.mdto add a description of the change, following repository CHANGELOG style.///).If you need help, consider asking for advice on the #hackers-new channel on Discord.