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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user