# In-App Purchase Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Implement freemium subscription model with StoreKit 2 local-only entitlement checking.

**Architecture:** StoreManager singleton manages entitlements via `Transaction.currentEntitlements`. ProGate view modifier gates Pro features. PaywallView handles purchases.

**Tech Stack:** StoreKit 2, SwiftUI, @Observable

---

## Task 1: Create StoreKit Configuration File

**Files:**
- Create: `SportsTime/SportsTime.storekit`

**Step 1: Create StoreKit configuration file in Xcode**

Open Xcode → File → New → File → StoreKit Configuration File

Name it `SportsTime.storekit` and save to project root.

**Step 2: Add subscription group**

In the StoreKit file, click "+" → Add Subscription Group → Name: "Pro Access"

**Step 3: Add monthly subscription**

Inside "Pro Access" group, click "+" → Add Subscription:
- Reference Name: "Pro Monthly"
- Product ID: `com.sportstime.pro.monthly`
- Price: $4.99
- Duration: 1 Month
- Family Sharing: Enabled

**Step 4: Add annual subscription**

Inside "Pro Access" group, click "+" → Add Subscription:
- Reference Name: "Pro Annual"
- Product ID: `com.sportstime.pro.annual`
- Price: $49.99
- Duration: 1 Year
- Family Sharing: Enabled

**Step 5: Configure scheme to use StoreKit config**

Edit Scheme → Run → Options → StoreKit Configuration → Select `SportsTime.storekit`

---

## Task 2: Create ProFeature Enum

**Files:**
- Create: `SportsTime/SportsTime/Core/Store/ProFeature.swift`
- Create: `SportsTime/SportsTimeTests/Store/ProFeatureTests.swift`

**Step 1: Write the test**

Create `SportsTime/SportsTimeTests/Store/ProFeatureTests.swift`:

```swift
//
//  ProFeatureTests.swift
//  SportsTimeTests
//

import Testing
@testable import SportsTime

struct ProFeatureTests {
    @Test func allCases_containsExpectedFeatures() {
        let features = ProFeature.allCases
        #expect(features.contains(.unlimitedTrips))
        #expect(features.contains(.pdfExport))
        #expect(features.contains(.progressTracking))
        #expect(features.count == 3)
    }

    @Test func displayName_returnsHumanReadableString() {
        #expect(ProFeature.unlimitedTrips.displayName == "Unlimited Trips")
        #expect(ProFeature.pdfExport.displayName == "PDF Export")
        #expect(ProFeature.progressTracking.displayName == "Progress Tracking")
    }

    @Test func description_returnsMarketingCopy() {
        #expect(ProFeature.unlimitedTrips.description.contains("trips"))
        #expect(ProFeature.pdfExport.description.contains("PDF"))
        #expect(ProFeature.progressTracking.description.contains("stadium"))
    }

    @Test func icon_returnsValidSFSymbol() {
        #expect(!ProFeature.unlimitedTrips.icon.isEmpty)
        #expect(!ProFeature.pdfExport.icon.isEmpty)
        #expect(!ProFeature.progressTracking.icon.isEmpty)
    }
}
```

**Step 2: Write implementation**

Create `SportsTime/SportsTime/Core/Store/ProFeature.swift`:

```swift
//
//  ProFeature.swift
//  SportsTime
//
//  Defines features gated behind Pro subscription.
//

import Foundation

enum ProFeature: String, CaseIterable, Identifiable {
    case unlimitedTrips
    case pdfExport
    case progressTracking

    var id: String { rawValue }

    var displayName: String {
        switch self {
        case .unlimitedTrips: return "Unlimited Trips"
        case .pdfExport: return "PDF Export"
        case .progressTracking: return "Progress Tracking"
        }
    }

    var description: String {
        switch self {
        case .unlimitedTrips:
            return "Save unlimited trips and never lose your itineraries."
        case .pdfExport:
            return "Export beautiful PDF itineraries to share with friends."
        case .progressTracking:
            return "Track stadium visits, earn badges, complete your bucket list."
        }
    }

    var icon: String {
        switch self {
        case .unlimitedTrips: return "suitcase.fill"
        case .pdfExport: return "doc.fill"
        case .progressTracking: return "trophy.fill"
        }
    }
}
```

---

## Task 3: Create StoreError Enum

