Skip to main content
This guide shows you how to check if a user has an active subscription and gate premium content accordingly. You’ll also learn how to handle user login/logout to sync subscriptions across devices.

What you’ll learn

  • Check if a user has access to a product
  • Gate content based on subscription status
  • Sync subscriptions across devices with user login

Prerequisites

Check subscription status

Use hasAccess(to:) to check if the user has access to a specific product:
struct ContentView: View {
    @Environment(WhopIAP.self) var iap

    var body: some View {
        VStack {
            // Basic features always available
            BasicFeaturesView()

            // Premium features for subscribers
            if iap.hasAccess(to: "prod_xxxxxxxxxxxxxx") {
                PremiumFeaturesView()
            }

            // If you have multiple tiers, check each product separately
            if iap.hasAccess(to: "prod_yyyyyyyyyyyyyy") {
                EnterpriseFeaturesView()
            }
        }
    }
}
The product ID must be included in the productIDs array when configuring the SDK. Only products you’ve configured can be checked with hasAccess(to:).

View active memberships

Access the memberships array to show subscription details:
struct SubscriptionStatusView: View {
    @Environment(WhopIAP.self) var iap

    var body: some View {
        List {
            if iap.memberships.isEmpty {
                Text("No active subscriptions")
                    .foregroundStyle(.secondary)
            } else {
                ForEach(iap.memberships) { membership in
                    MembershipRow(membership: membership)
                }
            }
        }
    }
}

struct MembershipRow: View {
    let membership: WhopIAPMembership

    var body: some View {
        VStack(alignment: .leading) {
            Text("Subscription")
                .font(.headline)

            if let expiresAt = membership.expiresAt {
                Text("Renews \(expiresAt, style: .date)")
                    .foregroundStyle(.secondary)
            }

            Text("Status: \(membership.status.rawValue)")
                .font(.caption)
                .foregroundStyle(.secondary)
        }
    }
}

User login and logout

User IDs allow subscriptions to sync across devices. When a user logs in, the SDK automatically claims any unclaimed memberships from the current device and loads their existing memberships.

Log in a user

func handleLogin(userId: String) async {
    do {
        try await iap.logIn(appUserId: userId)
        // Memberships are now synced for this user
    } catch {
        print("Login failed: \(error)")
    }
}

Log out a user

func handleLogout() {
    iap.logOut()
    // User is now logged out, memberships cleared
}

Check the current user

if let userId = iap.appUserId {
    Text("Logged in as \(userId)")
} else {
    Text("Not logged in")
}

Guest purchases

Users can purchase subscriptions even without logging in. The SDK tracks these purchases by device ID and automatically claims them when the user logs in.
struct PurchaseFlow: View {
    @Environment(WhopIAP.self) var iap
    @State private var showingLogin = false

    var body: some View {
        VStack {
            if iap.appUserId == nil {
                // User not logged in - they can still purchase
                Text("Purchase as guest or log in to sync across devices")

                Button("Continue as Guest") {
                    // Purchase will be tied to device
                    Task { try? await iap.purchase("plan_xxx") }
                }

                Button("Log In") {
                    showingLogin = true
                }
            } else {
                // User logged in - purchase tied to their account
                Button("Subscribe") {
                    Task { try? await iap.purchase("plan_xxx") }
                }
            }
        }
        .sheet(isPresented: $showingLogin) {
            // Your login view here
        }
    }
}

Complete example

Here’s a complete example showing entitlement checking with user auth:
struct MainView: View {
    @Environment(WhopIAP.self) var iap
    @State private var showingPaywall = false
    @State private var showingLogin = false

    var body: some View {
        NavigationStack {
            Group {
                if !iap.isInitialized {
                    ProgressView("Loading...")
                } else if iap.hasAccess(to: "prod_xxxxxxxxxxxxxx") {
                    PremiumContentView()
                } else {
                    FreeContentView(onUpgrade: { showingPaywall = true })
                }
            }
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Menu {
                        if let userId = iap.appUserId {
                            Text("Signed in as \(userId)")
                            Button("Sign Out", action: signOut)
                        } else {
                            Button("Sign In") { showingLogin = true }
                        }

                        Divider()

                        if iap.hasAccess(to: "prod_xxxxxxxxxxxxxx") {
                            Button("Manage Subscription") {
                                // Show subscription management
                            }
                        }
                    } label: {
                        Image(systemName: "person.circle")
                    }
                }
            }
            .sheet(isPresented: $showingPaywall) {
                PaywallView()
            }
            .sheet(isPresented: $showingLogin) {
                LoginView { userId in
                    Task {
                        try? await iap.logIn(appUserId: userId)
                        showingLogin = false
                    }
                }
            }
        }
    }

    func signOut() {
        iap.logOut()
    }
}

struct FreeContentView: View {
    let onUpgrade: () -> Void

    var body: some View {
        VStack(spacing: 20) {
            Text("Free Content")
                .font(.title)

            Text("Upgrade to access premium features")
                .foregroundStyle(.secondary)

            Button("Upgrade Now", action: onUpgrade)
                .buttonStyle(.borderedProminent)
        }
    }
}

struct PremiumContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "crown.fill")
                .font(.largeTitle)
                .foregroundStyle(.yellow)

            Text("Premium Content")
                .font(.title)

            Text("Thanks for subscribing!")
        }
    }
}

Reactive updates

The SDK automatically updates views when subscription status changes. Since WhopIAP is @Observable, any view using @Environment(WhopIAP.self) will re-render when:
  • A purchase completes
  • A user logs in or out
  • Memberships are loaded or updated
No manual refresh is needed.

Next steps