learning_ai_common_plat/packages/swift-platform-sdk/Sources/BLBroadcastClient.swift
saravanakumardb1 b96503dc2d feat(swift-sdk): Phase 3.3 - Broadcast and Survey clients
- BLBroadcastClient.swift: In-app message fetch, read/dismiss, click tracking, polling
- BLSurveyClient.swift: Survey fetch, start/submit/complete, offline cache, polling
2026-03-03 07:40:56 -08:00

153 lines
5.0 KiB
Swift

// 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<Void, Never>?
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?
}