Modeling In-App Purchases Inventory

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.

Product Types

There are four kinds of iAP “product archetypes” in Apple’s App Stores:

  • Non-Consumable
  • Non-Renewing Subscription
  • Auto-Renewing Subscription
  • Consumable

[decorative] Hand pointing to golden shopping cart

Non-Consumable Products

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.

Non-Renewing Subscription

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.

Auto-Renewing Subscription

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).

Consumable Products

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-Consumable Once Apple Backend Eternity(+)
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.

Keeping Inventory

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.

in-app purchase product inventory class diagram

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:

  • NonConsumableProductInventoryItem
  • NonRenewingSubscriptionProductInventoryItem
  • AutoRenewingSubscriptionProductInventoryItem
  • ConsumableProductInventoryItem

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.

ProductInventoryItem Class

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.

ConsumableProductInventoryItem Class

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.

ToggleProductInventoryItem Class

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.

SubscriptionProductInventoryItem Class

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 purchaseDate and 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.

NonRenewingSubscriptionProductInventoryItem Class

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)

AutoRenewingSubscriptionProductInventoryItem Class

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)

NonConsumableProductInventoryItem Class

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 StoreKit.

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.

Next Steps

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.

Avoid App Review rules by distributing outside the Mac App Store!

Get my FREE cheat sheets to help you distribute real macOS applications directly to power users.

* indicates required

When you subscribe you’ll also get programming tips, business advices, and career rants from the trenches about twice a month. I respect your e-mail privacy.

Avoid Delays and Rejections when Submitting Your App to The Store!

Follow my FREE cheat sheets to design, develop, or even amend your app to deserve its virtual shelf space in the App Store.

* indicates required

When you subscribe you’ll also get programming tips, business advices, and career rants from the trenches about twice a month. I respect your e-mail privacy.

0 thoughts on “Modeling In-App Purchases Inventory

Leave a Reply