Fixed critical compilation errors preventing iOS plugin build: - Updated logger API calls from logger.debug(TAG, msg) to logger.log(.debug, msg) across all iOS plugin files to match DailyNotificationLogger interface - Fixed async/await concurrency in makeConditionalRequest using semaphore pattern - Fixed NotificationContent immutability by creating new instances instead of mutation - Changed private access control to internal for extension-accessible methods - Added iOS 15.0+ availability checks for interruptionLevel property - Fixed static member references using Self.MEMBER_NAME syntax - Added missing .scheduling case to exhaustive switch statement - Fixed variable initialization in retry state closures Added DailyNotificationStorage.swift implementation matching Android pattern. Updated build scripts with improved error reporting and full log visibility. iOS plugin now compiles successfully. All build errors resolved.
293 lines
11 KiB
Swift
293 lines
11 KiB
Swift
//
|
|
// DailyNotificationCallbacks.swift
|
|
// DailyNotificationPlugin
|
|
//
|
|
// Created by Matthew Raymer on 2025-09-22
|
|
// Copyright © 2025 TimeSafari. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import Capacitor
|
|
import CoreData
|
|
|
|
/**
|
|
* Callback management for iOS Daily Notification Plugin
|
|
* Implements HTTP and local callback delivery with error handling
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.1.0
|
|
* @created 2025-09-22 09:22:32 UTC
|
|
*/
|
|
extension DailyNotificationPlugin {
|
|
|
|
// MARK: - Callback Management
|
|
|
|
@objc func registerCallback(_ call: CAPPluginCall) {
|
|
guard let name = call.getString("name"),
|
|
let callbackConfig = call.getObject("callback") else {
|
|
call.reject("Callback name and config required")
|
|
return
|
|
}
|
|
|
|
print("DNP-PLUGIN: Registering callback: \(name)")
|
|
|
|
do {
|
|
try registerCallback(name: name, config: callbackConfig)
|
|
call.resolve()
|
|
} catch {
|
|
print("DNP-PLUGIN: Failed to register callback: \(error)")
|
|
call.reject("Callback registration failed: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
@objc func unregisterCallback(_ call: CAPPluginCall) {
|
|
guard let name = call.getString("name") else {
|
|
call.reject("Callback name required")
|
|
return
|
|
}
|
|
|
|
print("DNP-PLUGIN: Unregistering callback: \(name)")
|
|
|
|
do {
|
|
try unregisterCallback(name: name)
|
|
call.resolve()
|
|
} catch {
|
|
print("DNP-PLUGIN: Failed to unregister callback: \(error)")
|
|
call.reject("Callback unregistration failed: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
@objc func getRegisteredCallbacks(_ call: CAPPluginCall) {
|
|
Task {
|
|
do {
|
|
let callbacks = try await getRegisteredCallbacks()
|
|
call.resolve(["callbacks": callbacks])
|
|
} catch {
|
|
print("DNP-PLUGIN: Failed to get registered callbacks: \(error)")
|
|
call.reject("Callback retrieval failed: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Content Management
|
|
|
|
@objc func getContentCache(_ call: CAPPluginCall) {
|
|
Task {
|
|
do {
|
|
let cache = try await getContentCache()
|
|
call.resolve(cache)
|
|
} catch {
|
|
print("DNP-PLUGIN: Failed to get content cache: \(error)")
|
|
call.reject("Content cache retrieval failed: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func clearContentCache(_ call: CAPPluginCall) {
|
|
Task {
|
|
do {
|
|
try await clearContentCache()
|
|
call.resolve()
|
|
} catch {
|
|
print("DNP-PLUGIN: Failed to clear content cache: \(error)")
|
|
call.reject("Content cache clearing failed: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func getContentHistory(_ call: CAPPluginCall) {
|
|
Task {
|
|
do {
|
|
let history = try await getContentHistory()
|
|
call.resolve(["history": history])
|
|
} catch {
|
|
print("DNP-PLUGIN: Failed to get content history: \(error)")
|
|
call.reject("Content history retrieval failed: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Private Callback Implementation
|
|
|
|
func fireCallbacks(eventType: String, payload: [String: Any]) async throws {
|
|
// Get registered callbacks from Core Data
|
|
let context = persistenceController.container.viewContext
|
|
let request: NSFetchRequest<Callback> = Callback.fetchRequest()
|
|
request.predicate = NSPredicate(format: "enabled == YES")
|
|
|
|
let callbacks = try context.fetch(request)
|
|
|
|
for callback in callbacks {
|
|
do {
|
|
try await deliverCallback(callback: callback, eventType: eventType, payload: payload)
|
|
} catch {
|
|
print("DNP-CB-FAILURE: Callback \(callback.id ?? "unknown") failed: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func deliverCallback(callback: Callback, eventType: String, payload: [String: Any]) async throws {
|
|
guard let callbackId = callback.id,
|
|
let kind = callback.kind else { return }
|
|
|
|
let event = [
|
|
"id": callbackId,
|
|
"at": Date().timeIntervalSince1970,
|
|
"type": eventType,
|
|
"payload": payload
|
|
] as [String: Any]
|
|
|
|
switch kind {
|
|
case "http":
|
|
try await deliverHttpCallback(callback: callback, event: event)
|
|
case "local":
|
|
try await deliverLocalCallback(callback: callback, event: event)
|
|
default:
|
|
print("DNP-CB-UNKNOWN: Unknown callback kind: \(kind)")
|
|
}
|
|
}
|
|
|
|
private func deliverHttpCallback(callback: Callback, event: [String: Any]) async throws {
|
|
guard let target = callback.target,
|
|
let url = URL(string: target) else {
|
|
throw NSError(domain: "DailyNotificationPlugin", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid callback target"])
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.httpBody = try JSONSerialization.data(withJSONObject: event)
|
|
|
|
let (_, response) = try await URLSession.shared.data(for: request)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse,
|
|
httpResponse.statusCode == 200 else {
|
|
throw NSError(domain: "DailyNotificationPlugin", code: 2, userInfo: [NSLocalizedDescriptionKey: "HTTP callback failed"])
|
|
}
|
|
|
|
print("DNP-CB-HTTP-SUCCESS: HTTP callback delivered to \(target)")
|
|
}
|
|
|
|
private func deliverLocalCallback(callback: Callback, event: [String: Any]) async throws {
|
|
// Local callback implementation would go here
|
|
print("DNP-CB-LOCAL: Local callback delivered for \(callback.id ?? "unknown")")
|
|
}
|
|
|
|
private func registerCallback(name: String, config: [String: Any]) throws {
|
|
let context = persistenceController.container.viewContext
|
|
|
|
let callback = Callback(context: context)
|
|
callback.id = name
|
|
callback.kind = config["kind"] as? String ?? "local"
|
|
callback.target = config["target"] as? String ?? ""
|
|
callback.enabled = true
|
|
callback.createdAt = Date()
|
|
|
|
try context.save()
|
|
print("DNP-CB-REGISTER: Callback \(name) registered")
|
|
}
|
|
|
|
private func unregisterCallback(name: String) throws {
|
|
let context = persistenceController.container.viewContext
|
|
let request: NSFetchRequest<Callback> = Callback.fetchRequest()
|
|
request.predicate = NSPredicate(format: "id == %@", name)
|
|
|
|
let callbacks = try context.fetch(request)
|
|
for callback in callbacks {
|
|
context.delete(callback)
|
|
}
|
|
|
|
try context.save()
|
|
print("DNP-CB-UNREGISTER: Callback \(name) unregistered")
|
|
}
|
|
|
|
private func getRegisteredCallbacks() async throws -> [String] {
|
|
let context = persistenceController.container.viewContext
|
|
let request: NSFetchRequest<Callback> = Callback.fetchRequest()
|
|
|
|
let callbacks = try context.fetch(request)
|
|
return callbacks.compactMap { $0.id }
|
|
}
|
|
|
|
private func getContentCache() async throws -> [String: Any] {
|
|
guard let latestContent = try await getLatestContent() else {
|
|
return [:]
|
|
}
|
|
return latestContent
|
|
}
|
|
|
|
private func clearContentCache() async throws {
|
|
let context = persistenceController.container.viewContext
|
|
let request: NSFetchRequest<ContentCache> = ContentCache.fetchRequest()
|
|
|
|
let results = try context.fetch(request)
|
|
for content in results {
|
|
context.delete(content)
|
|
}
|
|
|
|
try context.save()
|
|
print("DNP-CACHE-CLEAR: Content cache cleared")
|
|
}
|
|
|
|
private func getContentHistory() async throws -> [[String: Any]] {
|
|
let context = persistenceController.container.viewContext
|
|
let request: NSFetchRequest<History> = History.fetchRequest()
|
|
request.sortDescriptors = [NSSortDescriptor(keyPath: \History.occurredAt, ascending: false)]
|
|
request.fetchLimit = 100
|
|
|
|
let results = try context.fetch(request)
|
|
return results.map { history in
|
|
[
|
|
"id": history.id ?? "",
|
|
"kind": history.kind ?? "",
|
|
"occurredAt": history.occurredAt?.timeIntervalSince1970 ?? 0,
|
|
"outcome": history.outcome ?? "",
|
|
"durationMs": history.durationMs
|
|
]
|
|
}
|
|
}
|
|
|
|
func getHealthStatus() async throws -> [String: Any] {
|
|
let context = persistenceController.container.viewContext
|
|
|
|
// Get next runs (simplified)
|
|
let nextRuns = [Date().addingTimeInterval(3600).timeIntervalSince1970,
|
|
Date().addingTimeInterval(86400).timeIntervalSince1970]
|
|
|
|
// Get recent history
|
|
let historyRequest: NSFetchRequest<History> = History.fetchRequest()
|
|
historyRequest.predicate = NSPredicate(format: "occurredAt >= %@", Date().addingTimeInterval(-86400) as NSDate)
|
|
historyRequest.sortDescriptors = [NSSortDescriptor(keyPath: \History.occurredAt, ascending: false)]
|
|
historyRequest.fetchLimit = 10
|
|
|
|
let recentHistory = try context.fetch(historyRequest)
|
|
let lastOutcomes = recentHistory.map { $0.outcome ?? "" }
|
|
|
|
// Get cache age
|
|
let cacheRequest: NSFetchRequest<ContentCache> = ContentCache.fetchRequest()
|
|
cacheRequest.sortDescriptors = [NSSortDescriptor(keyPath: \ContentCache.fetchedAt, ascending: false)]
|
|
cacheRequest.fetchLimit = 1
|
|
|
|
let latestCache = try context.fetch(cacheRequest).first
|
|
let cacheAgeMs = latestCache?.fetchedAt?.timeIntervalSinceNow ?? 0
|
|
|
|
return [
|
|
"nextRuns": nextRuns,
|
|
"lastOutcomes": lastOutcomes,
|
|
"cacheAgeMs": abs(cacheAgeMs * 1000),
|
|
"staleArmed": abs(cacheAgeMs) > 3600,
|
|
"queueDepth": recentHistory.count,
|
|
"circuitBreakers": [
|
|
"total": 0,
|
|
"open": 0,
|
|
"failures": 0
|
|
],
|
|
"performance": [
|
|
"avgFetchTime": 0,
|
|
"avgNotifyTime": 0,
|
|
"successRate": 1.0
|
|
]
|
|
]
|
|
}
|
|
}
|