struct PaywallView: View {
@Environment(WhopIAP.self) var iap
@Environment(\.dismiss) var dismiss
@State private var selectedPlanId: String?
@State private var isPurchasing = false
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 24) {
// Header
VStack(spacing: 8) {
Image(systemName: "crown.fill")
.font(.system(size: 48))
.foregroundStyle(.yellow)
Text("Unlock Premium")
.font(.title)
.bold()
Text("Get access to all features")
.foregroundStyle(.secondary)
}
.padding(.top, 32)
// Plans
ForEach(iap.plans) { plan in
Button {
selectedPlanId = plan.id
} label: {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(plan.title ?? "Plan")
.font(.headline)
Text(plan.initialPrice, format: .currency(code: plan.baseCurrency))
.foregroundStyle(.secondary)
}
Spacer()
}
.padding()
.background(plan.id == selectedPlanId ? Color.accentColor.opacity(0.1) : Color(.secondarySystemBackground))
.cornerRadius(12)
.overlay {
RoundedRectangle(cornerRadius: 12)
.stroke(plan.id == selectedPlanId ? Color.accentColor : .clear, lineWidth: 2)
}
}
.buttonStyle(.plain)
}
// Purchase button
Button {
guard let planId = selectedPlanId else { return }
Task { await purchase(planId) }
} label: {
Group {
if isPurchasing {
ProgressView().tint(.white)
} else {
Text("Continue")
}
}
.font(.headline)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(Color.accentColor, in: RoundedRectangle(cornerRadius: 12))
}
.disabled(selectedPlanId == nil || isPurchasing)
.opacity(selectedPlanId == nil ? 0.5 : 1)
}
.padding()
}
.navigationTitle("Subscribe")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Close") { dismiss() }
}
}
}
}
func purchase(_ planId: String) async {
isPurchasing = true
defer { isPurchasing = false }
do {
_ = try await iap.purchase(planId)
dismiss()
} catch WhopIAPError.cancelled {
// User cancelled
} catch {
print("Purchase failed: \(error.localizedDescription)")
}
}
}