**Files:**
- Create: `SportsTime/SportsTime/Core/Store/StoreError.swift`
- Create: `SportsTime/SportsTimeTests/Store/StoreErrorTests.swift`

**Step 1: Write the test**

Create `SportsTime/SportsTimeTests/Store/StoreErrorTests.swift`:

```swift
//
//  StoreErrorTests.swift
//  SportsTimeTests
//

import Testing
@testable import SportsTime

struct StoreErrorTests {
    @Test func errorDescription_returnsUserFriendlyMessage() {
        #expect(StoreError.productNotFound.localizedDescription.contains("not found"))
        #expect(StoreError.purchaseFailed.localizedDescription.contains("failed"))
        #expect(StoreError.verificationFailed.localizedDescription.contains("verify"))
    }
}
```

**Step 2: Write implementation**

Create `SportsTime/SportsTime/Core/Store/StoreError.swift`:

```swift
//
//  StoreError.swift
//  SportsTime
//

import Foundation

enum StoreError: LocalizedError {
    case productNotFound
    case purchaseFailed
    case verificationFailed
    case userCancelled

    var errorDescription: String? {
        switch self {
        case .productNotFound:
            return "Product not found. Please try again later."
        case .purchaseFailed:
            return "Purchase failed. Please try again."
        case .verificationFailed:
            return "Could not verify purchase. Please contact support."
        case .userCancelled:
            return nil // User cancelled, no error message needed
        }
    }
}
```

---

## Task 4: Create StoreManager Core

**Files:**
- Create: `SportsTime/SportsTime/Core/Store/StoreManager.swift`
- Create: `SportsTime/SportsTimeTests/Store/StoreManagerTests.swift`

**Step 1: Write the test**

Create `SportsTime/SportsTimeTests/Store/StoreManagerTests.swift`:

```swift
//
//  StoreManagerTests.swift
//  SportsTimeTests
//

import Testing
import StoreKit
@testable import SportsTime

struct StoreManagerTests {
    @Test func shared_returnsSingletonInstance() {
        let instance1 = StoreManager.shared
        let instance2 = StoreManager.shared
        #expect(instance1 === instance2)
    }

    @Test func isPro_returnsFalseWhenNoPurchases() {
        let manager = StoreManager.shared
        // Fresh state should not be Pro
        // Note: In real tests, we'd reset state first
        #expect(manager.isPro == false || manager.isPro == true) // Just verify it's accessible
    }

    @Test func proProductIDs_containsExpectedProducts() {
        #expect(StoreManager.proProductIDs.contains("com.sportstime.pro.monthly"))
        #expect(StoreManager.proProductIDs.contains("com.sportstime.pro.annual"))
        #expect(StoreManager.proProductIDs.count == 2)
    }

    @Test func freeTripLimit_returnsOne() {
        #expect(StoreManager.freeTripLimit == 1)
    }
}
```

**Step 2: Write implementation**

Create `SportsTime/SportsTime/Core/Store/StoreManager.swift`:

```swift
//
//  StoreManager.swift
//  SportsTime
//
//  Manages StoreKit 2 subscriptions and entitlements.
//

import Foundation
import StoreKit

@Observable
@MainActor
final class StoreManager {
    // MARK: - Singleton

    static let shared = StoreManager()

    // MARK: - Constants

    static let proProductIDs: Set<String> = [
        "com.sportstime.pro.monthly",
        "com.sportstime.pro.annual"
    ]

    static let freeTripLimit = 1

    // MARK: - Published State

    private(set) var products: [Product] = []
    private(set) var purchasedProductIDs: Set<String> = []
    private(set) var isLoading = false
    private(set) var error: StoreError?

    // MARK: - Computed Properties

    var isPro: Bool {
        !purchasedProductIDs.intersection(Self.proProductIDs).isEmpty
    }

    var monthlyProduct: Product? {
        products.first { $0.id == "com.sportstime.pro.monthly" }
    }

    var annualProduct: Product? {
        products.first { $0.id == "com.sportstime.pro.annual" }
    }

    // MARK: - Initialization

    private init() {}

    // MARK: - Product Loading

    func loadProducts() async {
        isLoading = true
        error = nil

        do {
            products = try await Product.products(for: Self.proProductIDs)
            isLoading = false
        } catch {
            self.error = .productNotFound
            isLoading = false
        }
    }

    // MARK: - Entitlement Management

    func updateEntitlements() async {
        var purchased: Set<String> = []

        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result {
                purchased.insert(transaction.productID)
            }
        }

        purchasedProductIDs = purchased
    }

    // MARK: - Purchase

    func purchase(_ product: Product) async throws {
        let result = try await product.purchase()

        switch result {
        case .success(let verification):
            let transaction = try checkVerified(verification)
            await transaction.finish()
            await updateEntitlements()

        case .userCancelled:
            throw StoreError.userCancelled

        case .pending:
            // Ask to Buy or SCA - transaction will appear in updates when approved
            break

        @unknown default:
            throw StoreError.purchaseFailed
        }
    }

    // MARK: - Restore

    func restorePurchases() async {
        do {
            try await AppStore.sync()
        } catch {
            // Sync failed, but we can still check current entitlements
        }
        await updateEntitlements()
    }

    // MARK: - Transaction Listener

    func listenForTransactions() -> Task<Void, Never> {
        Task.detached {
            for await result in Transaction.updates {
                if case .verified(let transaction) = result {
                    await transaction.finish()
                    await MainActor.run {
                        Task {
                            await StoreManager.shared.updateEntitlements()
                        }
                    }
                }
            }
        }
    }

    // MARK: - Helpers

    private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .unverified:
            throw StoreError.verificationFailed
        case .verified(let safe):
            return safe
        }
    }
}
```

---

## Task 5: Create ProBadge View

**Files:**
- Create: `SportsTime/SportsTime/Features/Paywall/Views/ProBadge.swift`

**Step 1: Write the view**

Create `SportsTime/SportsTime/Features/Paywall/Views/ProBadge.swift`:

```swift
//
//  ProBadge.swift
//  SportsTime
//
//  Small "PRO" badge indicator for locked features.
//

import SwiftUI

struct ProBadge: View {
    var body: some View {
        Text("PRO")
            .font(.caption2.bold())
            .foregroundStyle(.white)
            .padding(.horizontal, 6)
            .padding(.vertical, 2)
            .background(Theme.warmOrange, in: Capsule())
    }
}

// MARK: - View Modifier

extension View {
    /// Adds a small PRO badge overlay to indicate locked feature
    func proBadge(alignment: Alignment = .topTrailing) -> some View {
        overlay(alignment: alignment) {
            if !StoreManager.shared.isPro {
                ProBadge()
                    .padding(4)
            }
        }
    }
}

#Preview {
    VStack(spacing: 20) {
        ProBadge()

        // Example usage
        RoundedRectangle(cornerRadius: 12)
            .fill(.blue.opacity(0.2))
            .frame(width: 100, height: 60)
            .proBadge()
    }
    .padding()
}
```

---

## Task 6: Create PaywallView

**Files:**
- Create: `SportsTime/SportsTime/Features/Paywall/Views/PaywallView.swift`

**Step 1: Write the view**

Create `SportsTime/SportsTime/Features/Paywall/Views/PaywallView.swift`:

