The popular writing app Ulysses recently moved to a subscription payment model and they extensively explained their reasoning for the transition. Switching to a subscription model is rarely a popular move, which tends to incite the masses to pull out their torches and pitchforks.
Max Seelemann, development lead for Ulysses, does an excellent job in outlining the reasons for moving over to subscription in an attempt to assuage those people who bothered to read the article before they start angrily gnashing their teeth. One thing a company does not want to do when changing their pricing model (whether it be going from paid to subscription or paid to freemium) is to anger the current customers with this change.
Ideally, those early customers should be rewarded, and they should be enticed to remain loyal customers. Punishing them with a subscription fee on top of what they had previously paid for will not win many adoring fans. If you are changing your pricing model for your app, you should check if this was a customer before the change.
So how do you determine if someone had used an earlier version of an iOS app? We'll take a look at several different approaches below.
Check the build number in the receipt
Every app downloaded from the iOS or Mac App Store has a receipt, which contains a variety of information such as the bundle identifier, any in-app purchases, the subscription expiration date, the original app version, and more. The item we are interested in is the original app version. For iOS, this is the build number (CFBundleVersion), which is not the version number of the app (e.g. 2.1.3). On macOS, the receipt returns the CFBundleShortVersionString, instead, which is the version number of the app. If the build number for each release of your iOS app is unique, this is a valid solution to determine when a user first purchased the app. However, if the build numbers are not unique (such as the build numbers are reset back to 1 after each new release), then this can become quite problematic. On macOS, the receipt will contain the app's version number (e.g. 2.1.3), which is far more straightforward in determining which version of the app was initially purchased.
As long as build number scheme has been consistent and continues to increase and each version is unique, then this is a reliable method to compare the check if the user had an early version of the app before the pricing switch.
Check by the earliest date in any IAP
If your app offers in-app purchases (IAP), iterate through them and search for the oldest item. However, if your app does not offer IAP, this is not a viable option.
On-line accounts
If available in the app, if there is a login process and proper records stored on a server, when the user logs in to the app, the server could return information on when the user first registered. This would be dependent upon if the login process had recorded such information and could return it, as well. There is also the consideration of how an account was first created, whether it was through the iOS app, a website, or via some other method (another app, in-store sign up, etc.). This option is geared more towards a larger company which may have a larger resources and can afford to have multiple channels. This may not apply to all apps.
Roll your own method
These first three approaches can work if the app is reinstalled or downloaded onto another device. If the user has been using the app on the same device for awhile, the next several approaches can be used.
The app can save previous versions of the app to the device (either to the user defaults or a database), which can be checked against the current version of the app. Unfortunately, this will not work so well if the user has deleted the app (which deletes the user data, as well) and then reinstalls the app. Same applies if the app was installed on a new device, and the old local data may not exist or get transferred.
One method to be able to check if the app had been installed on that particular device at one time is to save some data in the Keychain, instead of the user defaults, which will persist even if the original app has been deleted.
Check creation date of the app's documentation folder
A clever approach to determine when the app was installed on the particular device is to check the creation date for the app's documents folder. The same problems arises, though, if the app has been newly installed or was deleted and then reinstalled on the device. Otherwise, this is a fairly reliable method to determine when the app was installed, but it should be used as the final check to determine how long the user has been using this app.
Objective-C:
NSError *error = nil;
NSURL *documentsFolderURL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
NSDate *installDate = [[[NSFileManager defaultManager] attributesOfItemAtPath:documentsFolderURL.path error:&error] objectForKey:NSFileCreationDate];
if (error != nil) {
NSLog(@"Error retrieving install date: %@", [error localizedDescription]);
}
Swift 3:
let documentsFolderURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last!
let installDate = (try! FileManager.default.attributesOfItem(atPath: documentsFolderURL.path)[FileAttributeKey.creationDate])
January 2019 Update
In the WWDC 2018 video Best Practices and What’s New with In-App Purchases, they mentioned inspecting the Type 19 (original_application_version) attribute in your app's receipt to obtain the original application version.
From the WWDC session (emphasis mine):
So if you're paid up front and you want to move to a subscription or if you're paid up, paid up front and you want to move to a free trial use type 19. This will tell you the original version that the user purchased that app with from the App Store. Even if they delete the app and redownload it over and over again that type 19 will tell you exactly what version they bought their app with. If the user did originally pay up front make sure to give them the functionality that they bought. Just because you move to a subscription model if they bought it when it was a paid for app in the App Store you need to give them that access to that content they originally paid for.
So again use type 19 within the receipt to know what version they bought that app with.
As mentioned earlier in this post, the type 19 value corresponds to the CFBundleVersion
in an iOS app, which is the build number and not the actual version number of the app. As long as the build version is unique for each released version of the app, this is a viable solution to determine if the user has used an earlier version of the app. However, if the build number is reset back to 1 after each new version of the app, this can cause major (and unintended) problems since the build numbers may not be unique or continually increase with each successive build of the app.
The inspiration for this post originated from a problem I encountered when I was working on an iOS app for a client who unwisely decided to switch from a paid app to a subscription model, which locked out all of their previous customers until they submitted to paying for the subscription. Upon encountering the uproar of a mob of very angry customers, this company decided to back pedal a bit and allow their current customers to still access their previously paid for content without having to pay for the subscription.
When I took on this project I discovered that the previous developer who had implemented the subscription feature had reset the build number for the app back to 1, which made it difficult and confusing to determine if a customer was a grandfathered user or not. This resulted in finding a number of alternative solutions to work around this problem which have been detailed in this post. Had the client put the appropriate amount of forethought into their poor business decision, they would have allowed their current customers to keep what they had already purchased without trying to burden them with an unwanted subscription. As Apple explicitly mentioned in this session, give them that access to that content they originally paid for.
If you are a company considering going down the subscription route, tread extremely careful, ere you might anger your entire customer base with a single misguided stroke. There may be no perfect or gentle way to switch to a subscription model, but there is certainly a very wrong way to go about it by taking away what your customers have already paid for. Until Apple and Google implement a method to provide for paid upgrades, developers may be forced into the subscription route to ensure that they have a more consistent stream of income versus a one-time payment for the entire lifespan of the app.
References
- Adding In-App Purchase to Your Applications
- (iOS + StoreKit) Can I detect when I'm in the sandbox?
- Trouble getting the original app version that the user installed (receipt validation)?
- iOS7 receipt contains CFBundleVersion instead of CFBundleShortVersionString
- Convert existing iOS paid app to freemium model with in-app purchase
- How to test app store purchase receipt to obtain original application version
- How to determine the date an app is installed or used for the first time?
- Determine if previously installed app
- Using RMStore library to get originalAppVersion
- Detect previous paid version of app
- Changing paid app to freemium (existing users)
- Ulysses Switches to Subscription
- Why we’re switching Ulysses to Subscription
- Using Store Kit for In-App Purchases with Swift 3
- Preventing Unauthorized Purchases with Receipts
- Using Receipts to Protect Your Digital Sales
- Best Practices and What’s New with In-App Purchases
- Receipt Validation Programming Guide
- Receipt Fields