This post has been updated to include the recent additions in Xcode 12 dedicated to In-App purchases testing.
One of the In-App purchases implementation steps that every developer eventually faces is a choice of a suitable purchase record persistence strategy which will make possible to give access to paid content after app relaunch.
The most obvious way of persisting the purchase is to store some value in
UserDefaults that will indicate whether or not some content has been unlocked. Such approach is shown in two tutorials at raywenderlich.com written by Owen Brown and Pietro Rea, where
UserDefaults are used to store a boolean flag to indicate non-consumable products purchase status as well as
Date instance to persist the subscription expiration date. Such a solution is often criticized due to lack of data integrity protection, as content present in
UserDefaults is stored as an ordinary binary plist in the Preferences folder of app's bundle and thus can be edited by the user using software like iMazing, iFunBox or iExplorer as shown by Andrés Ibañez in his 2014 post.
While this is true, I would like to mention that since iOS 8.3 Apple introduced serious permission restrictions in file system access, which eliminated access to application’s internal files from outside on non-jailbroken devices. Speaking about Jailbreak, it is worth reminding that its popularity has been decreasing over last years to the point that its pioneers say that jailbreaking is dead. From personal observations, I can say that it takes more and more effort to release an untethered jailbreak for the newest iOS version. Starting with iOS 10 it takes at least 6 months for hackers to release a jailbreak, which can be used by an average iOS user. It is why I think that the chances are pretty low that any significant portion of the app’s users would tamper with app’s files.
Still, it doesn’t take much effort to use a
Keychain instead of
UserDefaults as shown by Axel Kee in his post. The principle is the same — store a string like “purchased” for identifiers of purchased products. It is worth mentioning that in iOS 10.3 betas, Apple started to remove all app-related records in Keychain after app uninstallation. However, as it turned out, many people relied on the previous behavior of Keychain even though it wasn’t documented, and Apple had to revert this change in iOS 11+. The bottom line is that if you want to persist In-App purchases between app installations, you can use the Keychain, but Apple may change its behavior once again in the future.
There is, however, an alternative approach to check if a user has purchased something without relying on
Keychain by using a receipt, which is automatically updated by the OS on every purchase. Even though the idea of receipt usage for this purpose is clearly documented in Apple’s In-App Purchase Programming Guide, it seems like this approach lacks attention in 3rd party tutorials. One reason for that might be that this persistence strategy is applicable only for non-consumable products and auto-renewable subscriptions. If you are implementing either consumable or non-renewing subscriptions, then you are left with Keychain, iCloud, or your own backend. The second obstacle of app receipt usage is its complex structure that requires a good amount of tricks to read the necessary information. The tutorial by Bill Morefield does a good job in showcasing what it takes to read an app receipt in Swift without 3rd party dependencies. Thankfully, folks from Cocoanetics created a Kvitto library, which makes work with an app receipt a breathe. Alternatively, you can also use a TPInAppReceipt library by Pavel Tikhonenko for the same purpose.
Recently I successfully implemented both non-consumable purchase and auto-renewable subscription in my app called Electronics Engineer Helper using app receipt as a persistent record of the purchase. The method of
InAppPurchaseManager that reads the receipt looks like this:
The method simply parses receipt to find relevant entries by comparing the product identifier and uses tuples inside of the switch statement along with ternary operations to return appropriate purchases status.
Another method worth checking out is a
In this method I simply forward events to
BehaviorRelay (a class from RxSwift library) and read purchase status from app receipt when all transactions have been handled.
In Xcode 12, Apple introduced a new In-App purchases environment called Xcode and StoreKit configuration files. By adding such a configuration (File > New > File … > StoreKit Configuration File) you can now test In-App purchases both in a simulator and physical device without resorting to workarounds like specifying purchases status via the environment variable.
What’s also great with StoreKit testing is that not only your code is called in exactly the same way as in the production environment, but you also see a production-like purchasing dialogs.
With a new Transactions Manager, you can easily remove purchases from the receipt and simulate edge cases like interrupted or deferred purchases.
You can check out the full code of my
InAppPurchaseManager in the gist. By leveraging
Kvitto, and app receipt an implementation of In-App purchases didn't cause much trouble.
If you enjoyed this post, please check out my app in the AppStore, and thank’s for reading.