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:
Trey t
2026-01-13 11:41:40 -06:00
parent e4204175ea
commit 22772fa57f
19 changed files with 1293 additions and 34 deletions

View File

@@ -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()