Question

Receipt is missing when trying to Purchase via TestFlight


Badge +3

As per the title:

I always get the ‘Receipt is missing error’ when testing via TestFlight.

In Xcode, when configured with a StoreKit file, it works fine.

 

Without StoreKit and TestFlight it does not work, the app review is also always getting rejected.

I cannot see why it does not work.

 

The products and offerings are configured correctly (and they are shown in the app), it’s just the receipt that is always missing when purchasing.


29 replies

Userlevel 5
Badge +8

@Prowl I believe your problem is likely to be related to something different - most likely, there’s a problem with the App Store Connect setup in the RevenueCat dashboard. This would explain why you can make purchases with StoreKit Config (doing that means the SDK doesn’t interact with App Store Connect), why you can make purchases in sandbox (which means the SDK solely interacts with App Store Connect), and why those purchases can’t be synced with RevenueCat’s backend (the backend tries to sync with App Store Connect and fails). 

 

Could you double-check your setup for App Store Connect / RevenueCat dashboard against the steps described here? https://docs.revenuecat.com/docs/itunesconnect-app-specific-shared-secret

@Andy Is there a way to determine where the breakdown is specifically? I will go through everything again to ensure that I have done it properly.

 

[UPDATE]

I think I was able to address it. I had not set the app-specific shared secret. Thanks for helping me out so quickly! 

Userlevel 3
Badge +8

Hi @Patrick Busch - thanks for those details and debug logs. It does seem strange that you’re specifically getting this error when purchasing… here are a few more questions if you don’t mind:

  • What SDK version are you on when you’re testing this?
  • Can you share how you’re configuring the SDK in your code? I see some warning messages about `allowSharingAppStoreAccount` in your logs - are you changing this property?
  • You mention testing with TestFlight and testing with StoreKit simulator. Have you tried with a physical device in the regular sandbox, without StoreKit files? Do you still get these error messages when you try there?
Badge +3

Hi @sundeep,

 

I tested with 4.1.0 and 4.3.0, in both versions with and without the StoreKit option.

Physical device via TestFlight has the error, both with and without StoreKit files.

In the simulator (XCode environment) it works with StoreKit files.

 

Here is the code I use with RevenueCat - it’s all encapsulated in this one file:

 

import Foundation
import RevenueCat

struct AppStoreItem: Hashable {
let identifier: String
let title: String
let description: String
let priceDescription: String
}

class AppStoreWrapper {
static let shared: AppStore = RevenueCatAppStore()
}

protocol AppStore {

func getEntitlements(_ delegate: @escaping ([Entitlement]) -> Void)
func getItems(_ delegate: @escaping ([AppStoreItem], Bool) -> Void)
func purchase(_ item: AppStoreItem, _ delegate: @escaping ([Entitlement], Bool) -> Void)
func restorePurchases(_ delegate: @escaping ([Entitlement], Bool) -> Void)
}

