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.
Property Type Description idStringPlan ID (starts with plan_) titleString?Display name planDescriptionString?Description text initialPriceDecimalPrice amount currencyStringISO currency code (e.g., “USD”) planTypePlanType.oneTime or .renewalrenewalPeriodRenewalPeriod?.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
Checkout sheet appears
The SDK presents a native sheet with the Whop payment form.
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 whop.memberships and isSubscribed() returns true.
Method Reference: purchase()
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 )
Property Reference: PurchaseResult
Property Type Description receiptIdStringUnique receipt identifier membershipMembershipThe created/updated membership
Membership properties: Property Type Description idStringMembership ID productIdStringAssociated product ID statusMembershipStatus.active, .cancelled, .expiredexpiresAtDate?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/Property Purpose 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