// // OnboardingPaywallView.swift // SportsTime // // First-launch upsell with feature pages. // import SwiftUI import StoreKit struct OnboardingPaywallView: View { @Environment(\.colorScheme) private var colorScheme @Binding var isPresented: Bool @State private var currentPage = 0 @State private var selectedProduct: Product? @State private var isPurchasing = false @State private var errorMessage: String? private let storeManager = StoreManager.shared private let pages: [(icon: String, title: String, description: String, color: Color)] = [ ("suitcase.fill", "Unlimited Trips", "Plan as many road trips as you want. Never lose your itineraries.", Theme.warmOrange), ("doc.fill", "Export & Share", "Generate beautiful PDF itineraries to share with friends.", Theme.routeGold), ("trophy.fill", "Track Your Journey", "Log stadium visits, earn badges, complete your bucket list.", .green) ] var body: some View { VStack(spacing: 0) { // Page content TabView(selection: $currentPage) { ForEach(0.. some View { let page = pages[index] return VStack(spacing: Theme.Spacing.xl) { Spacer() ZStack { Circle() .fill(page.color.opacity(0.15)) .frame(width: 120, height: 120) Image(systemName: page.icon) .font(.system(size: 50)) .foregroundStyle(page.color) } VStack(spacing: Theme.Spacing.md) { Text(page.title) .font(.title.bold()) .foregroundStyle(Theme.textPrimary(colorScheme)) Text(page.description) .font(.body) .foregroundStyle(Theme.textSecondary(colorScheme)) .multilineTextAlignment(.center) .padding(.horizontal, Theme.Spacing.xl) } Spacer() Spacer() } } // MARK: - Pricing Page private var pricingPage: some View { VStack(spacing: Theme.Spacing.lg) { Spacer() Text("Choose Your Plan") .font(.title.bold()) .foregroundStyle(Theme.textPrimary(colorScheme)) if storeManager.isLoading { ProgressView() } else { VStack(spacing: Theme.Spacing.md) { // Annual (recommended) if let annual = storeManager.annualProduct { OnboardingPricingRow( product: annual, title: "Annual", subtitle: "Best Value - Save 17%", isSelected: selectedProduct?.id == annual.id, isRecommended: true ) { selectedProduct = annual } } // Monthly if let monthly = storeManager.monthlyProduct { OnboardingPricingRow( product: monthly, title: "Monthly", subtitle: "Flexible billing", isSelected: selectedProduct?.id == monthly.id, isRecommended: false ) { selectedProduct = monthly } } } .padding(.horizontal, Theme.Spacing.lg) } if let error = errorMessage { Text(error) .font(.caption) .foregroundStyle(.red) } Spacer() Spacer() } } // MARK: - Bottom Buttons private var bottomButtons: some View { VStack(spacing: Theme.Spacing.md) { if currentPage < pages.count { // Next button Button { withAnimation { currentPage += 1 } } label: { Text("Next") .fontWeight(.semibold) .frame(maxWidth: .infinity) .padding(Theme.Spacing.md) .background(Theme.warmOrange) .foregroundStyle(.white) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) } // Skip Button { markOnboardingSeen() isPresented = false } label: { Text("Continue with Free") .font(.subheadline) .foregroundStyle(Theme.textMuted(colorScheme)) } } else { // Subscribe button Button { Task { await purchase() } } label: { HStack { if isPurchasing { ProgressView() .tint(.white) } else { Text("Subscribe") .fontWeight(.semibold) } } .frame(maxWidth: .infinity) .padding(Theme.Spacing.md) .background(Theme.warmOrange) .foregroundStyle(.white) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) } .disabled(selectedProduct == nil || isPurchasing) // Continue free Button { markOnboardingSeen() isPresented = false } label: { Text("Continue with Free") .font(.subheadline) .foregroundStyle(Theme.textMuted(colorScheme)) } } } } // MARK: - Actions private func purchase() async { guard let product = selectedProduct else { return } isPurchasing = true errorMessage = nil do { try await storeManager.purchase(product) markOnboardingSeen() isPresented = false } catch StoreError.userCancelled { // User cancelled } catch { errorMessage = error.localizedDescription } isPurchasing = false } private func markOnboardingSeen() { UserDefaults.standard.set(true, forKey: "hasSeenOnboardingPaywall") } } // MARK: - Pricing Row struct OnboardingPricingRow: View { let product: Product let title: String let subtitle: String let isSelected: Bool let isRecommended: Bool let onSelect: () -> Void @Environment(\.colorScheme) private var colorScheme var body: some View { Button(action: onSelect) { HStack { VStack(alignment: .leading, spacing: 4) { HStack(spacing: Theme.Spacing.xs) { Text(title) .font(.body.bold()) .foregroundStyle(Theme.textPrimary(colorScheme)) if isRecommended { Text("BEST") .font(.caption2.bold()) .foregroundStyle(.white) .padding(.horizontal, 6) .padding(.vertical, 2) .background(Theme.warmOrange, in: Capsule()) } } Text(subtitle) .font(.caption) .foregroundStyle(Theme.textSecondary(colorScheme)) } Spacer() Text(product.displayPrice) .font(.title3.bold()) .foregroundStyle(Theme.textPrimary(colorScheme)) Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") .foregroundStyle(isSelected ? Theme.warmOrange : Theme.textMuted(colorScheme)) } .padding(Theme.Spacing.lg) .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) .overlay { RoundedRectangle(cornerRadius: Theme.CornerRadius.large) .stroke(isSelected ? Theme.warmOrange : Theme.surfaceGlow(colorScheme), lineWidth: isSelected ? 2 : 1) } } .buttonStyle(.plain) } } #Preview { OnboardingPaywallView(isPresented: .constant(true)) }