class RevenueCatAppStore: AppStore {

private var availablePackages: [Package] = [Package]()

init() {
Purchases.configure(
withAPIKey: "appl_redacted",
appUserID: DebugSettings.currentIdentifier,
observerMode: false,
userDefaults: UserDefaults.standard,
useStoreKit2IfAvailable: true
)

let debugSettings = DebugSettings.shared

if debugSettings.debugAllowed && debugSettings.active {
Purchases.logLevel = .debug
}

Purchases.shared.collectDeviceIdentifiers()

}

func getEntitlements(_ callback: @escaping ([Entitlement]) -> Void) {

Purchases.shared.getCustomerInfo { (customerInfo, error) in

var entitlements = [Entitlement]()
guard error == nil else {
Logger.shared.error("Error while getting customerInfo: \(error!.localizedDescription)")
callback(entitlements)
return
}

guard let customerInfo = customerInfo else {
Logger.shared.error("CustomerInfo was nil")
callback(entitlements)
return
}

Logger.shared.debug("Entitlements: \(customerInfo.entitlements)")
Logger.shared.debug("Purchased Product Identifiers: \(customerInfo.allPurchasedProductIdentifiers)")
Logger.shared.debug("CustomerInfo raw data: \(customerInfo.rawData)")

Entitlement.all.forEach { entitlement in
if customerInfo.entitlements[entitlement.rawValue]?.isActive == true {
entitlements.append(entitlement)
Logger.shared.info("found entitlement: \(entitlement)")
}
}

callback(entitlements)
}

}

func getItems(_ callback: @escaping ([AppStoreItem], Bool) -> Void) {

Purchases.shared.getOfferings { offerings, error in

var items = [AppStoreItem]()
// Prefetched on launch, doesn't need network request
guard error == nil else {
Logger.shared.error("Error while getting purchases: \(error!.localizedDescription)")
Logger.shared.debug("Error: \(error!)")
callback(items, false)
return
}

guard let currentOffering = offerings?.current else {
Logger.shared.error("Offerings?.current was nil")
callback(items, false)
return
}

Logger.shared.debug("Offerings: \(offerings!)")

self.availablePackages = currentOffering.availablePackages
items.append(contentsOf: currentOffering.availablePackages.map { package in
AppStoreItem(
identifier: package.storeProduct.productIdentifier,
title: package.storeProduct.localizedTitle,
description: package.storeProduct.localizedDescription,
priceDescription: package.storeProduct.localizedPriceString
)
})
callback(items, true)
}

}

func purchase(_ item: AppStoreItem, _ callback: @escaping ([Entitlement], Bool) -> Void) {

var entitlements = [Entitlement]()
let packages = self.availablePackages.filter { package in
package.storeProduct.productIdentifier == item.identifier
}

guard packages.count == 1 else {
Logger.shared.error("Could not find package, count was \(packages.count)")
callback(entitlements, false)
return
}

Purchases.shared.purchase(package: packages[0]) { (_, customerInfo, error, userCancelled) in

guard !userCancelled else {
Logger.shared.info("User cancelled transaction")
callback(entitlements, false)
return
}

guard error == nil else {
Logger.shared.error("Error while purchasing: \(error!.localizedDescription)")
callback(entitlements, false)
return
}

guard let customerInfo = customerInfo else {
Logger.shared.error("customerInfo was nil")
callback(entitlements, false)
return
}

Entitlement.all.forEach { entitlement in
if customerInfo.entitlements[entitlement.rawValue]?.isActive == true {
entitlements.append(entitlement)
Logger.shared.info("found entitlement: \(entitlement)")
}
}
callback(entitlements, true)
}
}

func restorePurchases(_ callback: @escaping ([Entitlement], Bool) -> Void) {

Purchases.shared.restorePurchases { (customerInfo, error) in

var entitlements = [Entitlement]()

guard error == nil else {
Logger.shared.error("Error while purchasing: \(error!.localizedDescription)")
callback(entitlements, false)
return
}

guard let customerInfo = customerInfo else {
Logger.shared.error("customerInfo was nil")
callback(entitlements, false)
return
}

Entitlement.all.forEach { entitlement in
if customerInfo.entitlements[entitlement.rawValue]?.isActive == true {
entitlements.append(entitlement)
Logger.shared.info("found entitlement: \(entitlement)")
}
}
callback(entitlements, true)
}
}
}

 

The error is in the

 

      guard error == nil else {
        Logger.shared.error("Error while purchasing: \(error!.localizedDescription)")
        callback(entitlements, false)
        return
      }

 

section in the purchase function.

 

Thanks for your help!

Badge +3

Hi,

yesterday I lifted the AppStore implementation from another app to this one, which communicates directly without using RevenueCat.

Findings: It works fine. Same account, same device, same IAP configuration.

But I’d rather use the RC features.

 

So I think we can rule out account issues - the mistake must be either in the RevenueCat configuration or in the code above.

Again, I’m testing in TestFlight on a real device.

Any ideas would be greatly appreciated as I’m out of ideas.

 

Userlevel 5
Badge +8

Hi! 👋🏻 Thanks for reporting this!! 

Which watchOS version are you using?

I’m asking because watchOS has had a few issues with receipt file locations. In particular, watchOS < 7.0.0 would return an incorrect receipt URL. We added a workaround, but this could be another watchOS regression, so knowing the version number could help us try to reproduce. It’s also possible that they fixed the original bug and ironically our workaround stopped working. 

 

