// // NativeNotificationContentFetcher.swift // DailyNotificationPlugin // // Swift SPI mirroring org.timesafari.dailynotification.NativeNotificationContentFetcher (Android). // import Foundation /// Context for a native fetch (trigger, scheduled user notification time, metadata). public struct FetchContext { public let trigger: String public let scheduledTimeMillis: Int64? public let fetchTimeMillis: Int64 public let metadata: [String: Any] public init( trigger: String, scheduledTimeMillis: Int64?, fetchTimeMillis: Int64, metadata: [String: Any] = [:] ) { self.trigger = trigger self.scheduledTimeMillis = scheduledTimeMillis self.fetchTimeMillis = fetchTimeMillis self.metadata = metadata } } /// Host app implements this and registers via `DailyNotificationPlugin.registerNativeFetcher(_:)`. public protocol NativeNotificationContentFetcher: AnyObject { /// Called when TypeScript invokes `configureNativeFetcher` (mirrors Android `NativeNotificationContentFetcher.configure`). func configure(apiBaseUrl: String, activeDid: String, jwtToken: String, jwtTokenPool: [String]?) /// Background-safe fetch; return empty array when there is nothing to show (not an error). func fetchContent(context: FetchContext) async throws -> [NotificationContent] } public extension NativeNotificationContentFetcher { func configure(apiBaseUrl: String, activeDid: String, jwtToken: String, jwtTokenPool: [String]?) { // Default: no-op } } /// Holds the registered host fetcher (thread-safe). public final class NativeNotificationFetcherRegistry { public static let shared = NativeNotificationFetcherRegistry() private let lock = NSLock() private weak var weakFetcher: NativeNotificationContentFetcher? private init() {} public var fetcher: NativeNotificationContentFetcher? { lock.lock() defer { lock.unlock() } return weakFetcher } public func set(_ fetcher: NativeNotificationContentFetcher?) { lock.lock() weakFetcher = fetcher lock.unlock() } } enum NativeFetcherTimeoutError: Error { case timedOut(ms: Int) } func withTimeout(milliseconds: Int, operation: @escaping () async throws -> T) async throws -> T { try await withThrowingTaskGroup(of: T.self) { group in group.addTask { try await operation() } group.addTask { try await Task.sleep(nanoseconds: UInt64(milliseconds) * 1_000_000) throw NativeFetcherTimeoutError.timedOut(ms: milliseconds) } let first = try await group.next()! group.cancelAll() return first } }