Skip to content

Instantly share code, notes, and snippets.

@BigZaphod
Last active October 4, 2021 18:15
Show Gist options
  • Save BigZaphod/571d5bf618e3400f1259751b86e10024 to your computer and use it in GitHub Desktop.
Save BigZaphod/571d5bf618e3400f1259751b86e10024 to your computer and use it in GitHub Desktop.
-(BOOL)containsActiveAutoRenewableSubscriptionOfProductIdentifier:(NSString *)productIdentifier forDate:(NSDate *)date
{
// IMPORTANT NOTE!
//
// I, Sean, have modified this from the original algo that came with this library.
//
// The original implementation would get the newest purchase record by sorting based on the subscriptionExpirationDate.
// Then it would check /only/ that one newest purchase record to see if the date was within the purchase record's range.
//
// This was VERY WRONG because it made multiple bad assumptions - one is that it assumed the incoming date was always
// meant to be "now" or "current" or something. What if you were testing a past date? It would always fail in that case.
//
// Another issue here is that the date being used as the purchase start date was the record's "purchaseDate" which is set
// to the date that the rewnewal happened. This would normally seem fine, but the incorrect assumption was that the
// receipt file would only contain purchase records from the past. In fact Apple will deliver purchase records for the
// FUTURE in the case of an autorewnewing subscription! (Likely this is to attempt to avoid payment processing delays.)
//
// So what happened is, the receipt file would end up with a purchase record for the future in it. That record would be
// the winning record during the first pass sort. The current date would fall outside of the start/end range for the
// record because that record didn't have a start date until sometime in the future. End result is the app thinks it is
// not subscribed and customer gets pissed off. After a few hours, things would clear up as real time catches up to the
// next period's purchase record.
//
// My fix here is that rather than picking one winning purchase record and using it, I instead just check all relevant
// records. This means that the incoming test date could be in the past, too, and it'd still return a correct result
// for the date for as long as the records exist in the receipt file. This means that in the scenerios mentioned above,
// the record that is used for validation might be the slightly older record that IS NOT YET EXPIRED even if there's a
// newer record that is meant to take effect at some point several hours in the future.
//
// Alternatively, I could possibly have fixed this by simply checking the originalPurchaseDate instead of the record's
// purchaseDate. If it turns out this still somehow ends up with date gaps that trigger problems for customers,
// using the originalPurchaseDate is likely to be the next thing to try here - but I prefer the current approach.
//
// This is something we fought with for a long time while missing that the bug was here all along - because I trusted
// the logic of this code while making the same assumptions the original author did.
for (RMAppReceiptIAP *iap in self.inAppPurchases)
{
// skip if it's not the correct product
if (![iap.productIdentifier isEqual:productIdentifier]) continue;
// skip if it is not an auto renewing subscription
if (!iap.subscriptionExpirationDate) continue;
// the end date of a subscription is either the expiration date or the cancellation date if it was forced cancelled by Apple
NSDate *endDate = iap.cancellationDate ?: iap.subscriptionExpirationDate;
// NOTE - this should be safe and it allows for far easier reasoning of the date logic below, IMO, than using the dumb -compare: stuff.
const NSTimeInterval startTime = iap.purchaseDate.timeIntervalSinceReferenceDate;
const NSTimeInterval endTime = endDate.timeIntervalSinceReferenceDate;
const NSTimeInterval dateTime = date.timeIntervalSinceReferenceDate;
// if the given date is in range, then this is an active and valid subscription
if (dateTime >= startTime && dateTime <= endTime) {
return YES;
}
}
return NO;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment