I made a game. I didnt expect it to become wildly popular so I didn't care to write complicated cryptography code for receipt validation. My game's IAP (to remove ads) was working perfectly in sandbox mode. The app has been released live for about the last 16 hours and there is a problem. The app is behaving as if everyone has purchased the "remove ads" IAP.
this is my extremely basic receipt validation code:
override init(){
super.init()
// storekit delegation
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
if SKPaymentQueue.canMakePayments() {
let request = SKProductsRequest(productIdentifiers: NSSet(object: self.productID))
request.delegate = self
request.start()
}
self.checkReceipt()
}
func checkReceipt(){
if let url = NSBundle.mainBundle().appStoreReceiptURL {
if let receipt = NSData(contentsOfURL: url) {
self.adsRemoved = true
}
}
}
Questions:
in my sandbox environment.. the receipt variable wouldn't get unwrapped unless I had purchased the IAP. self.adsRemoved wasn't being set to true. It seems like on the app store itself, these variables always get unwrapped. What's up with that? What should i do.
I know now that it's standard for IAP to become active a day or two after app submission. Could this be related to that? I know if I hit my restore button nothing happens. In my sandbox environment it asks for credentials.. shows a success message, etc.
in my sandbox environment.. the receipt variable wouldn't get
unwrapped unless I had purchased the IAP. self.adsRemoved wasn't
being set to true. It seems like on the app store itself, these
variables always get unwrapped. What's up with that? What should i do.
Your checkReceipt() method checks for the presence of the receipt at the URL given by -appStoreReceiptURL.
This is problematic because every app that is downloaded from the App Store will contain a receipt, whether the app is free or paid.
This is documented in Apple's Receipt Validation Programming Guide:
When an application is installed from the App Store, it contains an
application receipt that is cryptographically signed, ensuring that
only Apple can create valid receipts. The receipt is stored inside the
application bundle. Call the appStoreReceiptURL method of the NSBundle
class to locate the receipt.
So why did it work for you in sandbox mode? This is because sandbox apps installed via Xcode contains no receipt, until either of the following occurs:
A receipt is requested via SKReceiptRefreshRequest (link)
When an in-app purchase is made, or
When previous transactions are restored via -restoreCompletedTransactions
So in a nut shell, your checkReceipt method will definitely set adsRemoved to true for anyone who has downloaded your game from the App Store, because you're checking for the PRESENCE of a receipt, which for reasons above, does not mean anything at all to you.
What you need to be doing is validating whether the receipt is (1) legit, and (2) whether it indicates that the correct in-app purchase has been made.
I know now that it's standard for IAP to become active a day or two after app submission. Could this be related to that? I know if I hit
my restore button nothing happens. In my sandbox environment it asks
for credentials.. shows a success message, etc.
Not true that IAP takes a day or two to activate after app approval, and no—the problem occurred because you're checking for the presence of the receipt, and not validating the receipt.
Unfortunately, receipt validation is complex, but necessary if you want to achieve what you want to achieve (i.e. check if an IAP has been made). You'd want to refer to this objc.io article on receipt validation for more information about how to actually validate the receipt. At bare minimum, the steps involved are:
Locate the receipt, which is what you're already doing.
Verify the receipt is legit and not tampered with.
Parse the receipt to figure out the IAP has been made, because a receipt exist even without purchasing the IAP
You have to validate the receipt sending the receipt data for Apple provided url. Later, based on the response you will receive you have to set the value self.adsRemoved = true/false.
Related
I have created an app which has auto-renewable subscriptions.
The following is the logic that I use to know if the user has an active subscription.
Whenever paymentQueue(_:updatedTransactions:) of SKPaymentQueue is called, I try to perform receipt validation using following steps
I check if the local receipt is present. If it is not present I use SKReceiptRefreshRequest to refresh the receipt.
I send the receipt information to verifyReceipt endpoint of the App Store server.
The server returns response which contains information about the subscription expiration date.
I store the expiration date in the app and present the appropriate UI based on whether the user has an active subscription or not.
The App Store review has rejected my app multiple times because the SKReceiptRefreshRequest errors out. I am unable to reproduce the error faced by the App Store review board.
While searching the internet to solve the problem, I got to know the following facts about the local receipt-
The local receipt is always present in the production mode. The local receipt may not present if the app is installed using Testflight or during testing. (link)
The App Store server will return the latest subscription information even it it sent an old local receipt (link)
From the above 2 pieces of information, I deduce that there is no need to ever call SKReceiptRefreshRequest in production because the App Store server will provide the latest details even if the local receipt is old and the local receipt is always present in production.
In order to get my app through the App Store review, I have decided to remove the SKReceiptRefreshRequest as it gives errors in the Testflight builds and is not required in the production.
Can anyone confirm if I am correct to do this?
Your logic has multiple flaws:
1) paymentQueue(_:updatedTransactions:) is called in the background and (as far as I know) updates already the local receipt. Also an app downloaded from the App Store always contains the receipt. So there is no need to call SKReceiptRefreshRequest in that method.
2) SKReceiptRefreshRequest requires the users to input his password to allow the receipt refresh. Since you triggered the method within paymentQueue(_:updatedTransactions:), which was called in the background, I reckon this is the problem why the refresh request failed and Apple rejected your app. Nevertheless this method has its reason for being: in production you need it to allow users to restore purchases after reinstalling the app or on other devices and for debug and TestFlight builds you need it to get the latest receipt.
3) You shouldn't send the receipt from your app to Apple's endpoint
Warning
Do not call the App Store server verifyReceipt endpoint from your app. You can't build a trusted connection between a user’s device and the App Store directly, because you don’t control either end of that connection, which makes it susceptible to a man-in-the-middle attack.
Source
How to proceed?
I would recommend to do the following things:
1) Do not trigger SKReceiptRefreshRequest in paymentQueue(_:updatedTransactions:)
2) If not already done provide a "restore purchases" button in your app (which calls SKReceiptRefreshRequest)
3) Implement local or server-to-server receipt validation
I have used the following way to generate the receipt and send the generated receipt to server for verification:
https://developer.apple.com/documentation/storekit/in-app_purchase/validating_receipts_with_the_app_store
I have a side project and I recently worked on my receipt manager to make it stronger and to relies more on the receipt of the app rather than persistently storing a value after a transaction.
However, there are 2 main points which although I read Apple docs and other answers on the web, I'm still confused about:
1. When a user restore their purchase, does the receipt get refreshed?
I made several tests in sandbox, and I have seen that when restoring, the receipt gets refreshed, and when I verify the receipt through the iTunes server verification, it returns a JSON including the latest transactions. This is very helpful because even if I close/open the app, the app receipt is updated and I can always verify it without refreshing it.
However, in production, this didn't work. The app receipt wasn't refreshed after restoring purchases and my users got asked to restore purchases continuously. Can anyone answer on this point?
2. Does the refresh receipt request triggers an alert asking for the Apple ID's password in production?
From the previous point, I thought ok, I will force receipt refresh after a user restores their purchases. However, in development / sandbox, I get asked to insert my sandbox user's pass every time I try to refresh the receipt (although I could restore purchases without a password request before asking for the refresh). I read a lot about this and someone says that might not happen in production. Does any of you have a clarification on this?
Note:
I know that when restoring / purchasing I get back a transaction with the receipt, however, I need to use the App Receipt to verify transactions (and this is also what Apple says to do).
Thank you in advance.
1. Refreshing the receipt
In theory, calling restore purchases should get the latest receipt. In the instances where you are experiencing issues, take a look at SKReceiptRefreshRequest. Typically, I use this in production when a call to restore purchases has encountered errors.
Use it wisely, triggering that API can lead to showing the Sign In prompts for the App Store.
2. When is the user asked to sign in?
Sadly, I have seen this vary so I cannot give a definitive answer. More often than not, a call to restore purchases should not trigger a sign in. Explicitly using SKReceiptRefreshRequest will.
If the user is not signed in to the store, calling any Store API like attempting a purchase or restoring purchases could trigger the sign in flow.
What Apple Says
From the docs
Refreshing a receipt doesn't create new transactions; it requests the latest copy of the receipt from the App Store. Refresh the receipt only once; refreshing multiple times in a row has the same result.
Restoring completed transactions creates a new transaction for every transaction previously completed, essentially replaying history for your transaction queue observer. Your app maintains its own state to keep track of why it’s restoring completed transactions and how to handle them. Restoring multiple times creates multiple restored transactions for each completed transaction.
My Recommendation
Store the hash of the last receipt you used on device. You can use this hash to check against the latest receipt so you know if anything has changed. Whenever your App resumes you can always check if the current receipt hash is different from the last cached value.
Try and submit the receipt as soon as possible. Typically when the App has launched.
If a user tries to manually restore purchases, I would start with a call to restoreCompletedTransactions. This can trigger an App Store sign in but is often less likely. Most of the time this is enough as the receipt on the device is often pretty up to date.
If the user tries another restore purchases, OR if the call failed, then move to SKReceiptRefreshRequest to guarantee a fresh receipt.
When using SKReceiptRefreshRequest, I would recommend wrapping this behind UIAlertController. I normally show something that indicates it has failed and have a "Retry" button that uses the request. This will trigger a new store sign in.
Use restoreCompletedTransactions to get a playback of all completed transactions the device is aware of.
When a user restore their purchase, does the receipt get refreshed?
Yes, it should. But it also sounds like you're doing some server-side validation? If that's the case, you can send any receipt from the user to the /verifyReceipt endpoint to get the latest status. You don't need to send the latest receipt, since /verifyReceipt will also refresh it.
Does the refresh receipt request triggers an alert asking for the Apple ID's password in production?
There's no clear Apple documentation on this, but it definitely will if there's no receipt file present in the app (rare in production). But if you're doing server-side validation (see #1), then you can send up any receipt you have, no need to refresh it. So you're only refreshing the receipt if nothing is present, which will trigger the sign-in. Keep in mind a receipt file is not present on the device after installing in sandbox - only after a purchase. This differs a lot from production where a receipt file is generated after installation.
From what it sounds like you're trying to do, my recommendation would be to check if any receipt file is present at launch, send it to /verifyReceipt to get the latest status for the user and cache the result. You can do this on every app launch.
In a perfect world you're storing the receipt server-side and keeping it up-to-date there, but you mentioned side project so that sounds like overkill. However, an out-of-the box solution that correctly implements all of this and will scale with you - such as RevenueCat - is another alternative (Disclaimer: I work there).
After many tests and after I sent my app in production, I'm now able to answer my questions properly:
1. When a user restores their purchase, does the receipt get refreshed?
YES, this is immediate as for Sandbox, BUT the problem is that the receipt DOESN'T include non-consumable purchases.
This means in other words that the receipt will include the purchased subscriptions, but you won't find purchases of non-consumable products.
However, when the user restores or purchases, you get the transactions in return, and you can extract the non-consumable products, and store this info somewhere like UserDefaults or Keychain, so you can use them when the user opens your app.
For the rest, the best approach is to always validate and check the receipt when the app is opened.
2. Does the refresh receipt request trigger an alert asking for the Apple ID's password in production?
YES. For sure it does the first time.
Thank you to Daniel and enc for their answers that can still be useful.
We've been happily processing in-app purchases in our iOS app since the beginning of time. Because the code has been around a while, it uses the now-deprecated transactionReceipt property on the transaction we get when our paymentQueue:updatedTransactions: method is called by Store Kit. We send that receipt to our server, which validates it through a post to Apple's server, does what we need to do on the server, and reports back "success" to our app. Works great.
Now we want to add a subscription-based product so I'm looking at having to re-implement IAP using the appStoreReceiptURL property on the main bundle and loading my app receipt from there. There are a couple things I don't get.
First and most obvious: I get the same SKPaymentTransactionStatePurchased state when paymentQueue:updatedTransactions: is called. For now, when I submit it to Apple I'm just displaying the JSON I get back. Since the status is non-zero I'm also submitting to the sandbox server and displaying what I get back from it, too. Problem is in both cases I get this:
{"status":21002, "exception":"com.apple.jingle.mzfairplay.validators.DrmInvalidArgumentException"}
I suspect this has something to do with the fact that I'm running a debug build, though not in the sim -- it's running on an actual device. To get around that, I tried uploading a build to the App Store so I could download through TestFlight, but since I'm using my sandbox Apple ID, TestFlight refuses to install it.
So the first question is why am I getting this "DrmInvalidArgumentException", and am I configured correctly to test (debug build on real device, using my sandbox Apple ID to make purchases).
Second question is more baffling to me. As I understand it, I will still get notified via paymentQueue:updatedTransactions, and I will iterate over the transactions I get there (?) but then instead of submitting the receipt in the transaction, I'll submit the app receipt from the URL in the main bundle. It will contain ALL IAP PURCHASES EVER and I will have to iterate over all those to figure out what's new and what I'm interested in, right?
The flow doesn't seem right. I'm getting a notification based on a transaction, but then looking at a dump of ALL my IAP transactions that are contained in the app receipt. So I can't possibly be understanding the flow correctly.
Status 21002 is The data in the receipt-data property was malformed or missing.. The body of the POST to the verify endpoint has to be a JSON dict containing a receipt-data key and a password key that contains your app-specific shared secret that you can get from iTunes Connect.
You might try using this tool to test your receipts. You don't need to use TestFlight, everything should work fine on a debug build as long as you are using a Sandbox iTunes account, which you can create in iTunes Connect.
Your explanation is correct, you will want to send the whole receipt for each purchased transaction. This is redundant, you could potentially keep a app side cache so you don't send the same receipt data each time but it is possible for the contents of appStoreReceiptURL to change at any time.
There are many other tricky edge cases implementing subscriptions. I built RevenueCat because of all these crappy things with subscriptions. Apple really bolted it onto the existing IAP stuff poorly in my opinion.
We have a successful app on the iOS app store with in-app purchases. Every time a purchase is completed we send the receipt to our server, our server than checks the receipt with Apple's servers and logs apple's response(including whether the purchase was valid and that they come from our app in that same time and date).
We have quite a few users who use iap cracks that send us receipts that apple says are invalid. However we started now to see cheaters that have receipts that apple replies that are VALID. What is strange in these cheats, that when such a cheater user purchases in our app, he usually purchases all of the purchases with the exact same receipt.
Have you heard of such a way to 'fool' apple receipt validation?(to generate receipts that apple will say they are from our app in the time of the 'purchase')
Is there something we can do to find those cheaters in their 1st purchase (for the next purchases we can simply check times of the next receipts or make sure that our receipts are unique)
Thanks!
Is there something we can do to find those cheaters in their 1st purchase
Actually, if this is the same hack I've seen discussed as a proof of concept recently, the first purchase is legitimate. The "innovation" is in decoding that legitimate receipt and rejigging its IAP ID with a different one while the receipt overall still appears valid. So simply avoiding the duplicates is enough. Didn't think that one was anywhere near production-ready though, so this might be something different.
We also faced similar issue while developing a game of iOS app store where business model was based on In App Purchase only.
Initially we used to check with Apple Servers for the receipts directly from the device. But some hacker has created a hack for the users where they can install the DNS server certificate on their device which spoofs the response from Apple.
The way to do this is let web server check for the receipts from Apple directly with some kind of hashing or md5 check to make sure the response if from Apple.
here is a link which have a detailed information on this https://www.objc.io/issues/17-security/receipt-validation/
Hope this helps.
We have issues fully understanding the receipt validation flow in iOS.
Here is what we currently do (in development):
In applicationDidFinishLaunching and in applicationWillEnterForeground we validate the receipt on the server side, if there is no receipt or the receipt is invalid, we try to refresh the receipt and revalidate it.
Here are some issues/questions:
What are the cases where there is no receipt available on the device ?
Should we always issue a receipt refresh request when there is no receipt ?
Why is this alert box sometimes shown on startup ? I understand this is shown on a receipt refresh request ?
When should a receipt verification happen ? We currently do it whenever a purchase is made to verify the purchase, is this correct usage ?
In production a receipt is always available on device. In test after
the first install there is not. So if you want to do a correct test,
you must restore a purchase even if it doesn't exist a purchase on that user in the test
environment. Why is that? App downloaded from the appstore always comes with a receipt even if they are free.
Depends on the business logic you want to apply. If you are validating the receipt against a server each time the use launch the app, of course you need the receipt. If it is not present (but in production is always) or not valid, you can ask for a refresh or restore, but as far as I remember you should always ask the user first if he/she want to do that (can be a reason for reject).
Restore and Refresh are not the same thing.
This usually appear in purchase/restor/refresh. But also If the account has some pending requests because the app has crashed or you interrupted the debugging before the request end somehow, you will be bored by a lot of that. There is no way to flush them programmatically, just login until they stop. Of course it will not be a valid test.
It's up to you and about the kind of purchase. If it is an autorenewable subscription, you can validate the receipt against a server, then store the the "end date" on the client and make another check after the date is expired. Pay attention that receipts can be quite big, because the have also all history values.
As Zhang mentioned, if there is no purchase or restore took place, there will be no receipt in the store
Locate the receipt. If no receipt is found, then the validation fails and you should not request receipt refresh again. Only when you will restore process by yourself, you should request for the receipt again.
This will be shown always when you will try to refresh the receipt (or you will pick from settings that you want not to ask for a password for 15 minutes).
Yes.
For more information, look here:
https://www.objc.io/issues/17-security/receipt-validation/#about-validation
If a user downloaded the app from the App Store – yes, receipt always exists.
However, in sandbox if your app was installed via Xcode or Testflight, then there won’t be a receipt until you make a purchase or restore.
Take a look at this complete FAQ about receipt validation in our blog:
https://blog.apphud.com/receipt-validation/
1.No purchase/Restore took place.
2.Nope.See 1
4.Sure.For consumable products,remember to save hash on your server,in order to defeat replay attack.