// // PaywallView.swift // SportsTime // // Full-screen paywall for Pro subscription using SubscriptionStoreView. // import SwiftUI import StoreKit struct PaywallView: View { @Environment(\.dismiss) private var dismiss @Environment(\.colorScheme) private var colorScheme private let storeManager = StoreManager.shared let source: String init(source: String = "unknown") { self.source = source } var body: some View { SubscriptionStoreView(subscriptions: storeManager.products.filter { $0.subscription != nil }) { VStack(spacing: 0) { // Hero section VStack(spacing: Theme.Spacing.sm) { Image(systemName: "shield.lefthalf.filled.badge.checkmark") .font(.system(size: 28)) .foregroundStyle(Theme.warmOrange) .accessibilityHidden(true) Text("SportsTime Pro") .font(.title2.bold()) .foregroundStyle(Theme.textPrimary(colorScheme)) .accessibilityIdentifier("paywall.title") Text("Your All-Access Pass") .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) } .padding(.vertical, Theme.Spacing.xl) .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: Theme.CornerRadius.large) .fill(Theme.warmOrange.opacity(0.06)) ) .padding(.horizontal, Theme.Spacing.md) // Feature grid — GeometryReader to make all cards identical squares GeometryReader { geo in let spacing = Theme.Spacing.sm let hPadding = Theme.Spacing.md * 2 let cardSize = (geo.size.width - hPadding - spacing * 3) / 4 HStack(spacing: spacing) { featureCard(icon: "infinity", label: "Unlimited\nTrips", size: cardSize) featureCard(icon: "doc.text.fill", label: "PDF\nExport", size: cardSize) featureCard(icon: "building.2.fill", label: "Stadium\nTracking", size: cardSize) featureCard(icon: "trophy.fill", label: "Achieve-\nments", size: cardSize) } .padding(.horizontal, Theme.Spacing.md) } .frame(height: 90) .padding(.top, Theme.Spacing.md) // Dashed ticket perforation separator Line() .stroke(style: StrokeStyle(lineWidth: 1.5, dash: [6, 4])) .foregroundStyle(Theme.textMuted(colorScheme).opacity(0.6)) .frame(height: 1) .padding(.horizontal, Theme.Spacing.lg) .padding(.top, Theme.Spacing.lg) .padding(.bottom, Theme.Spacing.sm) } } .storeButton(.visible, for: .restorePurchases) .storeButton(.visible, for: .redeemCode) .subscriptionStoreControlStyle(.prominentPicker) .subscriptionStoreButtonLabel(.displayName.multiline) .onInAppPurchaseStart { product in AnalyticsManager.shared.trackPurchaseStarted(productId: product.id, source: source) } .onInAppPurchaseCompletion { product, result in switch result { case .success(.success(_)): AnalyticsManager.shared.trackPurchaseCompleted(productId: product.id, source: source) Task { @MainActor in await storeManager.updateEntitlements() storeManager.trackSubscriptionAnalytics(source: "purchase_success") } dismiss() case .success(.userCancelled): AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: "user_cancelled") case .success(.pending): AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: "pending") case .failure(let error): AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: error.localizedDescription) @unknown default: AnalyticsManager.shared.trackPurchaseFailed(productId: product.id, source: source, error: "unknown_result") } } .task { await storeManager.loadProducts() } .onAppear { AnalyticsManager.shared.trackPaywallViewed(source: source) } } // MARK: - Feature Card private func featureCard(icon: String, label: String, size: CGFloat) -> some View { VStack(spacing: 4) { Image(systemName: icon) .font(.system(size: 18)) .foregroundStyle(Theme.warmOrange) .accessibilityHidden(true) Text(label) .font(.system(size: 10, weight: .medium)) .multilineTextAlignment(.center) .foregroundStyle(Theme.textSecondary(colorScheme)) .lineLimit(2) .minimumScaleFactor(0.8) } .accessibilityElement(children: .combine) .frame(width: size, height: size) .background( RoundedRectangle(cornerRadius: Theme.CornerRadius.small) .fill(Theme.warmOrange.opacity(0.08)) .overlay( RoundedRectangle(cornerRadius: Theme.CornerRadius.small) .strokeBorder(Theme.warmOrange.opacity(0.15), lineWidth: 1) ) ) } } // MARK: - Dashed Line Shape private struct Line: Shape { func path(in rect: CGRect) -> Path { var path = Path() path.move(to: CGPoint(x: 0, y: rect.midY)) path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) return path } } #Preview { PaywallView() }