Is it possible to detect at runtime that an application has been installed through TestFlight Beta (submitted through iTunes Connect) vs the App Store? You can submit a single app bundle and have it available through both. Is there an API that can detect which way it was installed? Or does the receipt contain information that allows this to be determined?
-
4Just to be clear you are talking about the new TestFlight beta testing through iTunes Connect? Or are you talking about when you have uploaded to TestFlight directly? – keji Sep 28 '14 at 04:59
-
The new TestFlight beta, will clarify – combinatorial Sep 28 '14 at 15:13
-
1Looks like -[NSString containsString:] is an ios8 addition. If the App Store auto testing tries to run it on ios7, no go. ([receiptURLString rangeOfString:@"sandboxReceipt"].location != NSNotFound) should do the trick. – rgeorge Sep 29 '14 at 03:19
-
2I was going to ask about detecting on iOS 6 which doesn't have appStoreReceiptURL, but it seems that the TestFlight app is iOS 8 only; so -[NSString containsString] might be fine after all. I've put app store beta testing on hold because of this, but I guess some people might be using a hybrid testing strategy, with Ad-Hoc for legacy testing and AppStore beta for public beta, so rangeOfString still wins. – Gordon Dove Nov 04 '14 at 10:05
5 Answers
For an application installed through TestFlight Beta the receipt file is named StoreKit/sandboxReceipt
vs the usual StoreKit/receipt
. Using [NSBundle appStoreReceiptURL]
you can look for sandboxReceipt at the end of the URL.
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSString *receiptURLString = [receiptURL path];
BOOL isRunningTestFlightBeta = ([receiptURLString rangeOfString:@"sandboxReceipt"].location != NSNotFound);
Note that sandboxReceipt
is also the name of the receipt file when running builds locally and for builds run in the simulator.
Swift Version:
let isTestFlight = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"

- 7,115
- 4
- 45
- 59

