// // CustomItemSubscriptionService.swift // SportsTime // // CloudKit subscription service for real-time custom item updates // import Foundation import CloudKit import Combine /// Manages CloudKit subscriptions for custom itinerary items on shared trips actor CustomItemSubscriptionService { static let shared = CustomItemSubscriptionService() private let container: CKContainer private let publicDatabase: CKDatabase /// Publisher that emits trip IDs when their custom items change private let changeSubject = PassthroughSubject() var changePublisher: AnyPublisher { changeSubject.eraseToAnyPublisher() } /// Track which trip IDs have active subscriptions private var subscribedTripIds: Set = [] private init() { self.container = CKContainer(identifier: "iCloud.com.sportstime.app") self.publicDatabase = container.publicCloudDatabase } // MARK: - Subscription Management /// Subscribe to changes for a specific trip's custom items func subscribeToTrip(_ tripId: UUID) async throws { guard !subscribedTripIds.contains(tripId) else { print("📡 [Subscription] Already subscribed to trip: \(tripId)") return } let subscriptionID = "custom-items-\(tripId.uuidString)" // Check if subscription already exists do { _ = try await publicDatabase.subscription(for: subscriptionID) subscribedTripIds.insert(tripId) print("📡 [Subscription] Found existing subscription for trip: \(tripId)") return } catch let error as CKError where error.code == .unknownItem { // Subscription doesn't exist, create it } catch { print("📡 [Subscription] Error checking subscription: \(error)") } // Create subscription for this trip's custom items let predicate = NSPredicate( format: "%K == %@", CKCustomItineraryItem.tripIdKey, tripId.uuidString ) let subscription = CKQuerySubscription( recordType: CKRecordType.customItineraryItem, predicate: predicate, subscriptionID: subscriptionID, options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion] ) // Configure notification let notificationInfo = CKSubscription.NotificationInfo() notificationInfo.shouldSendContentAvailable = true // Silent notification subscription.notificationInfo = notificationInfo do { try await publicDatabase.save(subscription) subscribedTripIds.insert(tripId) print("📡 [Subscription] Created subscription for trip: \(tripId)") } catch let error as CKError { print("📡 [Subscription] Failed to create subscription: \(error.code.rawValue) - \(error.localizedDescription)") throw error } } /// Unsubscribe from a specific trip's custom items func unsubscribeFromTrip(_ tripId: UUID) async throws { guard subscribedTripIds.contains(tripId) else { return } let subscriptionID = "custom-items-\(tripId.uuidString)" do { try await publicDatabase.deleteSubscription(withID: subscriptionID) subscribedTripIds.remove(tripId) print("📡 [Subscription] Removed subscription for trip: \(tripId)") } catch let error as CKError where error.code == .unknownItem { // Already deleted subscribedTripIds.remove(tripId) } } /// Subscribe to all trips that the user has access to (via polls) func subscribeToSharedTrips(tripIds: [UUID]) async { for tripId in tripIds { do { try await subscribeToTrip(tripId) } catch { print("📡 [Subscription] Failed to subscribe to trip \(tripId): \(error)") } } } // MARK: - Notification Handling /// Handle CloudKit notification - call this from AppDelegate/SceneDelegate func handleNotification(_ notification: CKNotification) { guard let queryNotification = notification as? CKQueryNotification, let subscriptionID = queryNotification.subscriptionID, subscriptionID.hasPrefix("custom-items-") else { return } // Extract trip ID from subscription ID let tripIdString = String(subscriptionID.dropFirst("custom-items-".count)) guard let tripId = UUID(uuidString: tripIdString) else { return } print("📡 [Subscription] Received change notification for trip: \(tripId)") changeSubject.send(tripId) } /// Handle notification from userInfo dictionary (from push notification) nonisolated func handleRemoteNotification(userInfo: [AnyHashable: Any]) async { guard let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) else { return } await handleNotification(notification) } // MARK: - Cleanup /// Remove all subscriptions (call on sign out or cleanup) func removeAllSubscriptions() async { for tripId in subscribedTripIds { try? await unsubscribeFromTrip(tripId) } subscribedTripIds.removeAll() } }