In-App Purchases (iAP) is a way to sell digital products inside an application. As far as Apple’s App Stores are concerned, apps would need to use the platforms’ iAP to sell digital services and products, unless if the user purchased those digital items elsewhere.
There are four kinds of iAP “product archetypes” in Apple’s App Stores:
- Non-Renewing Subscription
- Auto-Renewing Subscription
This iAP type is primarily for enabling a feature in the application. Once purchased, the corresponding feature should be available for the lifetime of the app — even if the user erased the app and re-install it in the future. In which case the app would need to provide a functionality to restore these purchases.
Interestingly, this iAP type can also be used to offer free trials. Quoting App Review Guidelines section 3.1.1,
… apps may offer a free time-based trial period before presenting a full unlock option by setting up a Non-Consumable IAP item at Price Tier 0 that follows the naming convention: “XX-day Trial.” …
In other words the app has a pair of two non-consumable iAPs, one for the “trial” at $0 and the other one for the real functionality of the app. What’s peculiar is that the $0 iAP can “expire” – the corresponding functionality is no longer enabled after a certain date past the purchase date. OmniGroup (makers of OmniGraffle, OmniFocus, and other productivity apps) uses this method to implement trial versions of its App Store apps.
I’ve also seen non-consumable iAP used to provide upgrade pricing. In this scenario, the iAP is only made available for purchase in the presence of an older version of the app. Presumably the app being upgraded verifies the receipt (or iAP) of the older app, and enable the “special discount” iAP should the validation passes. If the user doesn’t have the pre-requisite app installed, then there is another iAP for unlocking the same set of features at full price.
A non-consumable iAP can only be purchased once. This is the main difference with other types of purchases, which can be purchased multiple times.
A subscription iAP has an “end date” – the corresponding functionality would only be available for a certain period from the start of the purchase. Similar to non-consumable, this iAP also enables a feature. However after the subscription expires then you would also need to disable the corresponding feature.
A non-renewing subscription would need to be re-purchased for the user to keep on using it. Think of this iAP akin to a day pass to an amusement pass. The user buys access to a certain functionality but only for a limited time period.
Note that the App Store only provides the start date of the purchase inside the receipt of a non-renewing subscription iAP. It is up to the app to honor the subscription’s duration and make the corresponding functionality available within that time window. Similarly the app is also responsible for removing access to the functionality.
An Auto-Renewing subscription would charge the user again at the end of each period. It essentially repurchase itself when the subscription expires. The user can only start an auto-renewing subscription inside your app. There is no API to stop the subscription; users would need to go to the App Store app to cancel renewal of a subscription — or let it lapse by invalidating the credit card associated with the purchase.
Auto-renewing subscriptions would need to provide ongoing value to get approved by App Review. The criteria for this ongoing value is a bit vague, but centers on continuously-updated media content or cloud support (Section 3.1.2a of the App Review Guidelines). Apple did mention software-as-a-service (SaaS) as an allowable criteria, but the app review team seems to associate SaaS as “having cloud support” and unlikely to pass any apps claiming to be SaaS but lacking some kind of Internet-based synchronization or content update functionality coming from the vendor itself (i.e having iCloud sync doesn’t qualify as SaaS).
These are often encountered in games, as consumable products typically translates to in-game currency — be it points, coins, or credits. However Apple does not restrict its use to games. Thus for example if you make a greeting card app, you can use it to base user purchases on the number of greeting cards that the app can print — which in essence a result-based software-as-a-service application.
Consumable products put significant demands on the software architecture. For one it often needs a server (or backend) component which keeps records of users’ purchases and consumptions. Furthermore, the app would need to convey consumptions to that backend — which ideally includes measures to avoid reporting duplicated consumption records, or otherwise getting angry customers.
The primary reason for the need of a server is that Apple does not provide a history of consumable products receipts beyond the initial fulfillment. Likely because consumption is mostly business logic — which includes the way to identify individual consumption — hence would likely have too many variations to be covered by the platform. Besides, it’s very hard to secure consumable purchases against tampering when stored in the user’s device.
The second reason is that credits or “in-game currencies” may not expire, as per App Review Guidelines. Also the app would need to provide a restore purchases functionality just like a non-consumable product. All the more reason to store the master copy of this data in a backend.
Overview of iAP Types
Here is a table summarizing the four iAP types and the characteristics of each.
|iAP Type||Buy Count||Source of Truth||Validity|
|Non-Renewing Subscription||Multiple||Apple Backend||Until expiry|
|Auto-Renewing Subscription||Multiple||Apple Backend||Until canceled|
|Consumable||Multiple||Your Backend||Until consumed|
(+) Except for “non-consumable $0 trials”, which is allowed to expire.
When your app uses more than one kind of iAP, it would be quite challenging to keep the user’s inventory of purchases. These four iAP types have overlapping functionalities that are not shared equally. Whereas each have their own unique features. How can you organize all of these the right way?
Wouldn’t it be great if there is some sort of template that you can follow to implement all of your apps’ entire inventory of in-app purchases?
Why, you’re in luck. I’ve recently done a shared component to manage in-app purchase inventories of my apps and can share you what Iv’e learned. The component manages iAP items that have been purchased, can be purchased, or for the case of consumables, how many does the user has on-hand. It centralizes all the “toggles” that iAP makes so that the rest of the application does not need to be aware of
StoreKit. Better yet, it completely abstracts away non-consumable and subscription iAPs so that components implementing the corresponding feature doesn’t need to know what kind of iAP it is – only whether it is currently enabled or not.
The following class diagram depicts the data model of iAP inventory given a particular application for a user.
Note that these classes are mainly for expressing the in-memory values of the inventory. The corresponding data would need to be read by something else and then loaded into instances of these classes — more about that later on.
The four types of in-app purchases are represented by the four leaf classes:
Superclasses of those serves to factor out the common functionalities and attributes. Let’s take a visit at each class starting at the base and sweep downwards.
This base class contains attributes common to all in-app purchases. At a minimum, it must contain the product identifier of the iAP – the same string that you use to ask
SKProductsRequest for the localized description and price, among other things. Another common attribute is the last transaction date*, which would correspond to the purchase date for most iAP types – except for auto-renewing subscription, which would be the latest renewal date.
Instances of this would manage products of the consumable iAP type. Objects would contain the current amount of products that the user has in-hand. It subclasses
ProductInventoryItem, inheriting all of its attributes and methods.
This is the base class for all other iAP inventory classes. Apart from consumables, all other iAP are essentially toggles a feature on or off – activates a feature by purchasing it. Consequentially subscriptions that lapsed would de-activate its corresponding feature. Non-consumables can also be cancelled – should the user contacts Apple and asked for a re-fund on the iAP – in which case the corresponding features would need to be revoked as well.
By having a common base class, hence a common interface, the application components implementing the features to be enabled by iAP would not need to care what the concrete type is. Simply ask for the product inventory instance, check that it is a toggle iAP, and use the
isEnabled flag to see whether to enable the feature and/or let the user proceed with a certain workflow. Those other components would not need to care whether the iAP is a non consumable or one of the two subscription types.
Additionally the other useful common attributes would probably be the purchase date and cancelation date. These would be useful to display on the app’s iAP screens.
Common to all subscription iAP is an expiry date. This is the timestamp when the purchase will not be valid any more, and the corresponding functionality would need to be revoked.
Because of subscription iAPs may have multiple purchases, the
cancellationDate fields of
ToggleProductInventoryItem is initialized using the most recent purchase in the iAP. These fields would be useful for subclasses to determine whether the subscription is still valid.
A non-renewing subscription iAP would need to manage its expiry date by its own. That is the duration of the subscription would need to come from the app itself and not something that can be configured from App Store Connect. Therefore
StoreKit won’t return an expiry date for this iAP type – the app would need to deduce this on its own based on the purchase date as stated in the receipt.
expiryDate = purchaseDate != nil ? purchaseDate! + subscriptionDuration : nil isEnabled = cancellationDate == nil && (expiryDate != nil && expiryDate!.timeIntervalSinceNow > 0)
An auto-renewing subscription would consists of multiple purchases in the receipt file, one for every renewal. Accordingly the most relevant dates for display purposes would likely be the first purchase date and the latest purchase date. At each renewal,
StoreKit would create another purchase entry with an expiry date. The expiry date is present in the receipt since the subscription duration is configured in App Store connect.
The logic for determining whether an auto-renewing iAP is in an active state simply to ensure that the expiry date is in the future.
isEnabled = (expiryDate != nil && expiryDate!.timeIntervalSinceNow > 0)
This class doesn’t add much to its superclass. However non-consumable products are presented differently that subscription products. Specifically the legal text that needs to be shown in the iAP screens would be different. Therefore this class can provide those information that are specific to non-consumable items.
Finding out whether a non-consumable product is valid (hence enabled) is just a matter of making sure that it is purchased but not cancelled.
isEnabled = cancellationDate == nil && purchaseDate != nil
However if you use a non-consumable iAP as a mechanism to provide free trial periods, then this class would be a likely place to handle such functionality. That is, for a “free trial” non-consumable iAP, the
isEnabled flag would become
false if the current date has passed the trial period which started at the time the iAP was purchased.
Getting the Data
Purchase data for all iAP types except for consumables can be read from the app’s receipt file. This file is maintained by the App Store background processes. It is cryptographically signed by Apple and would be unique for each device and Apple ID combination. Usually the file is read and validated on startup to get the user’s iAP inventory.
For everything else but consumables, during a purchase or refresh transactions workflow, the app would nee to refresh the user’s inventory from whatever returned by
StoreKit. When a purchase occurs, the receipt is not yet updated to contain details on the newly purchased (or renewed) product, thus just take the details from the
SKProductsResponse object. Similarly the user chose to restore purchases likely due to a missing or broken receipt file (or the user moved computers or restore the app from a backup), thus just take the inventory data from the callbacks invoked by
Consumable iAP balances would need to be requested from your server. Consequentially, consumption of the iAP would need to be relayed to the server as soon as possible. Since the app only maintains the balance in-memory, there is less chance of tampering this balance by the user.
Take note that even though the iAP receipt contains fields for consumables, the data would not be persistent. Consumable receipts would be overwritten on the next purchase or should the user refresh the receipt.
Having the basic design for a shared iAP “inventory manager” component, now you would need to devise what would be the methods required for these classes. This would likely depend on supporting your existing portfolios of apps, or how would you design the iAP “purchasing experience” for a new app. Similarly, how would you validate the iAP receipts and how would you synchronize consumable purchases and the following consumptions. These would determine what needs to be provided by the underlying data model layer.
Some starter ideas of considerations for implementing the component:
- Auto-renewing subscriptions has trial period support built-in to the system whereas non-renewing subscriptions do not.
- Subscription products calls for mandatory legal texts (App Review requirement) which are different between auto-renewing and non-renewing.
- Free trials for non-consumables are done by pairing two of these together, a “$0 trial” non-consumable iAP with a corresponding iAP that is purchased at-cost.
- Upgrade from an older app, having a non-consumable item that would only be available for purchase if an older version of the app is present and the corresponding iAP of the older app is purchased. This includes validating receipts of another app.
0 thoughts on “Modeling In-App Purchases Inventory”
You must log in to post a comment.