LimitedRound 2 is open. Get your first month free, no extra charge.Join the waitlist ›
Wire in your backend

How to build a paywall in SwiftUI

TL;DR. A paywall is the screen that sells your in-app purchases. With StoreKit 2 you load Product values, present prices, call purchase, verify the transaction, and check current entitlements. There is no secret key, since StoreKit talks to Apple directly. AppFlight generates the paywall, and RevenueCat or Superwall add entitlements and remote testing.

What a paywall does for an iOS app

The paywall is where most of an app's revenue is decided. It has to load the right products, present prices in the user's currency, make the value clear, run the purchase reliably, and unlock access the instant it succeeds. It also needs a restore path, since Apple requires users to recover purchases on a new device. StoreKit 2 handles the plumbing with modern async APIs and signed transactions, so the hard part becomes the design and the offer, not the receipt handling that older StoreKit forced on you.

Building a paywall with AppFlight

In AppFlight you describe the offer, such as a monthly Pro subscription, and the AI generates a SwiftUI paywall on StoreKit 2: it loads products by id, lays out the prices, runs the purchase, verifies the transaction, and gates features on the entitlement. StoreKit needs no key, so nothing is classified as client or backend. If you connect RevenueCat, AppFlight wires entitlements through it, and if you connect Superwall, it can present remote paywalls you edit from a dashboard. You choose how much to hand off versus keep in plain StoreKit.

A SwiftUI example

Load products, run a verified purchase, and check entitlements with StoreKit 2:

import SwiftUI
import StoreKit

struct PaywallView: View {
    @State private var products: [Product] = []
    @State private var isPro = false

    var body: some View {
        VStack(spacing: 16) {
            Text("Unlock Pro").font(.title.bold())

            ForEach(products) { product in
                Button {
                    Task { await buy(product) }
                } label: {
                    Text("\(product.displayName) \(product.displayPrice)")
                }
                .buttonStyle(.borderedProminent)
            }

            Button("Restore purchases") {
                Task { try? await AppStore.sync() }
            }
        }
        .task {
            products = (try? await Product.products(for: ["com.example.pro.monthly"])) ?? []
            await refreshEntitlements()
        }
    }

    func buy(_ product: Product) async {
        guard let result = try? await product.purchase() else { return }
        if case .success(let verification) = result,
           case .verified(let transaction) = verification {
            await transaction.finish()
            isPro = true
        }
    }

    func refreshEntitlements() async {
        for await result in Transaction.currentEntitlements {
            if case .verified = result { isPro = true }
        }
    }
}

On iOS 17 and later you can also use SubscriptionStoreView, a ready-made StoreKit paywall you point at a subscription group, when you want less custom layout code.

Alternatives

RevenueCat manages entitlements, receipt validation, and cross-platform subscribers, so you stop writing that logic yourself, and it ships a basic paywall builder too. Superwall focuses on the paywall itself, rendering it from a dashboard so you can A/B test design and price without an App Store update. Hand-built StoreKit 2 stays the leanest path when you have a single product and do not need remote experiments or another dependency.

FAQ

Do I need a third-party tool to build a paywall?

No. StoreKit 2 lets you load products, run purchases, and check entitlements directly with no backend key. RevenueCat and Superwall sit on top of StoreKit to add cross-platform entitlements, receipt handling, and remote A/B testing, which save time once you scale.

How does StoreKit 2 verify a purchase?

A purchase returns a verification result you must check. Use the .verified case, which means Apple signed the transaction, then call finish() on it. Treating an unverified transaction as valid is the common security mistake.

Can I change the paywall without an App Store update?

Not with hand-built StoreKit, where the layout ships in the binary. Superwall exists for exactly this: it renders paywalls fetched at runtime so you can edit copy, design, and price experiments from a dashboard without resubmitting.

Sources

Build this app without opening Xcode.

AppFlight turns a plain-English prompt into a real native iOS app and ships it to the App Store. Round 2 is open: free for your first month.

Join the waitlist