Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forces refreshing the receipt when calling restore #287

Merged
merged 11 commits into from
Jul 31, 2020

Conversation

vegaro
Copy link
Contributor

@vegaro vegaro commented Jul 17, 2020

If the receipt does not contain any subscription purchases, calling /verifyReceipt will not update an empty receipt with any purchased subscriptions.

We should trigger an SKReceiptRefresh if the device receipt does not contain any subscription purchases.

Things missing

  • I have tested this but we really need to make sure it will not trigger login popups constantly. Maybe worth releasing a beta?
  • Unit tests
  • Test with Mac

@vegaro vegaro marked this pull request as draft July 17, 2020 22:31
- (void)receiptData:(void (^ _Nonnull)(NSData * _Nonnull data))completion
{
- (void)receiptData:(void (^ _Nonnull)(NSData * _Nonnull data))completion {
[self receiptDataWithForceRefresh:false completion:completion];
Copy link
Contributor

Choose a reason for hiding this comment

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

false -> NO

[self receiptDataWithForceRefresh:false completion:completion];
}

- (void)receiptDataWithForceRefresh:(BOOL)forceRefresh completion:(void (^ _Nonnull)(NSData * _Nonnull data))completion {
Copy link
Contributor

Choose a reason for hiding this comment

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

Chance for cleanup, this should probably have a typedef:

(void (^ _Nonnull)(NSData * _Nonnull data))

[self receiptDataWithForceRefresh:false completion:completion];
}

- (void)receiptDataWithForceRefresh:(BOOL)forceRefresh completion:(void (^ _Nonnull)(NSData * _Nonnull data))completion {
NSData *receiptData = [self.receiptFetcher receiptData];
if (receiptData == nil) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should a zero-bytes check here too. I've seen that before, where there is a file, but it's empty.

Copy link
Contributor

Choose a reason for hiding this comment

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

Possible there is a check in the fetcher.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have the check in a couple of places in the RCPurchases.m:
https://github.com/RevenueCat/purchases-ios/blob/cesardelavega/ch68/force-refresh-receipt/Purchases/Public/RCPurchases.m#L629
https://github.com/RevenueCat/purchases-ios/blob/cesardelavega/ch68/force-refresh-receipt/Purchases/Public/RCPurchases.m#L1084

I will add it a check when we fetch so we also log a message in that case and also here so that we also refresh if the receipt has 0 bytes.

/**
* Completion block for calls that send back receipt data
*/
typedef void (^RCReceiveReceiptDataBlock)(NSData *);
Copy link
Member

Choose a reason for hiding this comment

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

does this need to be public? if not, you can declare it in the interface within the .m

Comment on lines +913 to +917
- (void)receiptData:(RCReceiveReceiptDataBlock)completion {
[self receiptDataWithForceRefresh:NO completion:completion];
}

- (void)receiptDataWithForceRefresh:(BOOL)forceRefresh completion:(RCReceiveReceiptDataBlock)completion {
Copy link
Member

Choose a reason for hiding this comment

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

[not for this PR] we really should move all of this logic out of RCPurchases.

NSData *receiptData = [self.receiptFetcher receiptData];
if (receiptData == nil) {
if (receiptData == nil || receiptData.length == 0) {
Copy link
Member

Choose a reason for hiding this comment

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

I can't really think of a scenario where we would want to treat an empty receipt differently than a missing one. It might be a nice simplification to have the fetcher return nil if the data is empty. or even better, some form of an error.
Again, not for this 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.

yeah, I thought the same, right now the fetcher makes the data empty when the receipt is nil. And then there's some error handling in RCPurchases.m (like in the restore function) that checks if the data is empty instead of nil and then sends an error. I thought about moving the error to the fetcher, but it was going to make this PR bigger. I totally agree. I will add it to my list of things to refactor

Copy link
Member

Choose a reason for hiding this comment

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

I might tackle this as a part of local receipt validation, since I may have to do some new receipt fetching logic.

Comment on lines 920 to 925
RCDebugLog(@"Receipt empty, fetching");
[self refreshReceipt:completion];
} else if (forceRefresh) {
RCDebugLog(@"Forced receipt refresh");
[self refreshReceipt:completion];
} else {
Copy link
Member

Choose a reason for hiding this comment

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

if we're going to refresh the receipt anyway, this should be the first step, that way we don't make unnecessary calls to read the receipt from disk.

as in:

if (forceRefresh) { 
    [self refreshReceipt:completion];
    return;
}

// current logic here ...
NSData *receiptData = [self.receiptFetcher receiptData];
    if (receiptData == nil || receiptData.length == 0) {
    ...

@vegaro vegaro requested a review from aboedo July 21, 2020 21:19
Copy link
Member

@aboedo aboedo left a comment

Choose a reason for hiding this comment

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

looks great! I'll mark approved once the tests are in.

Purchases/Public/RCPurchases.m Show resolved Hide resolved
@aboedo
Copy link
Member

aboedo commented Jul 23, 2020

marking approved for now, but feel free to ask for another review when tests are in

@vegaro vegaro marked this pull request as ready for review July 24, 2020 18:38
@vegaro vegaro changed the title [HOLD] Forces refreshing the receipt when calling restore Forces refreshing the receipt when calling restore Jul 24, 2020
@vegaro vegaro requested a review from aboedo July 24, 2020 21:29
Copy link
Member

@aboedo aboedo left a comment

Choose a reason for hiding this comment

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

looks great, left a question about a comment

expect(self.requestFetcher.refreshReceiptCalled).to(beFalse())
expect(self.requestFetcher.refreshReceiptCalled).to(beTrue())
Copy link
Member

Choose a reason for hiding this comment

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

lol

Comment on lines 1562 to 1563
// Second one issues an error
self.purchases?.purchaseProduct(product) { (tx, info, error, userCancelled) in }
Copy link
Member

Choose a reason for hiding this comment

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

what does the comment mean?
also, we should make sure that this doesn't no-op if purchases is nil, right?
lastly, no need for explicit self, or even to name the variables in the block, so this is equivalent to

guard let purchases = purchases else { fatalError() }
purchases.purchaseProduct(product) { }

Copy link
Member

Choose a reason for hiding this comment

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

however, if the transaction should be made after the product is purchased, then maybe the best way to do this would be to move the creation of the transaction and the storeKitWrapperDelegate call to inside the completion of purchaseProduct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The thing is that the purchaseProduct call will add the completion block to the array of completion blocks and we need to call self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) in order to trigger the completion block of purchaseProduct.

In this case we don't care about the completion block of purchaseProduct, but we need to call it because it adds the payment to the "queue".

The weird comment is a copy paste issue

@vegaro
Copy link
Contributor Author

vegaro commented Jul 28, 2020

It looks like macOS will consider the login details stale after 30 minutes and trigger the login popup. This means that if someone is calling restore every app start, there will be an annoying popup every app start. I think we are fine since we state that people shouldn't be calling restore on app start in the docs, but I wanted to make sure everyone is in the loop @aboedo @jeiting @rkotzy

EDIT: Actually, I just tested on my iPad with iOS 13 and same result, the login details cache only lasts for like 15-20 mins on my tests!

@vegaro vegaro merged commit 4c89d35 into develop Jul 31, 2020
@vegaro vegaro deleted the cesardelavega/ch68/force-refresh-receipt branch July 31, 2020 01:18
@vegaro vegaro mentioned this pull request Aug 4, 2020
@aboedo aboedo mentioned this pull request Aug 27, 2020
@aboedo aboedo mentioned this pull request Sep 10, 2020
@aboedo aboedo mentioned this pull request Jul 13, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants