feat(store): add In-App Purchase system with Pro subscription

Implement freemium model with StoreKit 2:
- StoreManager singleton for purchase/restore/entitlements
- ProFeature enum defining gated features
- PaywallView and OnboardingPaywallView for upsell UI
- ProGate view modifier and ProBadge component

Feature gating:
- Trip saving: 1 free trip, then requires Pro
- PDF export: Pro only with badge indicator
- Progress tab: Shows ProLockedView for free users
- Settings: Subscription management section

Also fixes pre-existing test issues with StadiumVisit
and ItineraryOption model signature changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-13 11:41:40 -06:00
parent e4204175ea
commit 22772fa57f
19 changed files with 1293 additions and 34 deletions

View File

@@ -9,9 +9,13 @@ struct SettingsView: View {
@Environment(\.colorScheme) private var colorScheme
@State private var viewModel = SettingsViewModel()
@State private var showResetConfirmation = false
@State private var showPaywall = false
var body: some View {
List {
// Subscription
subscriptionSection
// Theme Selection
themeSection
@@ -187,6 +191,83 @@ struct SettingsView: View {
.listRowBackground(Theme.cardBackground(colorScheme))
}
// 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()
}
}
// MARK: - Helpers
private func sportColor(for sport: Sport) -> Color {