Skip to main content
The WhopCheckout SDK handles the entire purchase flow: displaying a checkout sheet, processing payment, and updating membership status automatically.

Displaying Available Plans

Access your plans through the checkout.plans array. Each plan includes pricing and display information:
struct PlansView: View {
    @Environment(Checkout.self) var checkout

    var body: some View {
        Group {
            if checkout.plans.isEmpty {
                ProgressView("Loading plans...")
            } else {
                List(checkout.plans) { plan in
                    HStack {
                        VStack(alignment: .leading) {
                            Text(plan.title ?? "Subscription")
                                .font(.headline)
                            Text(plan.description ?? "")
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                        }

                        Spacer()

                        Text(plan.initialPrice, format: .currency(code: plan.baseCurrency))
                            .bold()
                    }
                }
            }
        }
    }
}
Plans are fetched during configure(). The plans array will be empty until configuration completes successfully. You can refresh plans later using refreshPlans().
PropertyTypeDescription
idStringPlan ID (starts with plan_)
titleString?Display name
descriptionString?Description text
initialPriceDoublePrice amount
baseCurrencyStringISO currency code (e.g., “USD”)
planTypePlanType.oneTime or .renewal
renewalPeriodRenewalPeriod?.monthly, .yearly, etc.

Initiating a Purchase

Call purchase() with a plan ID. The SDK presents a checkout sheet, handles payment, and returns when complete:
struct PurchaseButton: View {
    @Environment(Checkout.self) var checkout
    let planId: String

    @State private var isPurchasing = false
    @State private var error: Error?

    var body: some View {
        Button {
            Task {
                await makePurchase()
            }
        } label: {
            if isPurchasing {
                ProgressView()
            } else {
                Text("Subscribe")
            }
        }
        .disabled(isPurchasing)
        .alert("Purchase Failed", isPresented: .constant(error != nil)) {
            Button("OK") { error = nil }
        } message: {
            Text(error?.localizedDescription ?? "")
        }
    }

    func makePurchase() async {
        isPurchasing = true
        defer { isPurchasing = false }

        do {
            let result = try await checkout.purchase(planId)
            print("Purchase complete! Receipt: \(result.receiptId)")
        } catch WhopCheckoutError.cancelled {
            // User dismissed the checkout - not an error
        } catch {
            self.error = error
        }
    }
}

What Happens During Purchase

1

Checkout sheet appears

The SDK presents a native sheet with the Whop payment form (US) or StoreKit (elsewhere).
2

User completes payment

User enters payment details and confirms. Supports cards, Apple Pay, and more.
3

Sheet dismisses automatically

Once payment succeeds (or user cancels), the sheet closes.
4

Membership updates

The SDK refreshes checkout.memberships and isSubscribed returns true.
func purchase(_ planId: String, method: PaymentMethod? = nil) async throws -> CheckoutPurchaseResult
Parameters:
  • planId: The plan ID to purchase (starts with plan_)
  • method: Optional override for payment method (.whop or .apple)
Returns: CheckoutPurchaseResult with receiptId and membership details.Throws:
  • WhopCheckoutError.cancelled - User dismissed checkout
  • WhopCheckoutError.notConfigured - SDK not configured
  • WhopCheckoutError.paymentFailed(String) - Payment was declined

Handling Purchase Results

The CheckoutPurchaseResult gives you confirmation details:
let result = try await checkout.purchase("plan_xxxxxxxxxxxxxx")

// Access the new membership (for Whop purchases)
if let membership = result.membership {
    print("Membership ID: \(membership.id)")
    print("Product: \(membership.productId)")
    print("Status: \(membership.status)")
}

// Store receipt for your records
saveReceipt(result.receiptId)
PropertyTypeDescription
receiptIdStringUnique receipt identifier
membershipCheckoutMembership?The created/updated membership (nil for StoreKit-only purchases)
Membership properties:
PropertyTypeDescription
idStringMembership ID
productIdStringAssociated product ID
statusStatus.active, .cancelled, .expired, etc.
isActiveBoolWhether the membership grants access
expiresAtDate?When the membership expires
createdAtDateWhen the membership was created

Error Handling

