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:
@@ -17,6 +17,7 @@ struct HomeView: View {
|
||||
@State private var suggestedTripsGenerator = SuggestedTripsGenerator()
|
||||
@State private var selectedSuggestedTrip: SuggestedTrip?
|
||||
@State private var displayedTips: [PlanningTip] = []
|
||||
@State private var showProPaywall = false
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
@@ -86,7 +87,13 @@ struct HomeView: View {
|
||||
|
||||
// Progress Tab
|
||||
NavigationStack {
|
||||
ProgressTabView()
|
||||
if StoreManager.shared.isPro {
|
||||
ProgressTabView()
|
||||
} else {
|
||||
ProLockedView(feature: .progressTracking) {
|
||||
showProPaywall = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabItem {
|
||||
Label("Progress", systemImage: "chart.bar.fill")
|
||||
@@ -121,6 +128,9 @@ struct HomeView: View {
|
||||
TripDetailView(trip: suggestedTrip.trip, games: suggestedTrip.richGames)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showProPaywall) {
|
||||
PaywallView()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Hero Card
|
||||
@@ -548,6 +558,63 @@ struct SavedTripListRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
HomeView()
|
||||
.modelContainer(for: SavedTrip.self, inMemory: true)
|
||||
|
||||
Reference in New Issue
Block a user