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:
@@ -10,6 +10,14 @@ import SwiftData
|
||||
|
||||
@main
|
||||
struct SportsTimeApp: App {
|
||||
/// Task that listens for StoreKit transaction updates
|
||||
private var transactionListener: Task<Void, Never>?
|
||||
|
||||
init() {
|
||||
// Start listening for transactions immediately
|
||||
transactionListener = StoreManager.shared.listenForTransactions()
|
||||
}
|
||||
|
||||
var sharedModelContainer: ModelContainer = {
|
||||
let schema = Schema([
|
||||
// User data models
|
||||
@@ -63,6 +71,11 @@ struct BootstrappedContentView: View {
|
||||
@State private var isBootstrapping = true
|
||||
@State private var bootstrapError: Error?
|
||||
@State private var hasCompletedInitialSync = false
|
||||
@State private var showOnboardingPaywall = false
|
||||
|
||||
private var shouldShowOnboardingPaywall: Bool {
|
||||
!UserDefaults.standard.bool(forKey: "hasSeenOnboardingPaywall") && !StoreManager.shared.isPro
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
@@ -76,6 +89,15 @@ struct BootstrappedContentView: View {
|
||||
}
|
||||
} else {
|
||||
HomeView()
|
||||
.sheet(isPresented: $showOnboardingPaywall) {
|
||||
OnboardingPaywallView(isPresented: $showOnboardingPaywall)
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
.onAppear {
|
||||
if shouldShowOnboardingPaywall {
|
||||
showOnboardingPaywall = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
@@ -109,10 +131,14 @@ struct BootstrappedContentView: View {
|
||||
// 3. Load data from SwiftData into memory
|
||||
await AppDataProvider.shared.loadInitialData()
|
||||
|
||||
// 4. App is now usable
|
||||
// 4. Load store products and entitlements
|
||||
await StoreManager.shared.loadProducts()
|
||||
await StoreManager.shared.updateEntitlements()
|
||||
|
||||
// 5. App is now usable
|
||||
isBootstrapping = false
|
||||
|
||||
// 5. Background: Try to refresh from CloudKit (non-blocking)
|
||||
// 6. Background: Try to refresh from CloudKit (non-blocking)
|
||||
Task.detached(priority: .background) {
|
||||
await self.performBackgroundSync(context: context)
|
||||
await MainActor.run {
|
||||
|
||||
Reference in New Issue
Block a user