feat(polls): implement group trip polling MVP

Add complete group trip polling feature allowing users to share trips
with friends for voting using Borda count scoring.

New components:
- TripPoll and PollVote domain models with share codes and rankings
- LocalTripPoll and LocalPollVote SwiftData models for persistence
- CKTripPoll and CKPollVote CloudKit record wrappers
- PollService actor for CloudKit CRUD operations and subscriptions
- PollCreation/Detail/Voting views and view models
- Deep link handling for sportstime://poll/{code} URLs
- Debug Pro status override toggle in Settings

Integration:
- HomeView shows polls section in My Trips
- SportsTimeApp registers SwiftData models and handles deep links

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-13 21:54:42 -06:00
parent 8e78828bde
commit 13385b6562
16 changed files with 2416 additions and 19 deletions

View File

@@ -35,6 +35,9 @@ struct SportsTimeApp: App {
VisitPhotoMetadata.self,
Achievement.self,
CachedGameScore.self,
// Poll models
LocalTripPoll.self,
LocalPollVote.self,
// Canonical data models
SyncState.self,
CanonicalStadium.self,
@@ -78,6 +81,7 @@ struct BootstrappedContentView: View {
@State private var bootstrapError: Error?
@State private var hasCompletedInitialSync = false
@State private var showOnboardingPaywall = false
@State private var pollShareCode: String?
private var shouldShowOnboardingPaywall: Bool {
!UserDefaults.standard.bool(forKey: "hasSeenOnboardingPaywall") && !StoreManager.shared.isPro
@@ -99,6 +103,11 @@ struct BootstrappedContentView: View {
OnboardingPaywallView(isPresented: $showOnboardingPaywall)
.interactiveDismissDisabled()
}
.sheet(item: $pollShareCode) { code in
NavigationStack {
PollDetailView(shareCode: code)
}
}
.onAppear {
if shouldShowOnboardingPaywall {
showOnboardingPaywall = true
@@ -109,6 +118,9 @@ struct BootstrappedContentView: View {
.task {
await performBootstrap()
}
.onOpenURL { url in
handleDeepLink(url)
}
.onChange(of: scenePhase) { _, newPhase in
switch newPhase {
case .active:
@@ -191,6 +203,25 @@ struct BootstrappedContentView: View {
print("Background sync error: \(error.localizedDescription)")
}
}
// MARK: - Deep Link Handling
private func handleDeepLink(_ url: URL) {
// Handle sportstime://poll/{code} deep links
guard url.scheme == "sportstime",
url.host == "poll",
let code = url.pathComponents.dropFirst().first,
code.count == 6
else { return }
pollShareCode = code.uppercased()
}
}
// MARK: - String Identifiable for Sheet
extension String: @retroactive Identifiable {
public var id: String { self }
}
// MARK: - Bootstrap Loading View