34

Is there a way in iOS to programmatically check if the currently running app was installed from the iOS App Store? This is in contrast to an app that was run via Xcode, TestFlight, or any non-official distribution source.

This is in the context of an SDK that doesn't have access to the app's source code.

To be clear - I am looking for some signature, so to speak, given to the app (presumably by Apple), that will, without dependence on any preprocessor flags or other build configurations, be accessible to any application at run time.

Carl Veazey
  • 18,392
  • 8
  • 66
  • 81
kevlar
  • 1,110
  • 3
  • 17
  • 30
  • Please clarify. What do you mean by SDK? Are you building a library? Are you building an SDK that outputs an App à la Titanium? In that case, you're still building an app anyway, so while you might be prevented from accessing your "sub-app" source code, you're definitely in control of the running app. – magma Sep 04 '13 at 00:30
  • 2
    To be clear - are you looking for some signature, so to speak, given to the app (presumably by Apple), that will, without dependence on any preprocessor flags or other build configurations, be accessible to any application at run time? – Carl Veazey Sep 04 '13 at 01:44
  • @CarlVeazey Yes, that is exactly is. – kevlar Sep 04 '13 at 07:24
  • @magma No, I'm am building a pure SDK that other developers download and integrate into their Xcode builds. – kevlar Sep 04 '13 at 07:25

4 Answers4

42

Apps downloaded from the App Store have a iTunesMetadata.plist file added by the store:

NSString *file=[NSHomeDirectory() stringByAppendingPathComponent:@"iTunesMetadata.plist"];
if ([[NSFileManager defaultManager] fileExistsAtPath:file]) {
    // probably a store app
}

Perhaps you might want to check if this file exists.

Update:

In iOS8, the application bundle has been moved. According to @silyevsk, the plist is now one level above [the new application main bundle path], at /private/var/mobile/Containers/Bundle/Application/4A74359F-E6CD-44C9-925D-AC82E‌‌​​B5EA837/iTunesMetadata.plist, and unfortunately, this can't be accessed from the app (permission denied)

Update Nov 4th 2015:

It appears that checking the receipt name can help. It must be noted that this solution is slightly different: it doesn't return whether we're running an App Store app, but rather whether we're running a beta Testflight app. This might or might not be useful depending on your context.

On top of that, it's a very fragile solution because the receipt name could change at any time. I'm reporting it anyway, in case you have no other options:

// Objective-C
BOOL isRunningTestFlightBeta = [[[[NSBundle mainBundle] appStoreReceiptURL] lastPathComponent] isEqualToString:@"sandboxReceipt"];

// Swift
let isRunningTestFlightBeta = NSBundle.mainBundle().appStoreReceiptURL?.lastPathComponent=="sandboxReceipt"

Source: Detect if iOS App is Downloaded from Apple's Testflight

How HockeyKit does it

By combining the various checks you can guess whether the app is running in a Simulator, in a Testflight build, or in an AppStore build.

Here's a segment from HockeyKit:

BOOL bit_isAppStoreReceiptSandbox(void) {
#if TARGET_OS_SIMULATOR
  return NO;
#else
  NSURL *appStoreReceiptURL = NSBundle.mainBundle.appStoreReceiptURL;
  NSString *appStoreReceiptLastComponent = appStoreReceiptURL.lastPathComponent;
  
  BOOL isSandboxReceipt = [appStoreReceiptLastComponent isEqualToString:@"sandboxReceipt"];
  return isSandboxReceipt;
#endif
}

