Receiving transaction that is not present in the receipt (auto-renewing subscriptions)

Hi,


We're currently experiencing a problem where our client is receiving a transaction ID that does not exist in the receipt that it received.


One case where this is easily reproduced is by attempting to purchase an auto-renewing subscription that the account is already subscribed to. The client displays the 'You are already subscribed' dialog from the OS, and immediately receives a SKPaymentTransactionStatePurchased from the IAP delegate. Our client then submits both the transaction ID and receipt from this to our server for verification. To verify, we find the specified transaction in the receipt as per the normal server verification process, except in this case, the transaction ID doesn't exist in the 'in-app' section of the receipt (or anywhere else that I can find).


What does this transaction mean and what are we expected to do with it? Is there some way for the client to distinguish this from an actual purchase transaction and ignore it?


This is similar to an existing post here from some time ago, which has no apparent resolution: forums.developer.apple.com/message/228066#228066


Note that this is in the sandbox environment - no idea if this also happens live since we haven't gone live yet.


We're also seeing this problem in some cases where an 'upgrade' to another SKU on the same subscription is processed, but we're currently unable to reproduce that reliably. The 'already subscribed' problem happens every time.


Thanks,


-- Brett

The transaction_ID field in the IAP section of the receipt indicates actual purchases that were made. The transactionID elsewhere may include a restoreCompletedTransaction or a repurchase of something that was already purchased. While that transaction itself is not a purchase, it is still a transaction and has its own transactionID. Buried in that receipt will be a transaction_ID field that reflects the original purchase.

Yes, I'm familiar with restored transactions, and we handle those... typically there will be a new transaction and looking up that transaction in the receipt will turn up an entry with an original_transaction_id that indicates which transaction is being restored.


In this case the provided transaction ID just doesn't exist in the receipt at all whatsoever, so I can't look up and see what original transaction it's attempting to restore.

>provided transaction ID


I assume you mean transaction.transactionID returned in updatedTransactions.


What do you want to do with this anyway? All purchased transactions are indicated in the receipt itself.


Regarding security issues: If you use a trusted method of transferring the receipt from the app to your server you avoid man-in-the-middle attacks and the Apple servers check to be sure the receipt is correctly signed. I am not aware of a hack that can load a duplicate receipt into [[NSBundle mainBundle] appStoreReceiptURL] but if that is your concern you need to verify on the device itself in - it uses the identifierForVendor as part of the signature.

The 'provided transaction ID' is from SKPaymentTransaction.transactionIdentifier. The IAP observer receives this object, and we load the receipt in order to verify the transaction is valid and to handle rewards, etc for that particular transaction.


What we want to do with it is distinguish these non-purchase 'you are already subscribed' transactions from actual purchases so that we can ignore it, preferably on the client side.


In addition I just don't really get why we're getting notifications for transactions that don't exist in the receipt, so I'd like to understand why that is. We're also seeing this in some other cases where there is a legit purchase and the transaction doesn't match anything in the receipt, although we're still trying to nail down when this is happening.

> What we want to do with it is distinguish these non-purchase 'you are already subscribed' transactions from actual purchases so that we can ignore it, preferably on the client side


Your problem:

"our client is receiving a transaction ID that does not exist in the receipt that it received"

becomes your solution:

ignore it.


That leaves you with "on the client side". There is no way to differentiate this 'repurchase for free' from a 'purchase' without decoding the receipt. You can do that in the app using OpenSSL - but that's a bit of a coding challenge. Another way is to send the receipt to the Apple servers directly from the app withhout invoilving your server. That is not secure (man-in-the-middle attacks) but it can be used to identify 'repurchase for free' events.

You're saying that we can figure this out server side by decoding the receipt, but the problem is we can't figure out what these are even with the decoded receipt because the transaction that we've been notified about is not in the receipt.


Simply ignoring all transactions that don't exist in the receipt seems like a very error-prone solution. Certainly we can (and must) fail them as invalid purchases, but I don't understand why we're even getting them in the first place or what we're expected to do with them.

They are not necessarily invalid. They are what you know they are - a repurchase (or restore) of a subscription that was already purchased by that user. Actually, they are a repurchase (or restore) of the subscription on the user's device A of a subscruption that was previously purchased on device B. Focus on the receipt. If the receipt indicates the user has a valid subscription then give it to them.


The app also knows whether the call to updatedTransactions that generated your confusing transaction.transactionID is a 'purchase' transaction (i.e. a repurchase for free) or a 'restored' transaction (i.e. restoreCompletedTransactions).


If your concern is whether or not you are being scammed then use on board decoding using OpenSSL. Because it uses identifierForVendor as part of the signature block, it is the only secure way of verifying receipts, IMHO.