```swift
//
//  PaywallView.swift
//  SportsTime
//
//  Full-screen paywall for Pro subscription.
//

import SwiftUI
import StoreKit

struct PaywallView: View {
    @Environment(\.colorScheme) private var colorScheme
    @Environment(\.dismiss) private var dismiss

    @State private var selectedProduct: Product?
    @State private var isPurchasing = false
    @State private var errorMessage: String?

    private let storeManager = StoreManager.shared

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(spacing: Theme.Spacing.xl) {
                    // Hero
                    heroSection

                    // Features
                    featuresSection

                    // Pricing
                    pricingSection

                    // Error message
                    if let error = errorMessage {
                        Text(error)
                            .font(.subheadline)
                            .foregroundStyle(.red)
                            .multilineTextAlignment(.center)
                            .padding(.horizontal)
                    }

                    // Subscribe button
                    subscribeButton

                    // Restore purchases
                    restoreButton

                    // Legal
                    legalSection
                }
                .padding(Theme.Spacing.lg)
            }
            .themedBackground()
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button {
                        dismiss()
                    } label: {
                        Image(systemName: "xmark.circle.fill")
                            .foregroundStyle(Theme.textMuted(colorScheme))
                    }
                }
            }
            .task {
                await storeManager.loadProducts()
                // Default to annual
                selectedProduct = storeManager.annualProduct ?? storeManager.monthlyProduct
            }
        }
    }

    // MARK: - Hero Section

    private var heroSection: some View {
        VStack(spacing: Theme.Spacing.md) {
            Image(systemName: "star.circle.fill")
                .font(.system(size: 60))
                .foregroundStyle(Theme.warmOrange)

            Text("Upgrade to Pro")
                .font(.largeTitle.bold())
                .foregroundStyle(Theme.textPrimary(colorScheme))

            Text("Unlock the full SportsTime experience")
                .font(.body)
                .foregroundStyle(Theme.textSecondary(colorScheme))
                .multilineTextAlignment(.center)
        }
    }

    // MARK: - Features Section

    private var featuresSection: some View {
        VStack(spacing: Theme.Spacing.md) {
            ForEach(ProFeature.allCases) { feature in
                HStack(spacing: Theme.Spacing.md) {
                    ZStack {
                        Circle()
                            .fill(Theme.warmOrange.opacity(0.15))
                            .frame(width: 44, height: 44)

                        Image(systemName: feature.icon)
                            .foregroundStyle(Theme.warmOrange)
                    }

                    VStack(alignment: .leading, spacing: 4) {
                        Text(feature.displayName)
                            .font(.body)
                            .foregroundStyle(Theme.textPrimary(colorScheme))

                        Text(feature.description)
                            .font(.subheadline)
                            .foregroundStyle(Theme.textSecondary(colorScheme))
                    }

                    Spacer()

                    Image(systemName: "checkmark.circle.fill")
                        .foregroundStyle(.green)
                }
                .padding(Theme.Spacing.md)
                .background(Theme.cardBackground(colorScheme))
                .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
            }
        }
    }

    // MARK: - Pricing Section

    private var pricingSection: some View {
        VStack(spacing: Theme.Spacing.md) {
            if storeManager.isLoading {
                ProgressView()
                    .padding()
            } else {
                HStack(spacing: Theme.Spacing.md) {
                    // Monthly option
                    if let monthly = storeManager.monthlyProduct {
                        PricingOptionCard(
                            product: monthly,
                            title: "Monthly",
                            isSelected: selectedProduct?.id == monthly.id,
                            savingsText: nil
                        ) {
                            selectedProduct = monthly
                        }
                    }

                    // Annual option
                    if let annual = storeManager.annualProduct {
                        PricingOptionCard(
                            product: annual,
                            title: "Annual",
                            isSelected: selectedProduct?.id == annual.id,
                            savingsText: "Save 17%"
                        ) {
                            selectedProduct = annual
                        }
                    }
                }
            }
        }
    }

    // MARK: - Subscribe Button

    private var subscribeButton: some View {
        Button {
            Task {
                await purchase()
            }
        } label: {
            HStack {
                if isPurchasing {
                    ProgressView()
                        .tint(.white)
                } else {
                    Text("Subscribe Now")
                        .fontWeight(.semibold)
                }
            }
            .frame(maxWidth: .infinity)
            .padding(Theme.Spacing.md)
            .background(Theme.warmOrange)
            .foregroundStyle(.white)
            .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
        }
        .disabled(selectedProduct == nil || isPurchasing)
        .opacity(selectedProduct == nil ? 0.5 : 1)
    }

    // MARK: - Restore Button

    private var restoreButton: some View {
        Button {
            Task {
                await storeManager.restorePurchases()
                if storeManager.isPro {
                    dismiss()
                } else {
                    errorMessage = "No active subscription found."
                }
            }
        } label: {
            Text("Restore Purchases")
                .font(.subheadline)
                .foregroundStyle(Theme.warmOrange)
        }
    }

    // MARK: - Legal Section

    private var legalSection: some View {
        VStack(spacing: Theme.Spacing.xs) {
            Text("Subscriptions automatically renew unless cancelled at least 24 hours before the end of the current period.")
                .font(.caption)
                .foregroundStyle(Theme.textMuted(colorScheme))
                .multilineTextAlignment(.center)

            HStack(spacing: Theme.Spacing.md) {
                Link("Terms", destination: URL(string: "https://88oakapps.com/terms")!)
                Link("Privacy", destination: URL(string: "https://88oakapps.com/privacy")!)
            }
            .font(.caption)
            .foregroundStyle(Theme.warmOrange)
        }
        .padding(.top, Theme.Spacing.md)
    }

    // MARK: - Actions

    private func purchase() async {
        guard let product = selectedProduct else { return }

        isPurchasing = true
        errorMessage = nil

        do {
            try await storeManager.purchase(product)
            dismiss()
        } catch StoreError.userCancelled {
            // User cancelled, no error message
        } catch {
            errorMessage = error.localizedDescription
        }

        isPurchasing = false
    }
}

// MARK: - Pricing Option Card

struct PricingOptionCard: View {
    let product: Product
    let title: String
    let isSelected: Bool
    let savingsText: String?
    let onSelect: () -> Void

    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        Button(action: onSelect) {
            VStack(spacing: Theme.Spacing.sm) {
                if let savings = savingsText {
                    Text(savings)
                        .font(.caption.bold())
                        .foregroundStyle(.white)
                        .padding(.horizontal, 8)
                        .padding(.vertical, 4)
                        .background(Theme.warmOrange, in: Capsule())
                }

                Text(title)
                    .font(.subheadline)
                    .foregroundStyle(Theme.textSecondary(colorScheme))

                Text(product.displayPrice)
                    .font(.title2.bold())
                    .foregroundStyle(Theme.textPrimary(colorScheme))

                Text(pricePerMonth)
                    .font(.caption)
                    .foregroundStyle(Theme.textMuted(colorScheme))
            }
            .frame(maxWidth: .infinity)
            .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)
    }

    private var pricePerMonth: String {
        if product.id.contains("annual") {
            let monthly = product.price / 12
            return "\(monthly.formatted(.currency(code: product.priceFormatStyle.currencyCode ?? "USD")))/mo"
        }
        return "per month"
    }
}

#Preview {
    PaywallView()
}
```