- 9,132
- 4
- 40
- 58
-
1
-
I don't see why it would be a problem, the app should still have a receipt. Take a look at the file system for the app and see if there is a StoreKit directory with a receipt. – combinatorial Oct 14 '14 at 17:23
-
7As noted, this works for local testing on the device, but not on the simulator. I added something like #if TARGET_IPHONE_SIMULATOR isRunningInTestMode = YES; #endif Obviously, this needs #import
– Gordon Dove Nov 01 '14 at 14:10 -
13Compact version: `[[[[NSBundle mainBundle] appStoreReceiptURL] lastPathComponent] isEqualToString:@"sandboxReceipt"]` (True if running TestFlight distributed binary) via [Supertop/Haddad](http://blog.supertop.co/post/108759935377/app-developer-friends-try-testflight) – Nick Feb 02 '15 at 07:29
-
3This method can't be used in extension bundles since the receipt is exists for only host bundle. – jeeeyul Aug 19 '15 at 05:15
-
2My iOS 8 testing results in `StoreKit/sandboxReceipt` when installed as a debug build via Xcode on device or simulator. So this may not accurately distinguish *testflight* builds from all other builds. – pkamb Aug 27 '15 at 02:07
-
4Also seems to return YES when installing a build with Ad Hoc distribution. – Keller Oct 05 '15 at 13:38
-
This is crazy. There has to be a way without having to code... I mean i'm using xcconfig files to store different configurations for different environments. Having to code this defeats the purpose of those files... I will need to add each and every env key in a single file with different names? :( Or is there a better way? How is this not a widely problem? Why don't I find more complaints about this? – Nuno Gonçalves May 05 '16 at 07:29
-
1@NunoGonçalves when you upload a build to TestFlight it can first be in TestFlight and then go live in the AppStore, so build time properties can't be changed when one build can be in two places. However, there is nothing from stopping you doing a special build for TestFlight that is never made live in the AppStore, in which case your approach works just fine. – combinatorial May 05 '16 at 16:34
-
@combinatorial thanks for your answer. That actually makes sense. The problem with that is that I always have to remember which build is supposed to be going to TestFlight and which one is supposed to be going to the App Store, right? That seems very error prone to me. I imagine sending a build for App Store with staging endpoints. Or worst, test flight with production ones. And test flight notifies every internal tester as soon as it's processed. – Nuno Gonçalves May 05 '16 at 17:05
-
1I'm getting "receipt" and not "sandboxReceipt" when running this on simulator in XCode 8. – nickdnk Nov 26 '16 at 10:34
-
7
-
2Still working as expected in 2021 (had a need to feed Bugsnag an identifier to distinguish TestFlight from development and production builds). – tbauer428 Oct 15 '21 at 14:30
-
This doesn't seem to work for Mac apps. The receipt is named "receipt" in TestFlight-distributed builds. – Niels Mouthaan Nov 01 '22 at 12:17
-
@NielsMouthaan I agree, if the app is built via Mac Catalyst, and it's distributed via TestFlight, it does not return "sandboxReceipt". Is there any solution how to check whether the build is TestFlight if it's a mac app? – Patrik Vaberer May 30 '23 at 14:28
Based on combinatorial's answer I created the following SWIFT helper class. With this class you can determine if it's a debug, testflight or appstore build.
enum AppConfiguration {
case Debug
case TestFlight
case AppStore
}
struct Config {
// This is private because the use of 'appConfiguration' is preferred.
private static let isTestFlight = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
// This can be used to add debug statements.
static var isDebug: Bool {
#if DEBUG
return true
#else
return false
#endif
}
static var appConfiguration: AppConfiguration {
if isDebug {
return .Debug
} else if isTestFlight {
return .TestFlight
} else {
return .AppStore
}
}
}
We use these methods in our project to supply different tracking id's or connection string per environment:
func getURL(path: String) -> String {
switch (Config.appConfiguration) {
case .Debug:
return host + "://" + debugBaseUrl + path
default:
return host + "://" + baseUrl + path
}
}
OR:
static var trackingKey: String {
switch (Config.appConfiguration) {
case .Debug:
return debugKey
case .TestFlight:
return testflightKey
default:
return appstoreKey
}
}
UPDATE 05-02-2016: A prerequisite to use a preprocessor macro like #if DEBUG is to set some Swift Compiler Custom Flags. More information in this answer: https://stackoverflow.com/a/24112024/639227

- 6,446
- 1
- 28
- 32

- 1,381
- 1
- 11
- 12
-
2@Urkman Make sure you are setting the `-D DEBUG` flag. More information can be found [here](http://stackoverflow.com/questions/24003291/ifdef-replacement-in-swift-language). – Caleb Feb 03 '16 at 19:35
-
Thnx @Caleb, I added more explanation on the prerequisites to the answer. – LorenzoValentijn Feb 05 '16 at 10:30
-
3Thanks for your answer, I found it very helpful! Also good to know, using `#if targetEnvironment(simulator)` you determine whether you're running in a simulator. So I have the options Simulator/TestFlight/AppStore (which is in my case preferred over `Debug`) :-) – Jeroen Dec 03 '19 at 07:36
Modern Swift version, which accounts for Simulators (based on accepted answer):
private func isSimulatorOrTestFlight() -> Bool {
guard let path = Bundle.main.appStoreReceiptURL?.path else {
return false
}
return path.contains("CoreSimulator") || path.contains("sandboxReceipt")
}

- 12,424
- 2
- 27
- 26
-
Nice to include the simulator, but you might want to change the function name as it is no longer true for all cases. – dbn Sep 29 '16 at 18:38
-
2WOW! It works! Awesome! Returns TRUE for TestFlight and FALSE for AppStore for the same build (one build builded in the one scheme with one provisioning). Perfect! Thank You! – Argus Sep 26 '18 at 08:52
-
-
3@Ethan this answer was edited after I made my comment; the method name used to be `isTestFlight()` – dbn Oct 04 '19 at 22:29
-
Please note that this method is a heuristic and may not be 100% accurate or future-proof, as Apple might change the receipt file paths or naming conventions in future updates. – Rob May 02 '23 at 08:37
I use extension Bundle+isProduction
on Swift 5.2:
import Foundation
extension Bundle {
var isProduction: Bool {
#if DEBUG
return false
#else
guard let path = self.appStoreReceiptURL?.path else {
return true
}
return !path.contains("sandboxReceipt")
#endif
}
}
Then:
if Bundle.main.isProduction {
// do something
}

- 15,320
- 6
- 84
- 70
There is one way that I use it for my projects. Here are the steps.
In Xcode, go to the the project settings (project, not target) and add "beta" configuration to the list:
Then you need to create new scheme that will run project in "beta" configuration. To create scheme go here:
Name this scheme whatever you want. The you should edit settings for this scheme. To do this, tap here:
Select Archive tab where you can select Build configuration
Then you need to add a key Config
with value $(CONFIGURATION)
the projects info property list like this:
Then its just the matter what you need in code to do something specific to beta build:
let config = Bundle.main.object(forInfoDictionaryKey: "Config") as! String
if config == "Debug" {
// app running in debug configuration
}
else if config == "Release" {
// app running in release configuration
}
else if config == "Beta" {
// app running in beta configuration
}

- 2,144
- 2
- 23
- 31
-
8While this is a helpful technique it does not answer the question. A single binary is submitted to the App Store and can either be run from downloading through TestFlight or later after approved run after downloading from the App Store. The question is about detecting which version is running. – combinatorial Feb 16 '17 at 15:31
-
Is there an option to make 2 archives in the first place. one for testflight one for the app store. – Klemen Feb 16 '17 at 21:41
-
It's possible, but they have to have different build numbers. And it means managing two builds instead of one. – combinatorial Feb 17 '17 at 01:00
-
ok, in my opinion it's worth it. Especially if you use continuous integration tools. – Klemen Feb 17 '17 at 11:38
-
@KlemenZagar, your approach is a well-known and good one but it does not answer the question. – Stanislav Pankevich May 08 '17 at 13:32
-
-
Agree that this post does not answer the question. Also, risks more complexity for the unwary. – Max MacLeod May 25 '23 at 10:52