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:
@@ -27,16 +27,18 @@ final class GamesHistoryViewModelTests: XCTestCase {
|
||||
stadiumId: "stadium-1",
|
||||
stadiumNameAtVisit: "Stadium 2026",
|
||||
visitDate: Calendar.current.date(from: DateComponents(year: 2026, month: 6, day: 15))!,
|
||||
sport: .mlb,
|
||||
visitType: .game,
|
||||
dataSource: .manual
|
||||
dataSource: .fullyManual
|
||||
)
|
||||
|
||||
let visit2025 = StadiumVisit(
|
||||
stadiumId: "stadium-2",
|
||||
stadiumNameAtVisit: "Stadium 2025",
|
||||
visitDate: Calendar.current.date(from: DateComponents(year: 2025, month: 6, day: 15))!,
|
||||
sport: .mlb,
|
||||
visitType: .game,
|
||||
dataSource: .manual
|
||||
dataSource: .fullyManual
|
||||
)
|
||||
|
||||
modelContext.insert(visit2026)
|
||||
@@ -60,8 +62,9 @@ final class GamesHistoryViewModelTests: XCTestCase {
|
||||
stadiumId: "yankee-stadium",
|
||||
stadiumNameAtVisit: "Yankee Stadium",
|
||||
visitDate: Date(),
|
||||
sport: .mlb,
|
||||
visitType: .game,
|
||||
dataSource: .manual
|
||||
dataSource: .fullyManual
|
||||
)
|
||||
|
||||
modelContext.insert(mlbVisit)
|
||||
@@ -83,16 +86,18 @@ final class GamesHistoryViewModelTests: XCTestCase {
|
||||
stadiumId: "stadium-1",
|
||||
stadiumNameAtVisit: "Old Stadium",
|
||||
visitDate: Date().addingTimeInterval(-86400 * 30), // 30 days ago
|
||||
sport: .mlb,
|
||||
visitType: .game,
|
||||
dataSource: .manual
|
||||
dataSource: .fullyManual
|
||||
)
|
||||
|
||||
let newVisit = StadiumVisit(
|
||||
stadiumId: "stadium-2",
|
||||
stadiumNameAtVisit: "New Stadium",
|
||||
visitDate: Date(),
|
||||
sport: .mlb,
|
||||
visitType: .game,
|
||||
dataSource: .manual
|
||||
dataSource: .fullyManual
|
||||
)
|
||||
|
||||
modelContext.insert(oldVisit)
|
||||
|
||||
@@ -29,24 +29,27 @@ final class VisitListTests: XCTestCase {
|
||||
stadiumId: stadiumId,
|
||||
stadiumNameAtVisit: "Yankee Stadium",
|
||||
visitDate: Date().addingTimeInterval(-86400 * 30), // 30 days ago
|
||||
sport: .mlb,
|
||||
visitType: .game,
|
||||
dataSource: .manual
|
||||
dataSource: .fullyManual
|
||||
)
|
||||
|
||||
let visit2 = StadiumVisit(
|
||||
stadiumId: stadiumId,
|
||||
stadiumNameAtVisit: "Yankee Stadium",
|
||||
visitDate: Date().addingTimeInterval(-86400 * 7), // 7 days ago
|
||||
sport: .mlb,
|
||||
visitType: .game,
|
||||
dataSource: .manual
|
||||
dataSource: .fullyManual
|
||||
)
|
||||
|
||||
let visit3 = StadiumVisit(
|
||||
stadiumId: stadiumId,
|
||||
stadiumNameAtVisit: "Yankee Stadium",
|
||||
visitDate: Date(), // today
|
||||
sport: .mlb,
|
||||
visitType: .tour,
|
||||
dataSource: .manual
|
||||
dataSource: .fullyManual
|
||||
)
|
||||
|
||||
modelContext.insert(visit1)
|
||||
@@ -77,8 +80,9 @@ final class VisitListTests: XCTestCase {
|
||||
stadiumId: stadium1,
|
||||
stadiumNameAtVisit: "Yankee Stadium",
|
||||
visitDate: Date().addingTimeInterval(Double(-i * 86400)),
|
||||
sport: .mlb,
|
||||
visitType: .game,
|
||||
dataSource: .manual
|
||||
dataSource: .fullyManual
|
||||
)
|
||||
modelContext.insert(visit)
|
||||
}
|
||||
@@ -87,8 +91,9 @@ final class VisitListTests: XCTestCase {
|
||||
stadiumId: stadium2,
|
||||
stadiumNameAtVisit: "Fenway Park",
|
||||
visitDate: Date(),
|
||||
sport: .mlb,
|
||||
visitType: .game,
|
||||
dataSource: .manual
|
||||
dataSource: .fullyManual
|
||||
)
|
||||
modelContext.insert(fenwayVisit)
|
||||
try modelContext.save()
|
||||
|
||||
Reference in New Issue
Block a user