How to get the original user-installed version of your app with Swift
Migrating your pricing strategy and don’t want to ruin it for your existing users? Learn how!
If you’ve started with a free app in the App Store, and then you want to make some part of it paid, or if you’ve started with a paid app and want to move it to a subscription option with a free trial, the very important thing is to make right by the users who have already installed your app before.
If you’ve suddenly stopped providing functionality that your users already paid for, or that they were getting before for free, it can badly damage your reputation, not to mention that it’s just a wrong thing to do.
The solution is to check which was the first version of your app that the user installed previously, and thus if they’re “eligible” to some of the perks that you are now trying to put behind a paywall. Perhaps there are other reasons you might want to know the originally installed version of the app too.
There is actually a very straightforward way to get exactly that information, and super reliably, including if the user deleted and reinstalled your app multiple times (you can still know the first version they ever installed).
For that, you just need to take a look at the App Store Receipt that’s included in your app’s bundle. This article by RevenueCat provides a good overview of it if you’re unfamiliar with the concept.
Parsing the App Store Receipt
Getting the useful information out of the NSBundle.appStoreReceiptURL
is not easy, as the file itself is a PKCS#7 container.
I won’t go into details about how one would extract the information on their own. It’s not a trivial task (again, the RevenueCat article outlines that). Instead, I’ll direct you to the amazing AppReceiptValidator Swift library by IdeasOnCanvas.
After you add the library to your project with SwiftPM, this is the only thing you’ll need to do:
import AppReceiptValidator
func getOriginalBuildNumber() -> String? {
let receiptValidator = AppReceiptValidator()
let appStoreReceipt = try? receiptValidator.parseReceipt(origin: .installedInMainBundle)
return appStoreReceipt?.originalAppVersion
}
But, if we’re getting the originalAppVersion
from the receipt, why did I call the function getOriginalBuildNumber
?
That’s because, according to Apple’s documentation:
Original Application Version
This corresponds to the value ofCFBundleVersion
(in iOS) orCFBundleShortVersionString
(in macOS) in theInfo.plist
file when the purchase was originally made.
And CFBundleVersion
is what you would see as a “build number” in Xcode as opposed to “marketing version”. So, if you’re making an iOS app, “original app version” will actually be “original build number”.
So, there are two important things to note about this:
- Remember that this number is the build number, not the marketing version
- If you’re using Xcode’s “Manage Version and Build Number” feature, you should know that the build number will get reset with each new version (so if you go from let’s say version 1.0 with build number 5 to version 1.1, next build will have build number 1). Because of this, “original build number” actually becomes absolutely useless.
In that case, you have two options.
First, you can stop using the “Manage Version and Build Number” feature, and make sure that your build numbers are not getting reset to zero with each new version. Build numbers should be unique and only go in ascending order. Otherwise, you cannot use App Store Receipt to figure out when the user first installed your app.
If you cannot achieve that, you’ll have to use original_purchase_date
, which you can only get by making a HTTP request with the App Store Receipt to Apple’s verifyReceipt
endpoint. However, as that endpoint is now deprecated, perhaps using consecutive build numbers is the only way forward.
Sadly, using receipt_creation_date
from the local App Store Receipt is not a solution either, as that date is reset when the app is deleted and reinstalled.
Comparing build numbers
So, the getOriginalBuildNumber()
function will return a build number.
Keep in mind that for TestFlight builds, the returned value will always be nil
, and for Debug builds — it’s either gonna be “1.0”
or also nil
(I’ve seen both).
Now, comparing any kind of version strings (including build numbers) is easy:
func isBuildNumber(_ lhs: String, earlierThanBuildNumber rhs: String) -> Bool {
rhs.compare(lhs, options: .numeric) == .orderedDescending
}
let newPricingBuildNumber = "15"
let originalBuildNumber = getOriginalBuildNumber()
if let originalBuildNumber, isBuildNumber(originalBuildNumber, earlierThanBuildNumber: newPricingBuildNumber) {
// user installed the app before build number 15
} else {
// user installed the app after build number 15
}
Thanks for reading! If you want to support me, please check out my apps: “Ask Yourself Everyday”, “Time and Again” & “Learn Numbers: Foreign Languages”. For business inquiries, please reach out to me at oleg@dreyman.dev. Thanks for reading!