154 lines
5.1 KiB
Swift
154 lines
5.1 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.
|
|
@available(iOS 15.0, macOS 12.0, watchOS 8.0, *)
|
|
public class BLBroadcastClient: ObservableObject {
|
|
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?
|
|
}
|