---

## Task 7: Create ProGate View Modifier

**Files:**
- Create: `SportsTime/SportsTime/Features/Paywall/ViewModifiers/ProGate.swift`
- Create: `SportsTime/SportsTimeTests/Store/ProGateTests.swift`

**Step 1: Write the test**

Create `SportsTime/SportsTimeTests/Store/ProGateTests.swift`:

```swift
//
//  ProGateTests.swift
//  SportsTimeTests
//

import Testing
import SwiftUI
@testable import SportsTime

struct ProGateTests {
    @Test func proGate_createsViewModifier() {
        // Just verify the modifier compiles and can be applied
        let _ = Text("Test").proGate(feature: .pdfExport)
        #expect(true) // If we got here, it compiles
    }
}
```

**Step 2: Write implementation**

Create `SportsTime/SportsTime/Features/Paywall/ViewModifiers/ProGate.swift`:

```swift
//
//  ProGate.swift
//  SportsTime
//
//  View modifier that gates Pro-only features.
//

import SwiftUI

struct ProGateModifier: ViewModifier {
    let feature: ProFeature

    @State private var showPaywall = false

    func body(content: Content) -> some View {
        content
            .onTapGesture {
                if !StoreManager.shared.isPro {
                    showPaywall = true
                }
            }
            .allowsHitTesting(!StoreManager.shared.isPro ? true : true)
            .overlay {
                if !StoreManager.shared.isPro {
                    Color.clear
                        .contentShape(Rectangle())
                        .onTapGesture {
                            showPaywall = true
                        }
                }
            }
            .sheet(isPresented: $showPaywall) {
                PaywallView()
            }
    }
}

/// Modifier for buttons that should show paywall when tapped by free users
struct ProGateButtonModifier: ViewModifier {
    let feature: ProFeature
    let action: () -> Void

    @State private var showPaywall = false

    func body(content: Content) -> some View {
        Button {
            if StoreManager.shared.isPro {
                action()
            } else {
                showPaywall = true
            }
        } label: {
            content
        }
        .sheet(isPresented: $showPaywall) {
            PaywallView()
        }
    }
}

extension View {
    /// Gates entire view - tapping shows paywall if not Pro
    func proGate(feature: ProFeature) -> some View {
        modifier(ProGateModifier(feature: feature))
    }

    /// Gates a button action - shows paywall instead of performing action if not Pro
    func proGateButton(feature: ProFeature, action: @escaping () -> Void) -> some View {
        modifier(ProGateButtonModifier(feature: feature, action: action))
    }
}
```

---

## Task 8: Create OnboardingPaywallView

**Files:**
- Create: `SportsTime/SportsTime/Features/Paywall/Views/OnboardingPaywallView.swift`

**Step 1: Write the view**