Could you try printing out the value of Bundle.main.appStoreReceiptURL and tell us what the value is, when running locally and on TestFlight? There’s a chance that TF is now reporting a slightly different URL for it too. 

 

Badge +3

Hi @Andy ,

sure.

I’m on watchOS 8.5.1 (19T252)

 

Contents of appStoreReceiptURL:

Simulator:

file:///Users/rick/Library/Developer/CoreSimulator/Devices/CE278E5B-5FDC-4E77-B8E0-EF3DF4960DF1/data/Containers/Data/PluginKitPlugin/CAC3E81F-B93A-410B-AA6B-1BD1ED861AAF/StoreKit/receipt

XCode on Device:

file:///private/var/mobile/Containers/Data/PluginKitPlugin/70364133-34AE-4888-8772-3A78ECD1BF7A/StoreKit/sandboxReceipt

TestFlight:

file:///private/var/mobile/Containers/Data/PluginKitPlugin/F0E94016-531B-468A-A5A3-D99C7FA6DBE2/StoreKit/sandboxReceipt

 

Does this help?

 

Thanks,
Patrick

Userlevel 5
Badge +8

That’s helpful, thanks! 

As for the alternative implementation that you tried and worked, does it actually verify the receipt contents? 

I’m wondering if maybe the file in the receipt URL is indeed empty (which is what happened with watchOS 6.2 - 7.0) and the alternative implementation is ignoring it. 

 

Badge +3

Hi,

 

This is the code I’m using relevant to our problem:

 

  private func verifyReceipt(sandbox: Bool = false, _ callback: @escaping ([Entitlement], Bool) -> Void) {

let entitlements = [Entitlement]()

let verifyURL: String = {
if sandbox {
return "https://sandbox.itunes.apple.com/verifyReceipt"
} else {
return "https://buy.itunes.apple.com/verifyReceipt"
}
}()

guard let receiptURL = Bundle.main.appStoreReceiptURL else {
callback(entitlements, false)
return
}
guard let receiptString = try? Data(contentsOf: receiptURL).base64EncodedString() else {
callback(entitlements, false)
return
}
guard let url = URL(string: verifyURL) else {
callback(entitlements, false)
return
}

let requestData: [String: Any] = [
"receipt-data": receiptString,
"password": "redacted",
"exclude-old-transactions": false
]
let httpBody = try? JSONSerialization.data(withJSONObject: requestData, options: [])

var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = httpBody
URLSession.shared.dataTask(with: request) { (data, response, error) in
// convert data to Dictionary and view purchases
guard error == nil else {
Logger.shared.error("Could not load receipts with error \(error!.localizedDescription)")
callback(entitlements, false)
return
}

if let data = data,
let jsonData = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: AnyObject] {

self.validate(response: jsonData, sandbox: sandbox, callback)
} else {
Logger.shared.error("Could not read receipt json data")
callback(entitlements, false)
return
}
}.resume()

 

Note: I’m doing this on device which is not recommended, b/c I don’t want to have my own server. Not best practice.

In validate I check the contents of the server response.

Userlevel 5
Badge +8

Thanks for the quick reply! 

 

🤔 this seems really odd - particularly because based on the logs, the SDK is hitting this line https://github.com/RevenueCat/purchases-ios/blob/main/Sources/Purchasing/ReceiptFetcher.swift#L60, which is pretty much the same you’re doing with an early return. 

 

I’m still not sure how this would happen.

 

Just to add one small clarification, though, StoreKit configuration files will only ever be used if you’re running an Xcode build - on TestFlight, those files are ignored, and the configuration for IAPs comes from App Store Connect. Not that it’s relevant here, but just in case you run into weirdness when testing that might be explained by this. 

 

Badge +3

Yeah I‘m actually differentiating between StoreKit and AppStore by naming the products slightly different. 
 

Do you have any idea yet on how to fix it?

Thanks,

Patrick

Badge +3

Hi,

 

I just added my RevenueCat implementation to another app, where I have an iPhone implementation.

There the same implementation works without problems.

 

