A Guide to iOS Introductory Prices
New tools and new burdens for subscription app developers.
In iOS 11.2 Apple introduced yet another improvement to the in-app subscriptions system on iOS: introductory prices. Introductory prices are an excellent addition to the iOS developer’s tool belt, however, Apple was sure to add in some of their signature pointless burden. I wrote this guide to illustrate some of the possibilities and to help navigate the unnecessarily difficult.
Update: As of iOS 12.0,
SKProduct now includes a
subscriptionGroupIdentifier property, so it’s now possible to compute Introductory Pricing eligibility locally. More details are available at the bottom of this post.
Update 2: It looks like Apple finally listened and introduced a new method called
isEligibleForIntroOffer, which allows you to bypass the implementation pains and get the value directly from StoreKit. The bad news: it’s only compatible with iOS 15+, tvOS 15+, watchOS 8+, macOS 12+. If your app targets older OS versions, keep reading to see how to implement eligibility checks. To perform the checks with the new method, you:
- Get the products by calling
- For a given product, check
Introductory Price Types
There are three types of introductory price available: Pay as you go, Pay up front, and Free trial. Each one provides a different buyer’s experience.
Pay as you go
As its name hints, Pay as you go charges users a reduced rate for some number of renewal periods before increasing to the regular price.
To configure a product for Pay as you go you need to specify the price and the number of periods via iTunes Connect. The introductory price must be less than the regular price of the subscription. The available number of periods differ depending on the duration of the subscription.
Intro period durations must be the same as their regular product durations; you can’t have a one-week Pay as you go period that leads to a monthly subscription. The maximum duration for an entire introductory period is one year, except in the case of a one-week subscription where the maximum is 12 weeks.
Pay up front
Rather than providing a lower price for multiple renewal periods, Pay up front charges any price for one period of a specified duration before the regular price takes over.
Pay up front has two parameters the developer can modify: the price and the duration. Unlike a Pay as you go price, which must be less than the regular price, Pay up front periods can use any price. This means you could use intro prices to do something like charge a one time high cost period, followed by short renewals.
I think the most viable use of Pay up front is something like the Comcast model: 1 year up front at a reduced rate, then month-to-month after that.
Ah, our good old friend Free trial. It has been around for a while now but, in a good move to combat API entropy, Apple has made the current Free trial one of the new introductory price types. The behavior mimics Pay up front, but with a $0.00 price and some additionally available durations: three days, one week, and two weeks. The old API for Free trial still exists, but I am sure Apple will deprecate it in the not too distant future.
Introductory Pricing StoreKit APIs
The new introductory pricing requires support on the iOS side to function. You must display to a user the introductory price and details about the offer in your user interface. To do this, Apple added an optional introductoryPrice property to SKProduct in iOS 11.2. If a product has an introductory price, this property will be non-nil.
SKProduct.introductoryPrice returns an object of a new class, SKProductDiscount, that contains all the information specified when configuring the intro price:
- price and priceLocale, used for presenting the price to the user
- paymentMode enum that is either payAsYouGo, payUpFront, or freeTrial
- numberOfPeriods that is used for payAsYouGo to tell the user the number of periods before the regular price kicks in, always 1 for payUpFront and freeTrial
- subscriptionPeriod that specifies the duration of a subscription period, i.e. 1 month, 2 months etc.
This data should be used in your payment flow to display the introductory pricing details to the user. Just like prices, you can’t rely on hardcoded knowledge of the products in your app (beyond the product identifiers). This data must be fetched from StoreKit before it can be displayed.
Determining introductory price eligibility
Here be dragons. Once a user completes an introductory pricing period in a subscription group, they are no longer eligible for introductory pricing for any other product in that same group.
When a user initiates a purchase, the App Store system UI will pop-up and present the correct price, either the intro or regular price. However, Apple did not provide us with this same sacred knowledge. We have to figure this out on our own in order to present correct prices to our users.
Apple’s instructions for this seem simple enough:
But there is actually a fair bit to unwrap here.
You now have to validate a receipt before a user can see a product’s price.
Validating first is complicated. It requires you to possibly refresh the receipt, which can trigger an App Store login prompt, and to do the actual receipt validation either locally or by sending it to a service. Verifying the receipt before a product is even visible adds yet another thing for developers to screw up. (Which we are known to do from time to time.)
Once you have parsed and validated the receipt, you can see what products the user has purchased. From there you know for what products an introductory period has been completed and can then determine for which subscription groups introductory pricing is unavailable. To compute the eligibility with certainty also requires knowing to which subscription group every product belongs. Neither the /verifyReceipt response nor SKProduct will tell you the group for a subscription product. You will now have to have to store both the group and the product in your app or on your backend to verify eligibility correctly.
Why does it have to be so hard?
All of this added complexity increases the time it takes to show the user a product’s price when they land on the purchase page of your app. You now need to:
- Read, and possibly refresh, the App Store receipt
- Verify the App Store receipt with Apple
- Fetch product info using an SKProductsRequest
This will add 1, maybe 2, additional remote calls and possibly add seconds to the time it takes to display products to your users. Time spent loading is sales lost.
It doesn’t have to be like this
The system purchasing UI always shows the correct price when initiating a purchase, so we know that they know the correct price. Why didn’t they just give us that information? SKProduct.isEligibleForIntroductoryPricing is all we needed.
I suspect it comes down to some combination of internal politics and technical debt but, it’s crazy to me that such an API shortcoming would ship. The developer experience around StoreKit and IAPs is already so broken, and this just adds one more thing to a developer’s plate. Neglect of the developer experience runs downhill. A small increase in complexity for the developer leads to a large increase in poor user experiences.
Looking for a way out?
I wouldn’t be a good salesman if I didn’t also have a solution to the problem. RevenueCat just added support for introductory pricing and along with that we also added support for checking intro pricing eligibility to the RevenueCat iOS SDK. We have been able to condense it down to one call that makes the process smoother for developers.
How to make introductory prices great
Introductory prices are a welcome addition to the in-app subscriptions ecosystem. The API is well designed and sensible, and the available product configurations are well thought out and very flexible. We just need a couple of things to make them great:
- Add an isEligibleForIntroPrices method or property to StoreKit
- Or, add a product’s group to SKProduct or the /verifyReceipt response.
Either of these would go a long way to making sure implementations of introductory prices do not create a substandard iOS user experience.
As of iOS 12.0, `SKProduct` now includes `subscriptionGroupIdentifier` property, so it’s now possible to compute Introductory Pricing eligibility locally.
While this update is great, calculating intro eligibility locally still requires a lot of unnecessary dev work.
To check intro eligibility on iOS >= 12.0, you need to:
- Refresh the receipt so you have updated information about the user’s purchases
- Parse the receipt and look for purchases made with introductory pricing
- Transform the identifiers for purchases made with intro pricing into `SKProducts`
- Transform the productIdentifiers you want to check eligibility for into `SKProducts`
- Check if there’s a match – if there’s at least one purchase made using intro pricing for the same product identifier or the same subscription group, the user is not eligible. If none are found, they’re eligible.
You will also need to be able to fall back to a backup solution that uses a server if the user’s OS is older than iOS 12.0.
Our hope is that Apple will one day add an `isEligibleForIntroPrices` property to StoreKit, which would make this feature very easy for developers to use.
It looks like Apple finally listened and introduced a new method called
isEligibleForIntroOffer, which allows you to bypass the implementation pains and get the value directly from StoreKit. The bad news: it’s only compatible with iOS 15+, tvOS 15+, watchOS 8+, macOS 12+. If your app targets older OS versions, you will still have to resort to the old, convoluted way of performing these checks. Or let us do it for you! Our
purchases-ios SDK version 4 automatically determines which version of introductory eligibility calculation to use depending on OS version availability.