When validating a receipt, what is the correct way to find the most recent transaction for an app with a single subscription?

I'm working on an app that uses subscriptions. It is relatively simple. There is a choice of a monthly or yearly subscription. They are part of the same subscription group, so only one subscription can be active at a time.

My app sends the receipt data to my server, which uses the verifyReceipt endpoint to validate the receipt. Aside from checking that the receipt is valid, I’m mainly just interested in one thing: what is the expiration date for the most recent purchase or renewal?

So far I’ve mainly worked in the sandbox. I have validated production receipts as well, but those do not contain in-app purchases or subscriptions since I have not yet released an app that includes these. As far as finding the latest purchase, I basically just check the first transaction in latest_receipt_info, which in my testing only ever contains one item.

Overall things seem to be working well. While a subscription is active, my app shows the upcoming expiration or renewal date, and I've never noticed any problems with that. After the subscription expires, the app shows a notice explaining when it expired. I've recently noticed this date is not accurate. When this happens, latest_receipt_info contains a single transaction that is not the most recent transaction. I’ve never seen this problem while a subscription is active.

Here are the questions I have right now:
  • Will latest_receipt_info actually contain multiple items for an app with a single subscription group? The sandbox only ever returns a one-item array for me. When a subscription is active, it seems to be accurate. When a subscription has expired, it seems to be an effectively random purchase—it’s different on every refresh.

  • If there may be multiple items in latest_receipt_info, are they in chronological order or is it important to check all of them to find the latest expiration date? The documentation for in_app makes it clear that the items will not be in chronological order, but the documentation for latest_receipt_info simply says “an array that contains all in-app purchase transactions” which from what I understand is not even accurate. (Elsewhere I’ve heard it’s the 100 most recent transactions, but again I’ve only ever seen one transaction here.)

  • In the sandbox I’ve found that the in_app array contains many transactions that are not included in latest_receipt_info. In some cases, the most recent transaction only appears in the in_app array. I’ve heard from others that latest_receipt_info sometimes contains recent transactions that are not in the in_app array. Do I need to check through all items in both arrays to find the latest expiration date? Or is it safe to trust that latest_receipt_info will contain the correct information in production? Previously my understanding was that I didn't have to worry about the in_app array for my app, but in the sandbox it seems to be the only way to get the latest expiration date.

For my first two questions it seems safest to assume that latest_receipt_info may contain multiple items that may not be in chronological order. That's an easy fix. But unless I'm looking at something wrong, it won't address the problem I've run into. So that last bullet point is my biggest question right now.

Bonus questions:
  • In the sandbox, server-to-server notifications don’t ever seem to include grace_period_expires_date_ms. I’m checking for that value in unified_receipt.pending_renewal_info[0]. grace_period_expires_date_ms. Is the correct place to look?

  • For an app with a single subscription group, would the pending_renewal_info array ever contain more than one item? I think I'm relatively safe assuming it would not. It's unclear how I would find the most relevant item if I'm wrong.

I should have mentioned that this is happening after using the "Restore Purchases" button in my app. This calls the restoreCompletedTransactions() method on the default SKPaymentQueue. I then wait for paymentQueueRestoreCompletedTransactionsFinished() and send the receipt to my server for validation. Afterward I go through the items in transactions where the transactionState is restored or purchased and call finishTransaction() for each one.

I did some testing, and it seems this is an important step in what's happening. If I validate the same receipt without restoring anything, I get consistent results. The transaction in latest_receipt_info is still not the most recent transaction—but it is the same transaction each time.

When I discovered this, I thought I might be able to solve the problem by sorting the transactions by date before calling finishTransaction(). This doesn't seem to make a difference. It's possible I'm overlooking something in my code but it seems like simply calling restoreCompletedTransactions() jumbles the transactions and results in a random one being considered the "latest" according to the verifyReceipt endpoint.

This post on Stack Overflow is the closest I've found to anyone discussing similar behavior. I hadn't noticed it previously but I'm seeing the same thing—restoreCompletedTransactions() does change the transaction IDs. Seems surprising to me, but it's not a problem. Then I noticed that original_purchase_date is also changing—it's the current date and time! The purchase_date is the actual date of the purchase. That's backwards from what I would expect. But this at least makes sense now: according to this date it is the latest transaction. It seems like it's re-running all the transactions in an effectively random order and updating the purchase date as it goes. This still doesn't make sense to me, but I at least feel like I understand what's happening.

The sole answer to that Stack Overflow post suggests using SKReceiptRefreshRequest instead of restoreCompletedTransactions(). My understanding was that these do not necessarily do the same thing, and restoreCompletedTransactions() should be done to make sure transactions appear on a new device, or after deleting and reinstalling the app. SKReceiptRefreshRequest is what you should use if the receipt file is invalid or missing. These seem like pretty distinct use cases that are backed up by the current documentation. There's also a reply here from someone at Apple suggesting the app won't pass app review if I rely on SKReceiptRefreshRequest for this.

I think at this point I'm going to:
  • Assume latest_receipt_info may contain more than one transaction in non-chronological order. I'll loop through each one and look for the latest expires_date_ms. This addresses my first two questions and seems like the safest approach.

  • Assume that latest_receipt_info may not include the most recent purchase and check receipt.in_app as well. It's extra work, but due to the way purchases are restored it seems necessary to show my customers when their subscription expired.

  • For my bonus questions I'm just going to assume I'm looking in the right place for the grace period and that pending_renewal_info should only contain a single item in my case. It shouldn't be hard to adjust these later as necessary.

I feel pretty comfortable with this now, but if anyone has relevant knowledge I'd love to hear it.
When validating a receipt, what is the correct way to find the most recent transaction for an app with a single subscription?
 
 
Q