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

Badge +3

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

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

Hi,

 

yes, signed in with a valid Apple account. The same account (and purchasing with it) works with another app where I built the App Store code myself.

 

The log just says: “Error while purchasing: This receipt is missing”. No other helpful info.

Products and offerings are loading fine, though.

Badge +3

@sundeep I just tried to re-setup any purchasing options and while I can see the offerings, the error ‘receipt is missing’ is still the same. I’m out of ideas and need advice - the sooner, the better.

 

Thanks,

Patrick

Badge +3
default	17:27:32.930759+0200	Body Measurements	[Purchases] - DEBUG: ℹ️ API request completed: GET /v1/subscribers/5278094C-0FF0-4B3A-9038-468AAE8B8FB1/offerings 304
default 17:27:32.930917+0200 Body Measurements [Purchases] - DEBUG: ℹ️ GetOfferingsOperation: Finished
default 17:27:32.930993+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Serial request done: GET subscribers/5278094C-0FF0-4B3A-9038-468AAE8B8FB1/offerings, 0 requests left in the queue
default 17:27:38.812907+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Vending Offerings from cache
default 17:27:40.669229+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.669337+0200 Body Measurements [Purchases] - WARN: 🍎‼️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.669570+0200 Body Measurements [Purchases] - ERROR: 🍎‼️ The receipt is missing.
default 17:27:40.669693+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.669817+0200 Body Measurements [Purchases] - WARN: 🍎‼️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.670051+0200 Body Measurements [Purchases] - ERROR: 🍎‼️ The receipt is missing.
default 17:27:40.670161+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.670274+0200 Body Measurements [Purchases] - WARN: 🍎‼️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.670500+0200 Body Measurements [Purchases] - ERROR: 🍎‼️ The receipt is missing.
default 17:27:40.670614+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.670727+0200 Body Measurements [Purchases] - WARN: 🍎‼️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.670944+0200 Body Measurements [Purchases] - ERROR: 🍎‼️ The receipt is missing.
default 17:27:40.671057+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.671169+0200 Body Measurements [Purchases] - WARN: 🍎‼️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.671386+0200 Body Measurements [Purchases] - ERROR: 🍎‼️ The receipt is missing.
default 17:27:40.671498+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.672743+0200 Body Measurements [Purchases] - WARN: 🍎‼️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.672956+0200 Body Measurements [Purchases] - ERROR: 🍎‼️ The receipt is missing.
default 17:27:40.673068+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.673178+0200 Body Measurements [Purchases] - WARN: 🍎‼️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:27:40.673403+0200 Body Measurements [Purchases] - ERROR: 🍎‼️ The receipt is missing.
default 17:28:05.576308+0200 Body Measurements [Purchases] - INFO: 💰 Purchasing product from package 'de.trickbusch.bfc.BodyMeasurements.store.ChartsHistory.monthly' in Offering 'de.trickbusch.bfc.BodyMeasurements.store.ChartsHistory'
default 17:29:41.330902+0200 Body Measurements [Purchases] - WARN: ⚠️ allowSharingAppStoreAccount is set to false and restorePurchases has been called. Are you sure you want to do this?
default 17:29:41.331454+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Found 3 unsynced attributes for App User ID: 5278094C-0FF0-4B3A-9038-468AAE8B8FB1
default 17:29:41.331511+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Unsynced attributes: ["$idfv": Subscriber attribute: key: $idfv value: 5278094C-0FF0-4B3A-9038-468AAE8B8FB1 setTime: 2022-04-15 15:16:36 +0000, "$idfa": Subscriber attribute: key: $idfa value: setTime: 2022-04-15 15:16:36 +0000, "$ip": Subscriber attribute: key: $ip value: true setTime: 2022-04-15 15:16:36 +0000]
default 17:29:41.331568+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Force refreshing the receipt to get latest transactions from Apple.
default 17:29:49.991890+0200 Body Measurements [Purchases] - DEBUG: ℹ️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:29:49.992006+0200 Body Measurements [Purchases] - WARN: 🍎‼️ Unable to load receipt, ensure you are logged in to a valid Apple account.
default 17:29:49.992130+0200 Body Measurements [Purchases] - WARN: 🍎‼️ App running in sandbox without a receipt file. Restoring transactions won't work until a purchase is made to generate a receipt. This should not happen in production unless user is logged out of Apple account.
default 17:29:49.992238+0200 Body Measurements [Purchases] - ERROR: 🍎‼️ The receipt is missing.
default 17:29:49.992353+0200 Body Measurements [Purchases] - ERROR: 💰 Product purchase for 'de.trickbusch.bfc.BodyMeasurements.store.ChartsHistory.monthly' failed with error: Error Domain=RevenueCat.ErrorCode Code=9 "The receipt is missing." UserInfo={source_file=RevenueCat/PurchasesOrchestrator.swift:666, source_function=syncPurchases(receiptRefreshPolicy:isRestore:completion:), readable_error_code=MISSING_RECEIPT_FILE, NSLocalizedDescription=The receipt is missing.}

 

Badge +3

I appended the complete log. The thing is, I am logged in with a valid Apple account (my main account) which works with purchases on the Watch in other apps.

 

I even get the success message via the purchasing overlay, but still the receipt is missing.

 

Any advice?

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

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.

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!! 🎉

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

@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

Reply