Skip to main content
This guide shows you how to build a paywall that displays your subscription plans and handles the purchase flow. By the end, you’ll have a working paywall that lets users subscribe to your product.

What you’ll build

  • A paywall screen that displays available plans with pricing
  • A purchase flow that handles payments through Whop
  • Error handling for cancelled or failed purchases

Prerequisites

Step 1: Configure the SDK

Initialize the SDK in your app’s entry point with your API key:
import SwiftUI
import WhopPayments

@main
struct MyApp: App {
    @State private var iap = WhopIAP.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(iap)
                .task {
                    iap = try? await WhopIAP.configure(
                        companyID: "biz_xxxxxxxxxxxxxx",
                        productIDs: ["prod_xxxxxxxxxxxxxx"],
                        apiKey: "your_api_key_here"
                    )
                }
        }
    }
}
Find your company ID in the URL of your Whop Dashboard (starts with biz_). Find your product ID in the Products tab (starts with prod_).

Step 2: Display available plans

Use the plans property to show available subscription options:
struct PaywallView: View {
    @Environment(WhopIAP.self) var iap

    var body: some View {
        VStack(spacing: 20) {
            Text("Choose a Plan")
                .font(.title)
                .bold()

            ForEach(iap.plans) { plan in
                PlanCard(plan: plan)
            }
        }
        .padding()
    }
}

struct PlanCard: View {
    let plan: WhopIAPPlan
    @Environment(WhopIAP.self) var iap

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(plan.title ?? "Plan")
                .font(.headline)

            Text(plan.initialPrice, format: .currency(code: plan.baseCurrency))
                .font(.title2)
                .bold()

            Button("Subscribe") {
                Task {
                    await purchase(plan)
                }
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .background(.ultraThinMaterial)
        .cornerRadius(12)
    }

    func purchase(_ plan: WhopIAPPlan) async {
        do {
            let result = try await iap.purchase(plan.id)
            // Purchase successful!
            print("Purchased: \(result.receiptId)")
        } catch WhopIAPError.cancelled {
            // User dismissed the checkout
        } catch {
            // Handle other errors
            print("Purchase failed: \(error)")
        }
    }
}

Step 3: Handle the purchase flow

When a user taps “Subscribe”, the SDK automatically:
  1. Presents a checkout sheet with the Whop payment flow
  2. Handles payment processing
  3. Dismisses the sheet when complete
  4. Returns the result or throws an error
func purchase(_ planId: String) async {
    do {
        let result = try await iap.purchase(planId)
        // Success - user now has an active membership
        print("Receipt ID: \(result.receiptId)")
    } catch WhopIAPError.cancelled {
        // User dismissed the checkout sheet
    } catch WhopIAPError.paymentFailed(let message) {
        // Payment failed - show error to user
        showError(message)
    } catch {
        // Other errors
        showError("Something went wrong. Please try again.")
    }
}

Complete example

Here’s a full paywall implementation:
struct PaywallView: View {
    @Environment(WhopIAP.self) var iap
    @Environment(\.dismiss) var dismiss
    @State private var selectedPlanId: String?
    @State private var isPurchasing = false

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(spacing: 24) {
                    // Header
                    VStack(spacing: 8) {
                        Image(systemName: "crown.fill")
                            .font(.system(size: 48))
                            .foregroundStyle(.yellow)

                        Text("Unlock Premium")
                            .font(.title)
                            .bold()

                        Text("Get access to all features")
                            .foregroundStyle(.secondary)
                    }
                    .padding(.top, 32)

                    // Plans
                    ForEach(iap.plans) { plan in
                        Button {
                            selectedPlanId = plan.id
                        } label: {
                            HStack {
                                VStack(alignment: .leading, spacing: 4) {
                                    Text(plan.title ?? "Plan")
                                        .font(.headline)
                                    Text(plan.initialPrice, format: .currency(code: plan.baseCurrency))
                                        .foregroundStyle(.secondary)
                                }
                                Spacer()
                            }
                            .padding()
                            .background(plan.id == selectedPlanId ? Color.accentColor.opacity(0.1) : Color(.secondarySystemBackground))
                            .cornerRadius(12)
                            .overlay {
                                RoundedRectangle(cornerRadius: 12)
                                    .stroke(plan.id == selectedPlanId ? Color.accentColor : .clear, lineWidth: 2)
                            }
                        }
                        .buttonStyle(.plain)
                    }

                    // Purchase button
                    Button {
                        guard let planId = selectedPlanId else { return }
                        Task { await purchase(planId) }
                    } label: {
                        Group {
                            if isPurchasing {
                                ProgressView().tint(.white)
                            } else {
                                Text("Continue")
                            }
                        }
                        .font(.headline)
                        .foregroundStyle(.white)
                        .frame(maxWidth: .infinity)
                        .padding(.vertical, 14)
                        .background(Color.accentColor, in: RoundedRectangle(cornerRadius: 12))
                    }
                    .disabled(selectedPlanId == nil || isPurchasing)
                    .opacity(selectedPlanId == nil ? 0.5 : 1)
                }
                .padding()
            }
            .navigationTitle("Subscribe")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Close") { dismiss() }
                }
            }
        }
    }

    func purchase(_ planId: String) async {
        isPurchasing = true
        defer { isPurchasing = false }

        do {
            _ = try await iap.purchase(planId)
            dismiss()
        } catch WhopIAPError.cancelled {
            // User cancelled
        } catch {
            print("Purchase failed: \(error.localizedDescription)")
        }
    }
}

Next steps