I know what it is in this case because I'm sitting there at the device causing it to happen, but in general, no we don't know what's happening if we receive an update for an unknown transaction. We're being told something was updated, but actually nothing was updated.


"The app also knows whether the call to updatedTransactions that generated your confusing transaction.transactionID is a 'purchase' transaction (i.e. a repurchase for free) or a 'restored' transaction (i.e. restoreCompletedTransactions)."


Ok, so we know this is a 'purchase' not a 'restored' transaction, because it's coming from updatedTransactions, not restoreCompletedTransactions. I'm not sure what a 'repurchase for free' is... can you clarify that? Regardless, it should have a corresponding receipt entry, no?


We're not concerned with getting scammed per-se, I'm just trying to implement correct handling for the subscriptions IAP flow. We're getting this update callback with an invalid transaction ID and have no way to handle it. Frankly, it seems like a bug in the IAP flow. I'd just like to know if this is expected to happen and what we're supposed to do with it (and whether this is just a sandbox thing that will go away or behave differently in the live environment). If the correct way of handling this update is to just ignore it and do nothing (as I believe you're suggesting), then why does it even exist?

let's unpack this....


1) Ok, so we know this is a 'purchase' not a 'restored' transaction, because it's coming from updatedTransactions, not restoreCompletedTransactions.


Not quite correct; if StoreKit is responding to a restoreCompletedTransactions it calls updatedTransactions with

transaction.transactionState==SKPaymentTransactionStateRestored.



I'm not sure what a 'repurchase for free' is... can you clarify that?


If the user has made a purchase on that device or some other device and they purchase the same IAP again then they are told something like 'you already purchased this, do you want to download it again for free?' If they say 'yes' then they get a call to updatedTransactions with SKPaymentTransactionStatePurchased just like the original purchase but they do not get charged for it. You can detect that by noting that the transaction.transactionIdentifier value is not represented in the receipt.


....it should have a corresponding receipt entry, no?


Correct - "no". Because there was no additional purchase. If I buy a book, leave the store, walk back in and ask if they can gift wrap it for free I do not get a second receipt for the gift wrapping because I didn't pay anything for that second 'purchase'. And as I walk out if the guard says "do you have a receipt for that" I show him, the receipt from my first purchase, not the second store entry.


We're getting this update callback with an invalid transaction ID


It's not invalid.


and have no way to handle it.....what we're supposed to do with it..... the correct way of handling this update is to just ignore it and do nothing


Correct! That is, satisfy the purchases represented on the receipt and then do nothing more. Ignore that transaction.transactionidentifier. Note that in case #1 and #2 below you are not ignoring the receipt that is presented by the user who made an earlier purchase.


then why does it (the update) even exist?


For three reasons.

1) user has 2 devices. purchase made on device 1. user taps 'purchase' on device 2 rather than 'restore' on device 2. Then Apple had a choice - they could say 'hey you jerk, tap restore not purchase' or they could say 'ok, purchased for free'. They chose the later.

2) user makes purchase. app crashes or is deleted and reinstalled. App fails to credit user with purchase. user taps 'purchase' rather than 'restore'. See #1 above.

3) user has initiated an interaction with the store and the app is awaiting a call back to updatedTransactions. There must be a call back to updatedTransactions with some state. You are arguing there should be another state called SKPaymentTransactionStatePurchasedAgain. Apple doesn't see the need for that since SKPaymentTransactionStatePurchased works for #1 above quite well.

> ....it should have a corresponding receipt entry, no?


> Correct - "no". Because there was no additional purchase.


The key word here is corresponding. I think the fact that you're missing here is that the transaction ID is a new transaction ID. It's not sending the original transaction ID from the first purchase. It's a new one that doesn't exist in the receipt at all, and never did. I don't know what to call this other than 'invalid'. If it sent an update message indicating the existing transaction ID, then everything you're saying would make total sense, but it isn't.


Also to be clear, this is not going into the 'do you want to download it again for free' flow that you're describing. It says you already have the subscription and all you can do is hit 'OK', but as soon as that popup appears, the update has already been posted.

I am differentiating between the transaction and the receipt. A new transaction has certainly taken place between the app, the user and StoreKit. That new transaction has resulted in a call to updatedTransactions. (That term 'updatedTransactions' supports my position that a new transaction has taken place.) That new transaction has a unique transactionIdentifier for important historical anti-fraud reasons.


Your complaint is that the receipt is not altered by the transaction. That complaint is based on the belief that the receipt should reflect the transactions not the purchases. I disagree with that belief and think you are using the receipt for something that it was not meant to do.


