Skip to main content
The iOS WhopIAP SDK is coming soon. This documentation is a preview.
The WhopIAP 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 whop.plans array. Each plan includes pricing and display information:
struct PlansView: View {
    @Environment(WhopIAP.self) var whop

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

                        Spacer()

                        Text(plan.initialPrice, format: .currency(code: plan.currency))
                            .bold()
                    }
                }
            }
        }
    }
}
Plans are fetched during configure(). The plans array will be empty until configuration completes successfully.
PropertyTypeDescription
idStringPlan ID (starts with plan_)
titleString?Display name
planDescriptionString?Description text
initialPriceDecimalPrice amount
currencyStringISO 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(WhopIAP.self) var whop
    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 whop.purchase(planId)
            print("Purchase complete! Receipt: \(result.receiptId)")
        } catch WhopIAPError.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.
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 whop.memberships and isSubscribed() returns true.
func purchase(_ planId: String) async throws -> PurchaseResult
Parameters:
  • planId: The plan ID to purchase (starts with plan_)
Returns: PurchaseResult with receiptId and membership details.Throws:
  • WhopIAPError.cancelled - User dismissed checkout
  • WhopIAPError.tokenUnavailable - Token fetch failed
  • WhopIAPError.paymentFailed(String) - Payment was declined

Handling Purchase Results

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

// Access the new membership
print("Membership ID: \(result.membership.id)")
print("Product: \(result.membership.productId)")
print("Status: \(result.membership.status)")

// Store receipt for your records
saveReceipt(result.receiptId)
PropertyTypeDescription
receiptIdStringUnique receipt identifier
membershipMembershipThe created/updated membership
Membership properties:
PropertyTypeDescription
idStringMembership ID
productIdStringAssociated product ID
statusMembershipStatus.active, .cancelled, .expired
expiresAtDate?When the membership expires
createdAtDateWhen the membership was created

Error Handling

Always handle the three possible error cases:
do {
    let result = try await whop.purchase("plan_xxxxxxxxxxxxxx")
    showSuccessMessage()
} catch WhopIAPError.cancelled {
    // User tapped outside the sheet or hit cancel
    // This is normal - don't show an error
} catch WhopIAPError.tokenUnavailable {
    // Your backend token endpoint failed
    showError("Unable to connect. Please try again.")
} catch WhopIAPError.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 WhopIAPError.cancelled as an error. Users commonly dismiss checkout sheets to think about their purchase or check something else first.

Complete Example

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

    @State private var selectedPlan: Plan?
    @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(whop.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 whop.purchase(plan.id)
            dismiss()
        } catch WhopIAPError.cancelled {
            // User cancelled - do nothing
        } catch WhopIAPError.paymentFailed(let message) {
            errorMessage = message
        } catch {
            errorMessage = "Something went wrong. Please try again."
        }
    }
}

struct PlanCard: View {
    let plan: Plan
    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.currency))
                    .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
whop.plansArray of available plans
whop.purchase(_:)Initiate checkout for a plan
WhopIAPError.cancelledUser dismissed checkout
WhopIAPError.paymentFailedPayment was declined

Next Steps

User Management

Handle login, logout, and check membership status