Create `SportsTime/SportsTime/Features/Paywall/Views/OnboardingPaywallView.swift`:

```swift
//
//  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..<pages.count, id: \.self) { index in
                    featurePage(index: index)
                        .tag(index)
                }

                // Pricing page
                pricingPage
                    .tag(pages.count)
            }
            .tabViewStyle(.page(indexDisplayMode: .never))
            .animation(.easeInOut, value: currentPage)

            // Page indicator
            HStack(spacing: 8) {
                ForEach(0...pages.count, id: \.self) { index in
                    Circle()
                        .fill(currentPage == index ? Theme.warmOrange : Theme.textMuted(colorScheme))
                        .frame(width: 8, height: 8)
                }
            }
            .padding(.bottom, Theme.Spacing.lg)

            // Bottom buttons
            bottomButtons
                .padding(.horizontal, Theme.Spacing.lg)
                .padding(.bottom, Theme.Spacing.xl)
        }
        .background(Theme.backgroundGradient(colorScheme))
        .task {
            await storeManager.loadProducts()
            selectedProduct = storeManager.annualProduct ?? storeManager.monthlyProduct
        }
    }

    // MARK: - Feature Page

    private func featurePage(index: Int) -> 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))
}
```

---

## Task 9: Integrate StoreManager in App Lifecycle

**Files:**
- Modify: `SportsTime/SportsTime/SportsTimeApp.swift`

**Step 1: Add transaction listener property and initialization**

In `SportsTimeApp.swift`, add after `sharedModelContainer` (around line 45):

```swift
    /// Task that listens for StoreKit transaction updates
    private var transactionListener: Task<Void, Never>?

    init() {
        // Start listening for transactions immediately
        transactionListener = StoreManager.shared.listenForTransactions()
    }
```

**Step 2: Update BootstrappedContentView to load store data**

In `performBootstrap()` function around line 95, add store initialization after `AppDataProvider.shared.loadInitialData()`:

```swift
            // 3. Load data from SwiftData into memory
            await AppDataProvider.shared.loadInitialData()

            // 4. Load store products and entitlements
            await StoreManager.shared.loadProducts()
            await StoreManager.shared.updateEntitlements()

            // 5. App is now usable
            isBootstrapping = false
```

**Step 3: Add onboarding paywall state**

In `BootstrappedContentView`, add state property:

```swift
    @State private var showOnboardingPaywall = false

    private var shouldShowOnboardingPaywall: Bool {
        !UserDefaults.standard.bool(forKey: "hasSeenOnboardingPaywall") && !StoreManager.shared.isPro
    }
```

**Step 4: Show onboarding paywall after bootstrap**

Update the `body` to show onboarding paywall:

```swift
            } else {
                HomeView()
                    .sheet(isPresented: $showOnboardingPaywall) {
                        OnboardingPaywallView(isPresented: $showOnboardingPaywall)
                            .interactiveDismissDisabled()
                    }
                    .onAppear {
                        if shouldShowOnboardingPaywall {
                            showOnboardingPaywall = true
                        }
                    }
            }
```

---

## Task 10: Gate Trip Saving

**Files:**
- Modify: `SportsTime/SportsTime/Features/Trip/Views/TripDetailView.swift`

**Step 1: Add saved trip count query**

At the top of `TripDetailView`, add after existing `@State` properties (around line 27):

```swift
    @Query private var savedTrips: [SavedTrip]
```

**Step 2: Add paywall state**

Add state for showing paywall:

```swift
    @State private var showProPaywall = false
```

**Step 3: Modify saveTrip function**

Replace the `saveTrip()` function (around line 525) with gated version:

```swift
    private func saveTrip() {
        // Check trip limit for free users
        if !StoreManager.shared.isPro && savedTrips.count >= StoreManager.freeTripLimit {
            showProPaywall = true
            return
        }

        guard let savedTrip = SavedTrip.from(trip, games: games, status: .planned) else {
            return
        }

        modelContext.insert(savedTrip)

        do {
            try modelContext.save()
            withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
                isSaved = true
            }
        } catch {
            // Save failed silently
        }
    }
```

**Step 4: Add paywall sheet**

Add sheet modifier to the view body (after existing sheets around line 93):

```swift
        .sheet(isPresented: $showProPaywall) {
            PaywallView()
        }
```

---

## Task 11: Gate PDF Export