>Also to be clear, this is not going into the 'do you want to download it again for free' flow that you're describing. It says you already have the subscription and all you can do is hit 'OK', but as soon as that popup appears, the update has already been posted.


Thanks for clarifying what is happening. But I think this is what is called 'a distinction without a difference' .

Yes, I believe the updatedTransaction should refer to something in the receipt, in order to allow verification of that transaction (among other things). Documented best practice is to verify all transactions against the receipt, so I don't know where you're getting the idea that transactions and the receipt are unrelated. All updated transactions should either be a re-play of an existing non-consumed transaction, a new transaction, or a restoration of a previous transaction. The latter two of those will generate a new entry in the receipt. Everything you have described in your previous post falls under these cases, makes sense, and we handle all that just fine. Only in this one specific case with subscriptions have I ever seen a transaction get updated that doesn't exist in the receipt.


Here's documentation contrary to your position that the transaction ID is not intended to index into the receipt.


https://developer.apple.com/documentation/storekit/skpaymenttransaction


> Completed transactions provide a receipt and transaction identifier that your app can use to save a permanent record of the processed payment.


https://developer.apple.com/documentation/storekit/skpaymenttransaction/1411288-transactionidentifier


> The value of this property corresponds to the Transaction Identifier property in the receipt.


Please let me know where the docs indicate that transaction IDs don't refer to an entry in the receipt.


P.S., I should mention:


> Thanks for clarifying what is happening. But I think this is what is called 'a distinction without a difference' .


Yeah, there is a difference. One works as expected, the other does not.

Help me understand what you would do differently with a receipt if it included a field for transaction_identifier for the current transaction when the current transaction was a ‘purchase’ of an item that had already been purchased by the user at an earlier date.

We would find that transaction in the receipt, verify that the purchase is valid, and enable whatever functionality/content is provided by that purchase (if not already enabled). The usual IAP stuff. If it worked the same as a restored purchase or a 'repurchase for free' I'd just handle it the way we handle those, and be totally happy, but it doesn't. I'm still unclear why you think it should be any different from those cases.


And yes, I know we can just walk the whole receipt and make sure that everything in there is handled and call it a day, but the whole point of these updatedTransaction notifications is to be told when a change (or previously unhandled purchase) happens so it can be processed and not have to keep and compare the state of the receipt constantly. And it works just fine in every other situation, except this 'you are already subscribed' case.

Thanks for clarifying your issue.


A bit of history. The transactionIdentifier used to be the key to preventing a fairly widespread hack used to steal IAPs. The idea was that you would use a non-unique transactionIdentifier to identify a duplicated signed receipt. With the new receipt system, the lack of a unique transactionIdentifier in signed receipts for both restored transactions and repurchase transactions has made that technique less useful in fereting out the duplicate receipt hack. I was an early complainer and have posted securtity bugs on this matter; but Apple has not responded I assume because they are secure in believing that if a receipt is obtained from [[NSBundle mainBundle] appStoreReceiptURL] it can't be a duplicate. I disagree so I decode on board which is more secure than the Apple server technique. But your concerns are different - you are not worried about fraud.


In a receipt after a restoreCompletedTransaction (for a non-consumable or an autoreneawable IAP) or after a re-purchase (after an earlier purchase of a non-consumable or autorenewable IAP) the receipt does not include any reference to the transaction.transactionIdentifier returned in the call to updatedTransaction. As I stated earlier, this is because the receipt documents paid events. A paid event is where money is transfered. There can be various transactions which do not represent a paid event - a failed transaction, a restore transaction, a repurchase of a previously purchased IAP. These non-paid events result in a transaction with a unique transaction.transactionIdentifier but no changes in the receipt (except for receipt_creation_date).


Your concern as I understand it is that you want some way of having the transactionIdentifier point to the section of the receipt that indicates the purchase made by that transaction. I assume you transfer to your server the value of transaction.transactionIdentifier, you send the receipt to the Apple servers for decoding and you scan the decoded receipt for the section that includes the transactionIdentifier. In a restore and in a repurchase you can't do that. What you can do is send to your servers the transaction.payment.productIdentifier (and transaction.transactionState). Then scan the receipt for the section of the receipt that contains that product_id and the latest value for purchase_date. That will be the thing the user is restoring or repurchasing. Also, you can discover that the transaction_id for that section either matches or does not match the transaction.transactionIdentifier value in updatedTransactions and note that the value of transaction.transactionState is either SKPaymentTransactionStateRestored or SKPaymentTransactionStatePurchased. Using these values you can determine if the transaction is a restore, a purchase or a repurchase for free. (Differentiating between purchase and repurchase is very important for certain apps, perhaps not yours.)

Receiving transaction that is not present in the receipt (auto-renewing subscriptions)
 
 
Q