How to use StoreKit views to build a subscription app paywall with SwiftUI
A guide on Apple’s new StoreView, ProductView, and SubscriptionStoreView APIs for building native paywalls for your subscription app.
The sample project used throughout this guide can be found at https://github.com/RevenueCat-Samples/storekit-views-demo-app.
At WWDC 2023 Apple introduced StoreKit views, a set of new APIs for rendering paywalls in your app using SwiftUI. These new APIs, available starting with iOS 17, enable developers to get a simple paywall up and running in their app quickly with some customization options to help them blend into your app.
In this guide, I’ll cover the three new StoreKit views, StoreView
, ProductView
, and SubscriptionStoreView
, and show how to hook them up to your In App Purchases and integrate them into your app. I’ll also cover some of the tradeoffs of using these new APIs. To follow along and see all of this code in action, check out our StoreKit views Sample App.
NOTE: If you’re new to StoreKit, I highly recommend reading our StoreKit 2 tutorial. It will walk you through setting up App Store Connect, StoreKit configuration files, and how to handle purchase events for unlocking your in-app features.
For all of the examples in this guide I am assuming these products are set up in App Store Connect (for our sample project these products are set up in a local StoreKitConfiguration file).
Auto-Renewable Subscription
- Group: Pro
- Products:
- “pro_weekly”
- $0.49 / week
- “pro_monthly”
- $0.99 / month
- “pro_yearly”
- $12.99 / year
- Includes 1 week free trial
- “pro_weekly”
- Products:
In-App Purchases
- “pro_lifetime” – $89.99
How to use StoreView
Let’s start with StoreView. This is the most straightforward of the new StoreKit views. Here’s a simple code example.
1let productIds = ["pro_weekly", "pro_monthly", "pro_yearly", "pro_lifetime"]
2StoreView(ids: productIds)
With just this code, you can render a paywall showing each of your in-app purchases that looks like this.
This renders each of our auto-renewing subscription products as well as our one in-app purchase product in a simple list with a button for each that initiates a purchase.
If you have already made a purchase, StoreView
will automatically show that product as purchased and disable the button. If you are subscribed to an auto-renewing plan and tap on a different plan within that subscription group, it will allow you to upgrade or downgrade and iOS’s built-in in-app purchase flow will explain how that billing works.
For a lifetime in-app purchase option outside of the subscription group however, the StoreView
will not automatically cancel a user’s subscription if they purchase your lifetime option. So if you want to support that you’ll have to explain to your users to do that manually.
Out of the box this is pretty boring, so let’s spruce this up by adding an image for each product. You simply return a SwiftUI view in the StoreView
’s callback and it will render it for each product. I’ve created a simple SwiftUI view called ProductImage
that takes in a product id and renders a unique image for each that I’ll use here.
1let productIds = ["pro_weekly", "pro_monthly", "pro_yearly", "pro_lifetime"]
2
3StoreView(ids: productIds) { product in
4 ProductImage(productId: product.id)
5}
Which renders this:
You can also add a button for users to restore their purchase using the storeButton
modifier.
1let productIds = ["pro_weekly", "pro_monthly", "pro_yearly", "pro_lifetime"]
2
3StoreView(ids: productIds) { product in
4 ProductImage(productId: product.id)
5}
6.storeButton(.visible, for: .restorePurchases)
Which renders like this:
Each of these products look a little chunky, so one thing we can do is set the productViewStyle
to compact. There are 3 different productViewStyles you can set, compact
, regular
, and large
.
1let productIds = ["pro_weekly", "pro_monthly", "pro_yearly", "pro_lifetime"]
2
3StoreView(ids: productIds) { product in
4 ProductImage(productId: product.id)
5}
6.productViewStyle(.compact)
7.storeButton(.visible, for: .restorePurchases)
In the current Xcode beta as of writing (beta 6), there doesn’t appear to be a way to disable the close button on the top right corner. According to Apple’s documentation you should be able to hide this by adding a storeButton(.hidden, for: .close) modifier to your StoreView
, but that API does not appear to be working in the current beta.
As far as other customizations, you can use the background
and foregroundColor
modifiers to tweak the look a little bit, but things are fairly limited. For a little bit more control, let’s check out ProductView
.
How to use ProductView
ProductView is a SwiftUI view that renders a single StoreKit product. It’s basically the building block that StoreView
is using to show your whole paywall. Using ProductView
you can make a paywall that’s much more customized to fit your app.
Each ProductView
takes a single productId and renders a product just like you saw in the StoreView
. And just like the StoreView
, you can customize the style into 3 different types: compact
, regular
, and large
. You can also add an image by passing a SwiftUI view into the ProductView
callback.
1ProductView(id: "pro_monthly") {
2 ProductImage(productId: "pro_monthly")
3}
4.productViewStyle(.compact)
This renders a functional, though pretty boring, view for purchasing an individual product.
To spruce things up you can add a background with rounded corners, some padding, and tweak the foreground color like this.
1ProductView(id: “pro_monthly”) {
2 ProductImage(productId: “pro_monthly”)
3}
4.padding()
5.background(.thinMaterial, in: .rect(cornerRadius: 20))
6.productViewStyle(.compact)
7.foregroundColor(.black)
8.padding(.horizontal)
Now that you’re controlling each element separately you can mix and match different productViewStyle
’s within a custom SwiftUI view to build a much more custom paywall.
There are a few drawbacks to this approach. ProductView
will not handle rendering the Restore Purchases button for you like StoreView
will, so you’ll have to render that on your own. You can refer to our StoreKit 2 guide or look at this guide’s sample project for an example of how to do that.
ProductView
also behaves the same way as StoreView
when it comes to mixing auto-renewing subscriptions with one time in-app purchases. Upgrades and downgrades between auto-renewing subscriptions will work automatically, but users will be able to purchase a lifetime in-app purchase and the system will not automatically cancel their subscription.
How to use SubscriptionStoreView
This brings us to the last new StoreKit view, SubscriptionStoreView
. This SwiftUI view is specifically tailored for rendering a paywall for a single subscription group set up in App Store Connect. This means in our example, it will not render our lifetime in-app purchase product at all. If you want to build a paywall that mixes auto-renewing subscriptions and in-app purchases, this is not the view for you.
If you are sticking with just a single auto-renewing subscription group however, SubscriptionStoreView
is more powerful than StoreView
or ProductView
.
Let’s start with the most basic implementation. You can initialize a SubscriptionStoreView
with a subscription group id, or a list of product ids if you don’t want to include all of the products in a group for some reason.
1SubscriptionStoreView(groupID: "GROUP-ID")
This one line of code will render this view.
There’s actually a lot going on here. Starting from the top, it renders your app icon with the name of your subscription group, in this case the excitingly named “Pro”.
Next it shows each of the products in your subscription group as selectable elements. The yearly product picks up that there is a 1 week trial and renders that as well.
At the bottom we have a clear CTA button that updates based on the user’s selection. If the user is eligible for a trial for the selected product, it shows “Try It Free”. Otherwise it shows “Subscribe”. The text above the button also updates based on the selection with the appropriate description for how billing works for the selected plan. This is an area that’s frequently rejected by App Review for stylistic reasons so presumably the choices Apple is making here should make it less likely to get a rejection.
If the user has already subscribed to a product within the group, SubscriptionStoreView
gives it a clear badge and still allows the user to upgrade or downgrade their plan.
If you want to change the copy of the CTA button you can choose between a few options using the subscriptionStoreButtonLabel
modifier. The current options are action
, displayName
, price
, multiline
, and singleline
. Apple is clear to point out that these options are preferences and are not guaranteed to behave the same way on all platforms.
Similar to StoreView
, you can add a “Restore Purchases” button at the bottom with the storeButton
modifier. You can also add a “Redeem Code” button this way on the SubsctiptionStoreView
. Additionally, you can add links to your Privacy Policy and Terms of use using the subscriptionStorePolicyDestination
modifier.
1SubscriptionStoreView(groupID: PurchaseManager.subscriptionGroupId)
2 .subscriptionStoreButtonLabel(.multiline)
3 .storeButton(.visible, for: .restorePurchases)
4 .storeButton(.visible, for: .redeemCode)
5 .subscriptionStorePolicyDestination(url: privacyUrl, for: .privacyPolicy)
6 .subscriptionStorePolicyDestination(url: termsUrl, for: .termsOfService)
One final functional element before getting into styling is visibleRelationships
. When initializing your SubscriptionStoreView
you can set the visibleRelationships
parameter to crossgrade
, upgrade
, or all
. This will change which products are presented to the user if they are already subscribed to another product in your subscription group. For more information on subscription groups you can read iOS Subscription Groups Explained.
Ok, so you have a subscription paywall that’s functionally complete, but… well it looks pretty bland. Let’s look at the ways we can customize its appearance.
Just like StoreView
and ProductView
, You can add an image for each product using the subscriptionStoreControlIcon
. It renders a bit smaller though so you may want to lean more towards a smaller icon as the modifier name suggests.
1SubscriptionStoreView(groupID: PurchaseManager.subscriptionGroupId)
2 .subscriptionStoreControlIcon { product, subscriptionInfo in
3 ProductImage(productId: product.id)
4 .frame(height: 18)
5 }
The marketing content at the top looks pretty rough. Fortunately we can pass in a SwiftUI view to make this look however we want. We do this by passing a view into the SubscriptionStoreView
’s callback.
1SubscriptionStoreView(groupID: PurchaseManager.subscriptionGroupId) {
2 VStack(spacing: 16) {
3 Image(systemName: "dollarsign.square.fill")
4 .resizable()
5 .scaledToFit()
6 .foregroundColor(.green)
7 .frame(height: 100)
8
9 Text("Subscribe to Pro!")
10 .font(.title)
11 .bold()
12
13 Text("Please buy me 🙏")
14 .font(.subheadline.weight(.medium))
15 .fontDesign(.rounded)
16 .multilineTextAlignment(.center)
17 .foregroundColor(.gray)
18 }
19 .padding()
20}
To add some color, let’s use the containerBackground(for:)
modifier on the marketing content SwiftUI view to add a gradient background. If you set the placement to subscriptionStoreHeader
the background will only render behind your marketing content. If you use subscriptionStoreFullHeight
it will render your background behind the full screen.
1SubscriptionStoreView(groupID: PurchaseManager.subscriptionGroupId) {
2 VStack(spacing: 16) {
3 Image(systemName: "dollarsign.square.fill")
4 .resizable()
5 .scaledToFit()
6 .foregroundColor(.green)
7 .frame(height: 100)
8
9 Text("Subscribe to Pro!")
10 .font(.title)
11 .bold()
12
13 Text("Please buy me 🙏")
14 .font(.subheadline.weight(.medium))
15 .fontDesign(.rounded)
16 .multilineTextAlignment(.center)
17 }
18 .padding()
19 .foregroundColor(.white)
20 .containerBackground(for: .subscriptionStoreFullHeight) {
21 LinearGradient(colors: [.blue, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
22 }
23
24}
By default, SubscriptionStoreView
renders a material background behind all of the products. You can hide this using the backgroundStyle
view modifier set to clear
. You can then tweak the background behind each individual product using the subscriptionStorePickerItemBackground
.
1SubscriptionStoreView(groupID: PurchaseManager.subscriptionGroupId) {
2 VStack(spacing: 16) {
3 Image(systemName: "dollarsign.square.fill")
4 .resizable()
5 .scaledToFit()
6 .foregroundColor(.green)
7 .frame(height: 100)
8
9 Text("Subscribe to Pro!")
10 .font(.title)
11 .bold()
12
13 Text("Please buy me 🙏")
14 .font(.subheadline.weight(.medium))
15 .fontDesign(.rounded)
16 .multilineTextAlignment(.center)
17 }
18 .padding()
19 .foregroundColor(.white)
20 .containerBackground(for: .subscriptionStoreFullHeight) {
21 LinearGradient(colors: [.blue, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
22 }
23}
24.backgroundStyle(.clear)
25.subscriptionStorePickerItemBackground(.thinMaterial)
While not quite as flexible as building your own SwiftUI paywall using the ProductView
building blocks, SubscriptionStoreView
allows you to create a more traditional subscription app paywall with a product picker and a strong call to action button at the bottom. It’s automatic handling of trial eligibility and upgrades also make it a good option.
Benefits and tradeoffs
These new StoreKit views are a nice way to easily get a usable paywall for your app. Combined with StoreKit 2, you can set up and manage a subscription app using just these native tools.
There are some missing pieces however. The styling options are very limited. You have more flexibility with ProductView
, but those views are not really suited well for a subscription app and you have to manage trial eligibility, restore functionality, and upgrade/crossgrade options manually.
SubscriptionStoreView
is more optimized for subscription apps, but doesn’t allow for one time purchase options like a lifetime purchase. You also cannot build multi-page paywalls where one plan is highlighted to purchase, but alternative plans can be found on a separate page.
Using with RevenueCat
StoreKit views are great for quickly rendering a paywall UI, but you will still need to handle transaction updates within your app, manage which features your users are entitled to, and validate your transactions with your server to prevent fraud. Here at RevenueCat we build tools to make all of that easy and do so much more.
In fact, we’ve already updated our SDK to make using StoreKit views with RevenueCat incredibly easy. If you’re building for iOS 17, have the SDK installed, and import RevenueCat in your file, both StoreView
and SubscriptionStoreView
will have a new initializer allowing you to pass in a RevenueCat offering object.
1StoreView(for: offering)
Or
1SubscriptionStoreView(for: offering)
Everything else should work exactly the same as in this guide, but it will now be powered by RevenueCat offerings. This means you’ll get all of RevenueCat’s charts, ability to run experiments to optimize your paywall offerings, validate your transactions, and of course utilize RevenueCat’s world class SDK to make managing your entitlements and transaction events in your code a breeze.
RevenueCat Paywalls
If you want a more customizable solution that’s also super easy to use, RevenueCat has recently released a new paywalls feature that expands the capabilities of your paywalls. RevenueCat paywalls allow you to define a paywall from our dashboard, and our SDK automatically renders that paywall natively (not a web view!) on device for you. This allows you to change your paywalls from our backend without needing to wait for an update to get through App Review. Plus it hooks up directly with our powerful experiments features to make optimizing your paywalls a breeze.
RevenueCat paywalls offer a few features that may make it a better choice over StoreKit views such as:
- iOS 15+ support
- Ability to include non-consumable in-app purchases such as lifetime products inside of your subscription paywall
- Our growing list of templates are much more flexible and follow industry best-practices for high performing paywalls
- Flexible footer view rendering mode allows for highly customizable native marketing content
- Easily run experiments to optimize your paywalls
- Cross-platform support for Android (coming soon)
Final thoughts
Our biggest goal at RevenueCat is to help developers make more money. We do this by offering an entire mobile subscription platform with native and cross-platform SDKs to make integrating in-app purchases as easy as possible in any application. Our customers don’t need to worry about the different native subscription APIs or the intricacies of keeping subscription status in sync with renewals, cancellations, and billing issues.
I wrote this guide because I believe StoreKit views may be the perfect solution for some of you to get started with subscription apps. Even if you don’t ultimately decide to use our tools, we want to help all developers succeed where we can.
You might also like
- Blog post
Implementing in-app purchases and monetizing your Roku app: A step-by-step guide
A comprehensive guide to implementing and monetizing your Roku channel, from choosing the right strategy to technical tips and integration best practices.
- Blog post
How we built the RevenueCat SDK for Kotlin Multiplatform
Explore the architecture and key decisions behind building the RevenueCat Kotlin Multiplatform SDK, designed to streamline in-app purchases across platforms.
- Blog post
Inside RevenueCat’s engineering strategy: Scaling beyond 32,000+ apps
The strategies and principles that guide our global team to build reliable, developer-loved software