// ── Feature Flag Client ─────────────────────────────────────── // Polls platform-service /flags/poll for feature flags. // Flags cached in memory, re-polled every 5 minutes. // Consumers call FeatureFlagService.shared.isEnabled("flag_key"). import Foundation @MainActor final class FeatureFlagService: ObservableObject { static let shared = FeatureFlagService() @Published private(set) var flags: [String: Bool] = [:] private var pollTimer: Timer? private let pollIntervalSec: TimeInterval = 5 * 60 // 5 minutes private let productId = "chronomind" private var baseURL: String { Bundle.main.object(forInfoDictionaryKey: "PLATFORM_SERVICE_URL") as? String ?? "https://api.chronomind.app" } private init() {} // MARK: - Public API func start(userId: String? = nil) { Task { await fetchFlags(userId: userId) } pollTimer?.invalidate() pollTimer = Timer.scheduledTimer(withTimeInterval: pollIntervalSec, repeats: true) { [weak self] _ in guard let self else { return } Task { @MainActor in await self.fetchFlags(userId: userId) } } } func stop() { pollTimer?.invalidate() pollTimer = nil } func isEnabled(_ key: String) -> Bool { flags[key] == true } // MARK: - Fetch private func fetchFlags(userId: String? = nil) async { var components = URLComponents(string: "\(baseURL)/api/flags/poll") var queryItems: [URLQueryItem] = [.init(name: "platform", value: "ios")] if let userId { queryItems.append(.init(name: "userId", value: userId)) } components?.queryItems = queryItems guard let url = components?.url else { return } var request = URLRequest(url: url) request.setValue(productId, forHTTPHeaderField: "X-Product-Id") request.timeoutInterval = 10 do { let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return } struct FlagsResponse: Codable { let flags: [String: Bool] } let parsed = try JSONDecoder().decode(FlagsResponse.self, from: data) flags = parsed.flags } catch { // Keep existing flags on error } } }