I hope you can resolve the issue on Apple Watch soon.

 

Best,

Patrick

Userlevel 5
Badge +8

I’m genuinely not sure what’s going on here. Would you be open to doing a quick call to debug it together? 

Badge +3

Yeah, I would be open to it, can you suggest some timeslots?

 

Thanks,

Patrick

Userlevel 5
Badge +8

Hi @Patrick Busch, I apologize for the delay in getting back to you, I’ve been traveling and just got back home. 

Do you have any time this week for this? I’m in GMT -3, and have decent availability this Wednesday or Friday

I am having the same problem, but with an iOS app. I had no problem testing on a simulator with a StoreKit config file, but I can’t get it to work on device in sandbox. I keep getting the same error about `"The receipt is not valid."` when attempting to restore purchases, or `”There was a problem with the App Store.”` when making a new purchase. I first get a message (I presume from Apple/the sandbox) that I am already subscribed, and when I tap “ok” that’s when my app receives the error from RevenueCat about a problem with the App Store.

Userlevel 5
Badge +8

That’s great to hear! Let me know if you run into anything else! 

Have a great day! 

Badge +3

my app is failing review consistently due to the ‘receipt is missing’ error.  local and TestFlight work flawlessly.  outside beta testers on TFlight...perfect.  only fails in actual review.  steps to attach the subscription have been followed to the letter multiple times.

i have blown away products and projects on both the apple and revenueCat side multiple times.  the shared app specific secret is locked into both places, all account info is correct.  this is extremely frustrating as it has been a week of rebuilds.  upgraded to react-native-purchases 5.0.1 and even talked with revCat dev who did the last patch on Purchases.purchaseProduct() … really think this is sitting on your side revCat...obv i could be wrong but have been round and round on this enough.  hopefully it can be resolved.

Badge +3

turns out using this solution 

which does work well in local and in TestFlight was tripping up the app review due to a lack of receipt.  i removed the solution (which helps with an edge case of someone on another users phone trying to buy the in-app purchase) and finally passed the app store review.

i’ll add a comment over there as a heads up to fill out that .restorePurchases() method solution (used to be called .restoreTransactions() ) so the missing receipt issue doesn’t rear its head should someone try it.

Badge +3

Don’t think it’s relevant, but it is a Watch App without iPhone app.

Badge +3

Hi @Andy ,

 

sorry, I didn’t have time the last weeks.

I debugged some of the code myself:

 

In class ReceiptFetcher everything seems fine at first at line 71:

(lldb) po FileManager.default.fileExists(atPath: receiptURL.path)
true

 

In line 79 the code handling for buggy code sets the URL to an invalid one.

(lldb) po FileManager.default.fileExists(atPath: receiptURL.path)
false

 

As the code in the library is set to fix this for all versions below 7.0.0 and I am on 8.6, I think the error is in maybe in the detection of the version.

Badge +3

In my opinion the error is in Line 74 of ReceiptFetcher:

 

let isBelowFirstOSVersionWithoutBug = ProcessInfo.processInfo.isOperatingSystemAtLeast(firstOSVersionWithoutBug)

 

Shouldn’t the be the other way round? This expects to be true if the current version is below the first fixed version, but the code delivers it exactly the other way round.

Userlevel 3
Badge +8

Hi @Patrick Busch , are you sure you were signed in when this occurred? Usually this error points to not being signed into a valid Apple account, docs for reference: https://docs.revenuecat.com/docs/errors#-missing_receipt_file 

Also, do you have debug logs you could share of this occurring (feel free to obscure any information you find sensitive)? Here’s how to enable debug logs: https://docs.revenuecat.com/docs/debugging#debugging 

Badge +3

@Andy

I can confirm this fixes my problem.

I created a pull request here:

https://github.com/RevenueCat/purchases-ios/pull/1625
 

Hopefully someone from your side can integrate it into the next bugfix version.

Userlevel 5
Badge +8

@Patrick Busch great catch!! You’re absolutely right, it looks like that bug was introduced during the Swift migration. I’ll review the PR and we’ll get it shipped soon! 

Thanks so much for reporting and for fixing!! 🎉

Reply