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

@@ -14,6 +14,8 @@ struct TripDetailView: View {
let trip: Trip
let games: [String: RichGame]
@Query private var savedTrips: [SavedTrip]
@State private var showProPaywall = false
@State private var selectedDay: ItineraryDay?
@State private var showExportSheet = false
@State private var showShareSheet = false
@@ -69,12 +71,21 @@ struct TripDetailView: View {
}
Button {
Task {
await exportPDF()
if StoreManager.shared.isPro {
Task {
await exportPDF()
}
} else {
showProPaywall = true
}
} label: {
Image(systemName: "doc.fill")
.foregroundStyle(Theme.warmOrange)
HStack(spacing: 2) {
Image(systemName: "doc.fill")
if !StoreManager.shared.isPro {
ProBadge()
}
}
.foregroundStyle(Theme.warmOrange)
}
}
}
@@ -90,6 +101,9 @@ struct TripDetailView: View {
ShareSheet(items: [trip.name, trip.formattedDateRange])
}
}
.sheet(isPresented: $showProPaywall) {
PaywallView()
}
.onAppear {
checkIfSaved()
}
@@ -523,6 +537,12 @@ struct TripDetailView: View {
}
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
}