// ── Broadcast Client ───────────────────────────────────── // In-app broadcast message client for iOS/watchOS/macOS. // Part of ByteLystPlatformSDK. import Foundation /// In-app message priority levels. public enum BLBroadcastPriority: String, Codable, Sendable { case low = "low" case normal = "normal" case high = "high" case urgent = "urgent" } /// In-app message display styles. public enum BLBroadcastStyle: String, Codable, Sendable { case banner = "banner" case modal = "modal" case toast = "toast" case fullscreen = "fullscreen" } /// In-app message status. public enum BLBroadcastStatus: String, Codable, Sendable { case unread = "unread" case read = "read" case dismissed = "dismissed" } /// Represents an in-app broadcast message. public struct BLInAppMessage: Codable, Sendable, Identifiable { public let id: String public let userId: String public let productId: String public let broadcastId: String public let title: String public let body: String public let bodyMarkdown: String? public let ctaText: String? public let ctaUrl: String? public let priority: BLBroadcastPriority public let style: BLBroadcastStyle public let dismissible: Bool public let expiresAt: String? public let status: BLBroadcastStatus public let createdAt: String public let updatedAt: String } /// Broadcast client for fetching and managing in-app messages. public actor BLBroadcastClient { private let platformClient: BLPlatformClient private var pollTask: Task? public init(platformClient: BLPlatformClient) { self.platformClient = platformClient } /// List active in-app messages for the current user. public func listMessages() async throws -> [BLInAppMessage] { let request = try platformClient.buildRequest(path: "/broadcasts") let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw BLPlatformError.requestFailed(String(data: data, encoding: .utf8) ?? "Unknown error") } let result = try JSONDecoder().decode(MessagesResponse.self, from: data) return result.messages } /// Mark a message as read. public func markRead(messageId: String) async throws { let request = try platformClient.buildRequest( path: "/broadcasts/\(messageId)/read", method: "POST" ) let (_, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw BLPlatformError.requestFailed("Failed to mark message as read") } } /// Mark a message as dismissed. public func markDismissed(messageId: String) async throws { let request = try platformClient.buildRequest( path: "/broadcasts/\(messageId)/dismiss", method: "POST" ) let (_, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw BLPlatformError.requestFailed("Failed to dismiss message") } } /// Track a CTA click and get the redirect URL. public func trackClick(messageId: String) async throws -> String? { let request = try platformClient.buildRequest( path: "/broadcasts/\(messageId)/click", method: "POST" ) let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw BLPlatformError.requestFailed("Failed to track click") } let result = try JSONDecoder().decode(ClickResponse.self, from: data) return result.redirectUrl } /// Start polling for new messages. public func startPolling(interval: TimeInterval = 60, onUpdate: @escaping ([BLInAppMessage]) -> Void) { stopPolling() pollTask = Task { while !Task.isCancelled { do { let messages = try await listMessages() onUpdate(messages) } catch { // Silently ignore polling errors } try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) } } } /// Stop polling for messages. public func stopPolling() { pollTask?.cancel() pollTask = nil } } // MARK: - Response Types private struct MessagesResponse: Codable { let messages: [BLInAppMessage] } private struct ClickResponse: Codable { let success: Bool let redirectUrl: String? }