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:
71
SportsTime/Features/Paywall/ViewModifiers/ProGate.swift
Normal file
71
SportsTime/Features/Paywall/ViewModifiers/ProGate.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user