BOOL bit_hasEmbeddedMobileProvision(void) {
  BOOL hasEmbeddedMobileProvision = !![[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
  return hasEmbeddedMobileProvision;
}

BOOL bit_isRunningInTestFlightEnvironment(void) {
#if TARGET_OS_SIMULATOR
  return NO;
#else
  if (bit_isAppStoreReceiptSandbox() && !bit_hasEmbeddedMobileProvision()) {
    return YES;
  }
  return NO;
#endif
}

BOOL bit_isRunningInAppStoreEnvironment(void) {
#if TARGET_OS_SIMULATOR
  return NO;
#else
  if (bit_isAppStoreReceiptSandbox() || bit_hasEmbeddedMobileProvision()) {
    return NO;
  }
  return YES;
#endif
}

BOOL bit_isRunningInAppExtension(void) {
  static BOOL isRunningInAppExtension = NO;
  static dispatch_once_t checkAppExtension;
  
  dispatch_once(&checkAppExtension, ^{
    isRunningInAppExtension = ([[[NSBundle mainBundle] executablePath] rangeOfString:@".appex/"].location != NSNotFound);
  });
  
  return isRunningInAppExtension;
}

Source: GitHub - bitstadium/HockeySDK-iOS - BITHockeyHelper.m

A possible Swift class, based on HockeyKit's class, could be:

//
//  WhereAmIRunning.swift
//  https://gist.github.com/mvarie/63455babc2d0480858da
//
//  ### Detects whether we're running in a Simulator, TestFlight Beta or App Store build ###
//
//  Based on https://github.com/bitstadium/HockeySDK-iOS/blob/develop/Classes/BITHockeyHelper.m
//  Inspired by https://stackoverflow.com/questions/18282326/how-can-i-detect-if-the-currently-running-app-was-installed-from-the-app-store
//  Created by marcantonio on 04/11/15.
//

import Foundation

class WhereAmIRunning {
    
    // MARK: Public
    
    func isRunningInTestFlightEnvironment() -> Bool{
        if isSimulator() {
            return false
        } else {
            if isAppStoreReceiptSandbox() && !hasEmbeddedMobileProvision() {
                return true
            } else {
                return false
            }
        }
    }
    
    func isRunningInAppStoreEnvironment() -> Bool {
        if isSimulator(){
            return false
        } else {
            if isAppStoreReceiptSandbox() || hasEmbeddedMobileProvision() {
                return false
            } else {
                return true
            }
        }
    }

    // MARK: Private

    private func hasEmbeddedMobileProvision() -> Bool{
        if let _ = NSBundle.mainBundle().pathForResource("embedded", ofType: "mobileprovision") {
            return true
        }
        return false
    }
    
    private func isAppStoreReceiptSandbox() -> Bool {
        if isSimulator() {
            return false
        } else {
            if let appStoreReceiptURL = NSBundle.mainBundle().appStoreReceiptURL,
                let appStoreReceiptLastComponent = appStoreReceiptURL.lastPathComponent
                where appStoreReceiptLastComponent == "sandboxReceipt" {
                    return true
            }
            return false
        }
    }
    
    private func isSimulator() -> Bool {
        #if arch(i386) || arch(x86_64)
            return true
            #else
            return false
        #endif
    }
    
}

Gist: GitHub - mvarie/WhereAmIRunning.swift

Update Dec 9th 2016:

User halileohalilei reports that "This no longer works with iOS10 and Xcode 8.". I didn't verify this, but please check the updated HockeyKit source (see function bit_currentAppEnvironment) at:

Source: GitHub - bitstadium/HockeySDK-iOS - BITHockeyHelper.m

Over time, the above class has been modified and it seems to handle iOS10 as well.

Update Oct 6th 2020:

Hockey has been deprecated/abandoned and replaced by Microsoft's AppCenter SDK.

This is their App Store / Testflight build detection class (link to repository below code):

MSUtility+Environment.h :

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#import <Foundation/Foundation.h>

#import "MSUtility.h"

/*
 * Workaround for exporting symbols from category object files.
 */
extern NSString *MSUtilityEnvironmentCategory;

/**
 *  App environment
 */
typedef NS_ENUM(NSInteger, MSEnvironment) {

  /**
   *  App has been downloaded from the AppStore.
   */
  MSEnvironmentAppStore = 0,

  /**
   *  App has been downloaded from TestFlight.
   */
  MSEnvironmentTestFlight = 1,

  /**
   *  App has been installed by some other mechanism.
   *  This could be Ad-Hoc, Enterprise, etc.
   */
  MSEnvironmentOther = 99
};

/**
 * Utility class that is used throughout the SDK.
 * Environment part.
 */
@interface MSUtility (Environment)

/**
 * Detect the environment that the app is running in.
 *
 * @return the MSEnvironment of the app.
 */
+ (MSEnvironment)currentAppEnvironment;

@end

MSUtility+Environment.m :

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#import "MSUtility+Environment.h"

/*
 * Workaround for exporting symbols from category object files.
 */
NSString *MSUtilityEnvironmentCategory;

@implementation MSUtility (Environment)

+ (MSEnvironment)currentAppEnvironment {
#if TARGET_OS_SIMULATOR || TARGET_OS_OSX || TARGET_OS_MACCATALYST
  return MSEnvironmentOther;
#else

  // MobilePovision profiles are a clear indicator for Ad-Hoc distribution.
  if ([self hasEmbeddedMobileProvision]) {
    return MSEnvironmentOther;
  }

  /**
   * TestFlight is only supported from iOS 8 onwards and as our deployment target is iOS 8, we don't have to do any checks for
   * floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1).
   */
  if ([self isAppStoreReceiptSandbox]) {
    return MSEnvironmentTestFlight;
  }

  return MSEnvironmentAppStore;
#endif
}

+ (BOOL)hasEmbeddedMobileProvision {
  BOOL hasEmbeddedMobileProvision = !![[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
  return hasEmbeddedMobileProvision;
}

+ (BOOL)isAppStoreReceiptSandbox {
#if TARGET_OS_SIMULATOR
  return NO;
#else
  if (![NSBundle.mainBundle respondsToSelector:@selector(appStoreReceiptURL)]) {
    return NO;
  }
  NSURL *appStoreReceiptURL = NSBundle.mainBundle.appStoreReceiptURL;
  NSString *appStoreReceiptLastComponent = appStoreReceiptURL.lastPathComponent;

  BOOL isSandboxReceipt = [appStoreReceiptLastComponent isEqualToString:@"sandboxReceipt"];
  return isSandboxReceipt;
#endif
}

@end

Source: GitHub - microsoft/appcenter-sdk-apple - MSUtility+Environment.m

magma
  • 8,432
  • 1
  • 35
  • 33
  • "`NSHomeDirectory()`" would return a path of "`~/iTunesMetadata.plist`", which sounds like it is *outside* the app sandbox. Are you certain this is the right path to check? – Michael Dautermann Sep 04 '13 at 19:04
  • 2
    @MichaelDautermann : good point! However in iOS NSHomeDirectory() behaves differently: "In iOS, the home directory is the application’s sandbox directory. In OS X, it is the application’s sandbox directory or the current user’s home directory (if the application is not in a sandbox)" – magma Sep 04 '13 at 22:40
  • Will try this out and verify if it works or not, thanks for the suggestion. – kevlar Sep 05 '13 at 02:19
  • @kevlar - this stopped working for me in ios8, did you see the same behavior – ekeren Jul 06 '15 at 13:58
  • @ekeren in iOS8, apps are now in a different folder, like `/private/var/mobile/Containers/Bundle/Application/4A74359F-E6CD-44C9-925D-AC82EB5EA837/whatever.app` — see updated answer – magma Jul 09 '15 at 00:36
  • 1
    Actually, it's one level above, at /private/var/mobile/Containers/Bundle/Application/4A74359F-E6CD-44C9-925D-AC82E‌​B5EA837/iTunesMetadata.plist, and unfortunately, this can't be accessed from the app (permission denied) – silyevsk Jul 13 '15 at 08:11
  • @silyevsk thank you, I have modified the answer accordingly. – magma Jul 13 '15 at 08:15
  • Does it work with xcode7 and iOS9 and for the VPP Store as well? (Both app states hidden and not hidden vpp apps?) – LoVo Jan 28 '16 at 13:21
3

If you're talking about your own app, you could add a state that returns true if it was build as part of a Store version (e.g. a compiler conditional) and false in every other case.

If you're talking about another app, it's not easy or straightforward (or maybe not even possible) to query other apps outside of your sandbox.

Michael Dautermann
  • 88,797
  • 17
  • 166
  • 215
  • Thanks for your quick response Michael. I've edited my question to specify that I'm building an SDK, so I don't have direct access to the app's code. Do you know any workaround for that? – kevlar Aug 16 '13 at 22:51
  • > If you're talking about your own app, you could add a state that returns true if it was build as part of a Store version (e.g. a compiler conditional) and false in every other case. You don't explain how, which is actually the real question here. – Fab1n May 04 '23 at 13:38
2

Since the code by @magma no longer works IOS11.1 Here is a bit of a long winded solution.

We check the app version on the app store and compare it to the version in the Bundle

static func isAppStoreVersion(completion: @escaping (Bool?, Error?) -> Void) throws -> URLSessionDataTask {
    guard let info = Bundle.main.infoDictionary,
      let currentVersion = info["CFBundleShortVersionString"] as? String,
      let identifier = info["CFBundleIdentifier"] as? String else {
        throw VersionError.invalidBundleInfo
    }
    let urlString = "https://itunes.apple.com/gb/lookup?bundleId=\(identifier)"
    guard let url = URL(string:urlString) else { throw VersionError.invalidBundleInfo }
    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
      do {
        if let error = error { throw error }
        guard let data = data else { throw VersionError.invalidResponse }
        let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any]
        guard let result = (json?["results"] as? [Any])?.first as? [String: Any], let appStoreVersion = result["version"] as? String else {
          throw VersionError.invalidResponse
        }
        completion(appStoreVersion == currentVersion, nil)
      } catch {
        completion(nil, error)
      }
    }
    task.resume()
    return task
}

Called like this

DispatchQueue.global(qos: .background).async {

    _ = try? VersionManager.isAppStoreVersion { (appStoreVersion, error) in
      if let error = error {
        print(error)
      } else if let appStoreVersion = appStoreVersion, appStoreVersion == true {
         // app store stuf
      } else {
        // other stuff

      }
    }
}

enum VersionError: Error {
    case invalidResponse, invalidBundleInfo
}
Ryan Heitner
  • 13,119
  • 6
  • 77
  • 119
  • 1
    Ok, and what will be happened when application version is not last? – General Failure Aug 15 '18 at 07:43
  • I do not really understand your question. My code just compares the bundle version to the version on the app store. – Ryan Heitner Aug 19 '18 at 10:22
  • When new version publishes in store, not all users updates it immediately (old iOS version, partically update setting in App Store Connect etc). So, user with old version will see debug-version behaviour. – General Failure Aug 28 '18 at 04:30
  • 2
    If the version on the app store is higher than the bundle version you can deduce that it is an older version not yet updated. If it is the same it is an updated version and if the bundle is higher than the app store then you are running an unreleased or debug version. – Ryan Heitner Aug 28 '18 at 07:25
  • This doesn't answer the question. Comparing versions doesn't do anything about that issue whatsoever. Context: An app can be distributed in different ways as a release version: App Store, TestFlight Beta, Firebase, dev build on device, etc. – Fab1n May 04 '23 at 13:36
-1

My observation is when a device connected to Xcode, and then we open Organiser, switch to Devices pane it will list all Applications which is not installed from App Store. So what you have to do is download Xcode, then connect your device, go to Devices pane and see which all applications are installed from non-App Store sources. This is the simplest solution.

rakeshNS
  • 4,227
  • 4
  • 28
  • 42