17

This question is regarding Auto-Renewable IAPs and how they should be restored. These links: this and this have not helped me unfortunately.

In my app I have users subscribing to Auto-Renewable In-App Purchases. They can subscribe either 1, 6 or 12 months.

When they subscribe, the transaction receipt is sent to my server for later validation. I do not validate the receipt immediately since it would slow down the user experience (a receipt validation query to apples servers takes about 1 - 2 seconds for me). Instead, I use the naive approach and provide the content that the users subscribed to, without any direct receipt verification. I schedule a cron job to validate every user's receipt once a day and revokes privileges upon outdated receipts.

Now since apples guidelines clearly state that a restore functionality is required for applications with auto-renewable subscriptions, I have chosen to implement that.

When I try to restore the purchases in sandbox mode, using:

[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];

I obtain not only current subscriptions, but all previous subscriptions(including outdated ones) in the callback to:

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions

Currently I have tried my IAPs about 30 times, which means that the above method is sent 30 different transactions(outdated and active). For each of these transactions I upload the transactions receipt to my web service for later verification.

Now. Should it happen that the last transaction has an outdated receipt(but the second to last transaction was actually valid), it would overwrite the current(valid) receipt for the current user and thereby revoke the privileges for the user falsely.

Basically my problem is that when calling restoreCompletedTransactions I obtain a list of both outdated and active transactions. And on the server-side they might invalidate each other. Optimally, I would like to only retrieve one transaction(The most relevant) and have that receipt sent to my server for later validation.

All in all I guess my main question is:

How can I make sure that only an active(i.e. the most current) transaction is restored?

Community
  • 1
  • 1
Eyeball
  • 3,267
  • 2
  • 26
  • 50
  • Can you give us the JSON of your server-side receipt (since this seems to be a server-side issue)? Can you use the dates of the purchases within the receipt, on the server, to resolve this? Will the purchase with the most recent date be the purchase you want to deal with? – Chris Prince Mar 09 '14 at 18:28
  • Yeah but will the purchase with the most recent date always be the one that is currently active? Also, using my naive approach, there is nothing that prevents the users from restoring their Iaps once per day, how is this issue solved? Is my approach wrong? – Eyeball Mar 10 '14 at 05:29
  • I'm not really sure what active means. I haven't specifically worked with auto-renewables. Do auto-renewable receipts have a field that indicates "active"? One way to determine this though would be to take the original_purchase_date field of the receipt, and say you have a 1 month subscription, just add a month to that date, and see if that date has already passed. Does that make sense? – Chris Prince Mar 11 '14 at 05:45
  • Sure, but this solution is not elegant. Yeah renewable subscriptions have fields for expiration date, but only in the json response from apple(I can only see the expiration date after querying apple and validating the transactions receipt). I do not want to do this for all my , potentially outdated, receipts. I just want to make sure that the most recent receipt is uploaded to my web service for validation – Eyeball Mar 11 '14 at 10:05
  • I take it you are supporting not just iOS7? – Chris Prince Mar 12 '14 at 02:47
  • Exactly, down iOS 6.1 – Eyeball Mar 12 '14 at 05:53

3 Answers3

3

My solution: retrieve the receipt and validate it against your productIdentifiers. Using SKPaymentQueue.defaultQueue().restoreCompletedTransactions() for auto-renewable subscriptions does not make sense because:

  1. takes too long because it causes receipt validation to be called way too many times (once for each transaction in the past)
  2. may cause a valid transaction validation to be overwritten by a subsequent failed transaction

For example, if you have three durations for your auto-renewable subscription, just validate the receipt once against the three productIdentifiers associated with the three subscription durations.

RawMean
  • 8,374
  • 6
  • 55
  • 82
  • This solution makes the most sense. Have you managed to get your app past review? Apple seem to require users to be able to `SKPaymentQueue.defaultQueue().restoreCompletedTransactions()` for auto-renewable purchases. – ken Oct 03 '16 at 04:29
  • @ken Yes, the app passed review with no problem (https://appsto.re/i6hm2yX). Apple does require the capability to restore previous purchases. This answer describes how to restore previous purchases w/o using `restoreCompletedTransactions` – RawMean Oct 03 '16 at 17:01
  • Awesome! Just implemented my app this way and it works well! – ken Oct 04 '16 at 04:53
0

I believe you'll have to process the receipt and look at the "Original Purchase Date and Subscription Expiration Date" (https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/Subscriptions.html) of each purchase in the receipt to see if a particular purchase is still active. You can process the receipt either by using a server and verifying it with Apple to obtain JSON for the receipt. Or, if you are working with iOS7, you can verify the receipt on the device, and also obtain the JSON (e.g., see A complete solution to LOCALLY validate an in-app receipts and bundle receipts on iOS 7). If you are working with iOS7, you will have a single receipt with all of the purchases contained within it obtained with:

[[NSBundle mainBundle] appStoreReceiptURL]

Community
  • 1
  • 1
Chris Prince
  • 7,288
  • 2
  • 48
  • 66
  • Sure, what I have done now is to filter the receipts based on the original transaction date. I am always only restoring the latest one. But will the receipt with the latest transaction date always be the most relevant one? What if that receipt belongs to a failed transaction? This is my concern – Eyeball Mar 11 '14 at 10:01
  • Check my revised answer. Do you have reason to believe that Apple would give you a receipt for a failed transaction? – Chris Prince Mar 12 '14 at 02:46
  • maybe bot a failed one, but restored- and expired. I do not want front-end validation. The validation is performed in the back-end system – Eyeball Mar 12 '14 at 05:56
0

By using uniqueID by using KeyChains we can store receipt.

-(NSString*)checkUniqueIDInKeyChains {
    NSString *uniqueID = [apDelegate.keyChain objectForKey:(__bridge id)kSecValueData];
    return uniqueID;
 }

 -(void)saveUniqIDinKeyChain:(NSString*)uniqueID {
     [apDelegate.keyChain setObject:uniqueID forKey:(__bridge id)kSecValueData];
 }

-(NSString *)generateUUID {
    //Check for the UDID in the keychain , if not present create else take it from keychain.
    CFUUIDRef theUUID = CFUUIDCreate(NULL);
    CFStringRef string = CFUUIDCreateString(NULL, theUUID);
    CFRelease(theUUID);
    return (__bridge NSString *)string;
 }

-(NSDateFormatter*)getDateFormatter {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];
    [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]];
    [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    return dateFormatter;
}

Purchase the product by verifying the receipt.

-(BOOL)isPurchaseExpired:(NSDate*)expireDate {
    NSDateFormatter *dateFormatter = [self getDateFormatter];
    NSString *expireDateString = [[NSUserDefaults standardUserDefaults] objectForKey:@"purchaseExpireDate"];
    expireDate = [dateFormatter dateFromString:[expireDateString substringToIndex:18]];
    NSComparisonResult result = [expireDate compare:[dateFormatter dateFromString:      [dateFormatter stringFromDate:[NSDate date]]]];

    NSLog(@"\n %@ \n %@ ", expireDate, [dateFormatter dateFromString:[dateFormatter stringFromDate:[NSDate date]]]);
    if (result ==  NSOrderedAscending) {
        NSLog(@"Current Date is Greater than the Purchased, allowing user to access the content");
        return YES;
    }
    else if (result == NSOrderedDescending) {
        NSLog(@"Current date is Smaller than the Purchase Date");
        return NO;
    }
    else {
        NSLog(@"Current and Purchase Dates are Equal , allowing user to access the content");
        return YES;
    }
}
user3666197
  • 1
  • 6
  • 50
  • 92
Lova
  • 134
  • 7