> This page is for For Developers.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://whop.ferndocs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://whop.ferndocs.com/_mcp/server.

# Purchasing

> Display plans and handle the checkout flow

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:

```swift
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()
                    }
                }
            }
        }
    }
}
```

> **Note**
>
> Plans are fetched during `configure()`. The `plans` array will be empty until
> configuration completes successfully. You can refresh plans later using
> `refreshPlans()`.

#### Property Reference: CheckoutPlan

\| Property | Type | Description | |----------|------|-------------| | `id` |
`String` | Plan ID (starts with `plan_`) | | `title` | `String?` | Display
name | | `description` | `String?` | Description text | | `initialPrice` |
`Double` | Price amount | | `baseCurrency` | `String` | ISO currency code
(e.g., "USD") | | `planType` | `PlanType` | `.oneTime` or `.renewal` | |
`renewalPeriod` | `RenewalPeriod?` | `.monthly`, `.yearly`, etc. |

## Initiating a Purchase

Call `purchase()` with a plan ID. The SDK presents a checkout sheet, handles payment, and returns when complete:

```swift
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

#### Checkout sheet appears

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

#### User completes payment

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

#### Sheet dismisses automatically

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

#### Membership updates

The SDK refreshes `checkout.memberships` and `isSubscribed` returns `true`.

#### Method Reference: purchase()

```swift
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 `.storeKit`). For Apple Pay, use `Checkout.ApplePayButton` instead.

**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:

```swift
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)
```

#### Property Reference: CheckoutPurchaseResult

| Property     | Type                  | Description                                                      |
| ------------ | --------------------- | ---------------------------------------------------------------- |
| `receiptId`  | `String`              | Unique receipt identifier                                        |
| `membership` | `CheckoutMembership?` | The created/updated membership (nil for StoreKit-only purchases) |

**Membership properties:**

| Property    | Type     | Description                               |
| ----------- | -------- | ----------------------------------------- |
| `id`        | `String` | Membership ID                             |
| `productId` | `String` | Associated product ID                     |
| `status`    | `Status` | `.active`, `.cancelled`, `.expired`, etc. |
| `isActive`  | `Bool`   | Whether the membership grants access      |
| `expiresAt` | `Date?`  | When the membership expires               |
| `createdAt` | `Date`   | When the membership was created           |

## Error Handling

Always handle the possible error cases:

```swift
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.")
}
```

> **Tip**
>
> 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:

```swift
// 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:

```swift
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/Property                   | Purpose                      |
| --------------------------------- | ---------------------------- |
| `checkout.plans`                  | Array of available plans     |
| `checkout.purchase(_:)`           | Initiate checkout for a plan |
| `checkout.refreshPlans()`         | Refresh plans from server    |
| `WhopCheckoutError.cancelled`     | User dismissed checkout      |
| `WhopCheckoutError.paymentFailed` | Payment was declined         |

## Next Steps

#### [User Management](/developer/guides/ios/checkout/users)

Handle login, logout, and check membership status