Always handle the possible error cases:
do {
    let result = try await checkout.purchase("plan_xxxxxxxxxxxxxx")
    showSuccessMessage()
} catch WhopCheckoutError.cancelled {
    // User tapped outside the sheet or hit cancel
    // This is normal - don't show an error
} catch WhopCheckoutError.notConfigured {
    // SDK not configured - shouldn't happen in production
    showError("Please restart the app.")
} catch WhopCheckoutError.paymentFailed(let message) {
    // Card declined, insufficient funds, etc.
    showError("Payment failed: \(message)")
} catch {
    // Unexpected error
    showError("Something went wrong. Please try again.")
}
Don’t treat WhopCheckoutError.cancelled as an error. Users commonly dismiss checkout sheets to think about their purchase or check something else first.

Refreshing Plans

You can refresh plans after initialization to get updated pricing or availability:
// Refresh and use the return value
let plans = try await Checkout.shared.refreshPlans()

// Or just refresh (plans property updates automatically)
try await Checkout.shared.refreshPlans()

Complete Example

A full paywall view combining plans display and purchasing:
struct PaywallView: View {
    @Environment(Checkout.self) var checkout
    @Environment(\.dismiss) var dismiss

    @State private var selectedPlan: CheckoutPlan?
    @State private var isPurchasing = false
    @State private var errorMessage: String?

    var body: some View {
        NavigationStack {
            VStack(spacing: 24) {
                // Header
                VStack(spacing: 8) {
                    Text("Go Premium")
                        .font(.largeTitle)
                        .bold()
                    Text("Unlock all features")
                        .foregroundStyle(.secondary)
                }
                .padding(.top, 32)

                // Plans
                VStack(spacing: 12) {
                    ForEach(checkout.plans) { plan in
                        PlanCard(
                            plan: plan,
                            isSelected: selectedPlan?.id == plan.id
                        ) {
                            selectedPlan = plan
                        }
                    }
                }
                .padding(.horizontal)

                Spacer()

                // Purchase button
                Button {
                    Task { await purchase() }
                } label: {
                    Group {
                        if isPurchasing {
                            ProgressView()
                        } else {
                            Text("Continue")
                        }
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(selectedPlan == nil ? Color.gray : Color.accentColor)
                    .foregroundStyle(.white)
                    .clipShape(RoundedRectangle(cornerRadius: 12))
                }
                .disabled(selectedPlan == nil || isPurchasing)
                .padding(.horizontal)
                .padding(.bottom)
            }
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") { dismiss() }
                }
            }
            .alert("Error", isPresented: .constant(errorMessage != nil)) {
                Button("OK") { errorMessage = nil }
            } message: {
                Text(errorMessage ?? "")
            }
        }
    }

    func purchase() async {
        guard let plan = selectedPlan else { return }

        isPurchasing = true
        defer { isPurchasing = false }

        do {
            _ = try await checkout.purchase(plan.id)
            dismiss()
        } catch WhopCheckoutError.cancelled {
            // User cancelled - do nothing
        } catch WhopCheckoutError.paymentFailed(let message) {
            errorMessage = message
        } catch {
            errorMessage = "Something went wrong. Please try again."
        }
    }
}

struct PlanCard: View {
    let plan: CheckoutPlan
    let isSelected: Bool
    let onTap: () -> Void

    var body: some View {
        Button(action: onTap) {
            HStack {
                VStack(alignment: .leading) {
                    Text(plan.title ?? "Plan")
                        .font(.headline)
                    if let period = plan.renewalPeriod {
                        Text(period.description)
                            .font(.subheadline)
                            .foregroundStyle(.secondary)
                    }
                }

                Spacer()

                Text(plan.initialPrice, format: .currency(code: plan.baseCurrency))
                    .bold()
            }
            .padding()
            .background(isSelected ? Color.accentColor.opacity(0.1) : Color(.secondarySystemBackground))
            .clipShape(RoundedRectangle(cornerRadius: 12))
            .overlay(
                RoundedRectangle(cornerRadius: 12)
                    .stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 2)
            )
        }
        .buttonStyle(.plain)
    }
}

Quick Reference

Method/PropertyPurpose
checkout.plansArray of available plans
checkout.purchase(_:)Initiate checkout for a plan
checkout.refreshPlans()Refresh plans from server
WhopCheckoutError.cancelledUser dismissed checkout
WhopCheckoutError.paymentFailedPayment was declined

Next Steps

User Management

Handle login, logout, and check membership status