Unable to read or download (sandbox) receipt to find original install version on iOS

I am trying to read the [appStoreReceiptURL](https://developer.apple.com/documentation/foundation/bundle/1407276-appstorereceipturl#) to read the receipt in hopes to find the initial date of purchase or initial version the user has. The plan to use this is to migrate from a paid upfront version to a free + IAP as many might have done over the years.

I know I am under a sandbox environment, while I develop this, so the path is file:///.../StoreKit/sandboxReceipt. Currently, I don't require neither local or remote validation of the receipt for the time being so I only need to read the file.

Checking if it exists using the FileManager it says that it doesn't so I start a SKReceiptRefreshRequest to retrieve a copy of the receipt from Apple's servers. I know that receipt won't have the exact data I am looking for but as far as I know it will give me an initial install version field for this environment that's 1.0 so it'd work for testing purposes. After setting the SKRequestDelegate and starting the request when the receipt is not found neither the requestDidFinish nor didFailWithError delegate methods are called. Currently I am not able to retrieve any copy of even a "sample" receipt for the sandbox environment. This sample code I am using it's pretty simple so far and is as follows,

Code Block
import StoreKit
@objc
class ReceiptFetcher : NSObject, SKRequestDelegate {
let receiptRefreshRequest = SKReceiptRefreshRequest()
override init() {
super.init()
receiptRefreshRequest.delegate = self
}
@objc
func fetchReceipt() {
guard let receiptUrl = Bundle.main.appStoreReceiptURL else {
print("unable to retrieve receipt url")
return
}
do {
// if the receipt does not exist, start refreshing
let reachable = try receiptUrl.checkResourceIsReachable()
// the receipt does not exist, start refreshing
if reachable == false {
receiptRefreshRequest.start()
}
} catch {
print("error: \(error.localizedDescription)")
/*
error: The file “sandboxReceipt” couldn’t be opened because there is no such file
*/
DispatchQueue.main.async {
self.receiptRefreshRequest.start()
}
}
}
// MARK: SKRequestDelegate methods
func requestDidFinish(_ request: SKRequest) {
print("request finished successfully")
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("request failed with error \(error.localizedDescription)")
}
}


This is being called from the AppDelegate to first of all request a fresh receipt or load one if it's not present.

Currently I am testing this using an iPad with my Apple ID logged in but under Settings.app --> App Store --> Sandbox Account I've logged in with a sandbox account created inside the development team which this app is published under that account.
  1. Do I need to enable IAP in the AppStore Connect account so I am able to retrieve receipts?

  2. Is receipt validation strictly necessary in this case? I wouldn't need to validate against some third party as the initial purchase has been through the App Store.

  3. I would want to only read the fields of the receipt that show information on the initial purchase date/version. I remember did this some time ago with another app that was free but with an IAP. And AFAIK every app, either paid or free, has a receipt attached with this kind of information but being currently on a sandbox environment I am a bit confused.

I haven't found anything so far that's relevant and can point me towards what's going on.

Running iOS 14b3 and Xcode 12b3.

Replies

Just managed to achieve this and I don't know why, maybe beta issues? Tried to do this for the first time on my iPhone and worked as expected. I am logged against the same Apple ID and Sandbox Account on both devices, iPad and iPhone, but the former continuously failed to update the receipt and I don't know why yet.
Here's a canned response that I have on this subject - it's doesn't address the code aspect of the question, but you'll find this information useful anyway.

When a developer has an existing paid application, but would like to make the app Free with In-App Purchase the transition process is called the “Paid to Freemium” process. The free version of the app will be a upgrade to the existing "paid" version with an upgraded version string. You will modify the app so that at first launch, the app validates the appStoreReceipt. Assuming that the validation status is 0, the app process then checks the JSON payload, for the “originalapplicationversion” value to determine which version of the app, the iTunes user first installed. If the “originalapplicationversion” value is for a previous “paid” app version, the application provides the user with access to the premium content. If the “originalappversion” value is for the current “free” version, then the user must make the In-App Purchase to have access to the premium content. If the user is a “free” app customer, then the user has access to only minimal functionality, but they can access the “Buy” button to obtain premium content access. The “paid” app version customer should never be presented with the “Buy” button.

To determine whether a customer should have access to the content check the "originalappversion" field.

The specific process for a paid to freemium app
  1. on first launch, the app determines whether it has checked the applicationReceipt pointed to by

NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];