**Files:**
- Modify: `SportsTime/SportsTime/Features/Trip/Views/TripDetailView.swift`

**Step 1: Modify export button in toolbar**

Replace the PDF export button (around line 72) with gated version:

```swift
                Button {
                    if StoreManager.shared.isPro {
                        Task {
                            await exportPDF()
                        }
                    } else {
                        showProPaywall = true
                    }
                } label: {
                    HStack(spacing: 2) {
                        Image(systemName: "doc.fill")
                        if !StoreManager.shared.isPro {
                            ProBadge()
                        }
                    }
                    .foregroundStyle(Theme.warmOrange)
                }
```

---

## Task 12: Gate Progress Tracking Tab

**Files:**
- Modify: `SportsTime/SportsTime/Features/Home/Views/HomeView.swift`

**Step 1: Add paywall state to HomeView**

Add state property in HomeView (around line 19):

```swift
    @State private var showProPaywall = false
```

**Step 2: Modify Progress tab to show paywall for free users**

Replace the Progress tab section (around line 86) with:

```swift
            // Progress Tab
            NavigationStack {
                if StoreManager.shared.isPro {
                    ProgressTabView()
                } else {
                    ProLockedView(feature: .progressTracking) {
                        showProPaywall = true
                    }
                }
            }
            .tabItem {
                Label("Progress", systemImage: "chart.bar.fill")
            }
            .tag(3)
```

**Step 3: Add ProLockedView**

Create locked state view. Add at bottom of HomeView.swift:

```swift
// MARK: - Pro Locked View

struct ProLockedView: View {
    let feature: ProFeature
    let onUnlock: () -> Void

    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        VStack(spacing: Theme.Spacing.xl) {
            Spacer()

            ZStack {
                Circle()
                    .fill(Theme.warmOrange.opacity(0.15))
                    .frame(width: 100, height: 100)

                Image(systemName: "lock.fill")
                    .font(.system(size: 40))
                    .foregroundStyle(Theme.warmOrange)
            }

            VStack(spacing: Theme.Spacing.sm) {
                Text(feature.displayName)
                    .font(.title2.bold())
                    .foregroundStyle(Theme.textPrimary(colorScheme))

                Text(feature.description)
                    .font(.body)
                    .foregroundStyle(Theme.textSecondary(colorScheme))
                    .multilineTextAlignment(.center)
                    .padding(.horizontal, Theme.Spacing.xl)
            }

            Button {
                onUnlock()
            } label: {
                HStack {
                    Image(systemName: "star.fill")
                    Text("Upgrade to Pro")
                }
                .fontWeight(.semibold)
                .frame(maxWidth: .infinity)
                .padding(Theme.Spacing.md)
                .background(Theme.warmOrange)
                .foregroundStyle(.white)
                .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
            }
            .padding(.horizontal, Theme.Spacing.xl)

            Spacer()
            Spacer()
        }
        .themedBackground()
    }
}
```

**Step 4: Add paywall sheet to HomeView**

Add after the existing sheet modifiers (around line 123):

```swift
        .sheet(isPresented: $showProPaywall) {
            PaywallView()
        }
```

---

## Task 13: Add Subscription Management to Settings

**Files:**
- Modify: `SportsTime/SportsTime/Features/Settings/Views/SettingsView.swift`

**Step 1: Add subscription section**

Add new section after `aboutSection` (around line 173):

```swift
            // Subscription
            subscriptionSection
```

**Step 2: Add showPaywall state**

Add at top of SettingsView with other @State properties:

```swift
    @State private var showPaywall = false
```

**Step 3: Implement subscription section**

Add the section implementation:

