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