(The following step is primarily a sandbox environment issue)
If the receipt is not present, the app alerts the user that the applicationReceipt may need refreshing and asks whether the user want to refresh the receipt. If the user agrees, the app makes the SKReceiptRefreshRequest call and process the resulting receipt. See step 2. If the user declines to refresh the receipt, the application makes the assumption that the user is a new user who has no privileges to access premium content and exits the receipt validation process to the main part of the app.

The gotcha here is that when you install the app using Xcode, there will not be an appStoreReceipt. This is true whenever the app is installed by some other means than by the app store. This also happens in App Review which uses a custom App Install process. One other note, if the app finds no appReceipt, it must not force the issue to require the use of SKReceiptRefreshRequest to have a receipt installed. The SKReceiptRefreshRequest presents the user with an Authentication dialog. If the user cancels this dialog, then treat the user in the same manner as if the appStoreReceipt was present, but the contents of the inapp array is empty or that the user is using the most recent version of the app. The contents of the inapp array is an array of purchase records of In-App Purchases made by the user within the app.

2. Validating the receipt. I refer you to the Apple Developer documentation.
<https://developer.apple.com/documentation/storekit/in-app_purchase/validating_receipts_with_the_app_store>

There are 2 validation methods - static or App Store Server receipt validation. For an iOS app, the static method is more complicated in that you are required to obtain and build your own OpenSSL library to use to validate the receipt with. The recommended process is to forward the base64 encoded appStoreReceipt to your server. Your server then forwards the receipt to the iTunes Store verifyReceipt server for validation (known as server validation). An application may send the base64 encoded appStoreReceipt directly to the verifyReceipt server (which makes the validation process susceptible to a man-in-the-middle attack.

The advantage with static receipt validation is that no network connection is required. However, if your application offers an auto-renewable subscription In-App Purchase type, server validation provides specific validation support advantages which is not available under static validation.


What url should I use to verify my receipt?

Use the sandbox URL https://sandbox.itunes.apple.com/verifyReceipt while testing your application in the sandbox and while your application is in review.
Use the production URL https://buy.itunes.apple.com/verifyReceipt once your application is live in the App Store.

Important: The App Review team reviews apps in the sandbox.

Always verify your receipt first with the production URL; proceed to verify with the sandbox URL if you receive a 21007 status code. Following this approach ensures that you do not have to switch between URLs while your application is being tested or reviewed in the sandbox or is live in the App Store.
The 21007 status code indicates that this receipt is a sandbox receipt, but it was sent to the production service for verification. A status of 0 indicates that the receipt was properly verified.

3. Parsing the validated receipt.
Assuming that the validation status result is 0, then parse the JSON results and look for the field “originalapplicationversion”. The field “originalapplicationversion” is a UTF8String and must be compared to the CFBundleVersion for the paid version of the app. If the originalapplicationversion is equal to or less than the paid version of the app, the user is a paid app version user and should be given access to the paid content of the app. If the originalapplicationversion is greater than the paid version of the app, then this is a new user who must purchase premium access using In-App Purchase.

The gotcha here is that in the sandbox, using SKReceiptRefreshRequest to install an appStoreReceipt will result in one where the originalapplicationversion is “1.0”. What you want to make sure, is to compare this field with the paid-app version - lets say that the paid app version is “4.0” make sure to compare against this value in the production build of the app. So the issue becomes, if there’s no appStoreReceipt, and the app uses the SKReceiptRefreshRequest call to refresh the receipt, the originalappversion field with be “1.0” - so use that as your test case for a paid app customer. Use the lack of receipt as the new app user test case so that you can test the in app purchase process.

When the app makes it to the production App Store, and a paid user installs the app from the app store, the appStoreReceipt will be present and the originalappversion will match the version the user first installed - or it could be the current version of the app, if for a new user.

rich kubota - rkubota@apple.com
developer technical support CoreOS/Hardware/MFI