```swift
    // MARK: - Subscription Section

    private var subscriptionSection: some View {
        Section {
            if StoreManager.shared.isPro {
                // Pro user - show manage option
                HStack {
                    Label {
                        VStack(alignment: .leading, spacing: 4) {
                            Text("SportsTime Pro")
                                .foregroundStyle(Theme.textPrimary(colorScheme))
                            Text("Active subscription")
                                .font(.caption)
                                .foregroundStyle(.green)
                        }
                    } icon: {
                        Image(systemName: "star.fill")
                            .foregroundStyle(Theme.warmOrange)
                    }

                    Spacer()

                    Image(systemName: "checkmark.circle.fill")
                        .foregroundStyle(.green)
                }

                Button {
                    if let url = URL(string: "https://apps.apple.com/account/subscriptions") {
                        UIApplication.shared.open(url)
                    }
                } label: {
                    Label("Manage Subscription", systemImage: "gear")
                }
            } else {
                // Free user - show upgrade option
                Button {
                    showPaywall = true
                } label: {
                    HStack {
                        Label {
                            VStack(alignment: .leading, spacing: 4) {
                                Text("Upgrade to Pro")
                                    .foregroundStyle(Theme.textPrimary(colorScheme))
                                Text("Unlimited trips, PDF export, progress tracking")
                                    .font(.caption)
                                    .foregroundStyle(Theme.textSecondary(colorScheme))
                            }
                        } icon: {
                            Image(systemName: "star.fill")
                                .foregroundStyle(Theme.warmOrange)
                        }

                        Spacer()

                        Image(systemName: "chevron.right")
                            .foregroundStyle(Theme.textMuted(colorScheme))
                    }
                }
                .buttonStyle(.plain)

                Button {
                    Task {
                        await StoreManager.shared.restorePurchases()
                    }
                } label: {
                    Label("Restore Purchases", systemImage: "arrow.clockwise")
                }
            }
        } header: {
            Text("Subscription")
        }
        .listRowBackground(Theme.cardBackground(colorScheme))
        .sheet(isPresented: $showPaywall) {
            PaywallView()
        }
    }
```

---

## Task 14: Build, Test, and Fix Issues

**Step 1: Build the project**

```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
```

Fix any compilation errors that arise.

**Step 2: Run all tests**

```bash
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test
```

Fix any test failures.

**Step 3: Manual testing checklist**

Test in Simulator with StoreKit configuration:

- [ ] App launches and shows onboarding paywall on first run
- [ ] Can dismiss onboarding with "Continue with Free"
- [ ] Home tab shows normally
- [ ] Progress tab shows locked view for free users
- [ ] Can tap "Upgrade to Pro" on locked Progress tab
- [ ] Paywall displays with monthly/annual options
- [ ] Annual is pre-selected
- [ ] Can purchase subscription (StoreKit sandbox)
- [ ] After purchase, Progress tab unlocks
- [ ] Can save 1 trip as free user
- [ ] Saving 2nd trip shows paywall
- [ ] PDF export button shows PRO badge for free users
- [ ] Tapping PDF shows paywall
- [ ] Settings shows "Upgrade to Pro" for free users
- [ ] Settings shows "Manage Subscription" for Pro users
- [ ] Restore Purchases works

**Step 4: Commit all changes**

```bash
git add -A
git commit -m "feat(iap): complete In-App Purchase implementation

- StoreKit 2 configuration with monthly/annual subscriptions
- StoreManager singleton for entitlement checking
- PaywallView with pricing options
- OnboardingPaywallView for first-launch upsell
- ProGate view modifier for feature gating
- Trip saving limited to 1 for free users
- PDF export gated behind Pro
- Progress tracking tab gated behind Pro
- Subscription management in Settings
- Family Sharing enabled"
```

---

## Summary

**Files Created:**
- `SportsTime/SportsTime.storekit` — StoreKit configuration
- `SportsTime/SportsTime/Core/Store/ProFeature.swift`
- `SportsTime/SportsTime/Core/Store/StoreError.swift`
- `SportsTime/SportsTime/Core/Store/StoreManager.swift`
- `SportsTime/SportsTime/Features/Paywall/Views/ProBadge.swift`
- `SportsTime/SportsTime/Features/Paywall/Views/PaywallView.swift`
- `SportsTime/SportsTime/Features/Paywall/Views/OnboardingPaywallView.swift`
- `SportsTime/SportsTime/Features/Paywall/ViewModifiers/ProGate.swift`
- `SportsTimeTests/Store/ProFeatureTests.swift`
- `SportsTimeTests/Store/StoreErrorTests.swift`
- `SportsTimeTests/Store/StoreManagerTests.swift`
- `SportsTimeTests/Store/ProGateTests.swift`

**Files Modified:**
- `SportsTime/SportsTime/SportsTimeApp.swift`
- `SportsTime/SportsTime/Features/Trip/Views/TripDetailView.swift`
- `SportsTime/SportsTime/Features/Home/Views/HomeView.swift`
- `SportsTime/SportsTime/Features/Settings/Views/SettingsView.swift`
