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.
174 lines
6.5 KiB
Swift
174 lines
6.5 KiB
Swift
//
|
|
// DailyNotificationBackgroundTasks.swift
|
|
// DailyNotificationPlugin
|
|
//
|
|
// Created by Matthew Raymer on 2025-09-22
|
|
// Copyright © 2025 TimeSafari. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import BackgroundTasks
|
|
import UserNotifications
|
|
import CoreData
|
|
|
|
/**
|
|
* Background task handlers for iOS Daily Notification Plugin
|
|
* Implements BGTaskScheduler handlers for content fetch and notification delivery
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.1.0
|
|
* @created 2025-09-22 09:22:32 UTC
|
|
*/
|
|
extension DailyNotificationPlugin {
|
|
|
|
func handleBackgroundFetch(task: BGAppRefreshTask) {
|
|
print("DNP-FETCH-START: Background fetch task started")
|
|
|
|
task.expirationHandler = {
|
|
print("DNP-FETCH-TIMEOUT: Background fetch task expired")
|
|
task.setTaskCompleted(success: false)
|
|
}
|
|
|
|
Task {
|
|
do {
|
|
let startTime = Date()
|
|
let content = try await performContentFetch()
|
|
|
|
// Store content in Core Data
|
|
try await storeContent(content)
|
|
|
|
let duration = Date().timeIntervalSince(startTime)
|
|
print("DNP-FETCH-SUCCESS: Content fetch completed in \(duration)s")
|
|
|
|
// Fire callbacks
|
|
try await fireCallbacks(eventType: "onFetchSuccess", payload: content)
|
|
|
|
task.setTaskCompleted(success: true)
|
|
|
|
} catch {
|
|
print("DNP-FETCH-FAILURE: Content fetch failed: \(error)")
|
|
task.setTaskCompleted(success: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleBackgroundNotify(task: BGProcessingTask) {
|
|
print("DNP-NOTIFY-START: Background notify task started")
|
|
|
|
task.expirationHandler = {
|
|
print("DNP-NOTIFY-TIMEOUT: Background notify task expired")
|
|
task.setTaskCompleted(success: false)
|
|
}
|
|
|
|
Task {
|
|
do {
|
|
let startTime = Date()
|
|
|
|
// Get latest cached content
|
|
guard let latestContent = try await getLatestContent() else {
|
|
print("DNP-NOTIFY-SKIP: No cached content available")
|
|
task.setTaskCompleted(success: true)
|
|
return
|
|
}
|
|
|
|
// Check TTL
|
|
if isContentExpired(content: latestContent) {
|
|
print("DNP-NOTIFY-SKIP-TTL: Content TTL expired, skipping notification")
|
|
try await recordHistory(kind: "notify", outcome: "skipped_ttl")
|
|
task.setTaskCompleted(success: true)
|
|
return
|
|
}
|
|
|
|
// Show notification
|
|
try await showNotification(content: latestContent)
|
|
|
|
let duration = Date().timeIntervalSince(startTime)
|
|
print("DNP-NOTIFY-SUCCESS: Notification displayed in \(duration)s")
|
|
|
|
// Fire callbacks
|
|
try await fireCallbacks(eventType: "onNotifyDelivered", payload: latestContent)
|
|
|
|
task.setTaskCompleted(success: true)
|
|
|
|
} catch {
|
|
print("DNP-NOTIFY-FAILURE: Notification failed: \(error)")
|
|
task.setTaskCompleted(success: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func performContentFetch() async throws -> [String: Any] {
|
|
// Mock content fetch implementation
|
|
// In production, this would make actual HTTP requests
|
|
let mockContent = [
|
|
"id": "fetch_\(Date().timeIntervalSince1970)",
|
|
"timestamp": Date().timeIntervalSince1970,
|
|
"content": "Daily notification content from iOS",
|
|
"source": "ios_platform"
|
|
] as [String: Any]
|
|
|
|
return mockContent
|
|
}
|
|
|
|
private func storeContent(_ content: [String: Any]) async throws {
|
|
let context = persistenceController.container.viewContext
|
|
|
|
let contentEntity = ContentCache(context: context)
|
|
contentEntity.id = content["id"] as? String
|
|
contentEntity.fetchedAt = Date(timeIntervalSince1970: content["timestamp"] as? TimeInterval ?? 0)
|
|
contentEntity.ttlSeconds = 3600 // 1 hour default TTL
|
|
contentEntity.payload = try JSONSerialization.data(withJSONObject: content)
|
|
contentEntity.meta = "fetched_by_ios_bg_task"
|
|
|
|
try context.save()
|
|
print("DNP-CACHE-STORE: Content stored in Core Data")
|
|
}
|
|
|
|
func getLatestContent() async throws -> [String: Any]? {
|
|
let context = persistenceController.container.viewContext
|
|
let request: NSFetchRequest<ContentCache> = ContentCache.fetchRequest()
|
|
request.sortDescriptors = [NSSortDescriptor(keyPath: \ContentCache.fetchedAt, ascending: false)]
|
|
request.fetchLimit = 1
|
|
|
|
let results = try context.fetch(request)
|
|
guard let latest = results.first else { return nil }
|
|
|
|
return try JSONSerialization.jsonObject(with: latest.payload!) as? [String: Any]
|
|
}
|
|
|
|
private func isContentExpired(content: [String: Any]) -> Bool {
|
|
guard let timestamp = content["timestamp"] as? TimeInterval else { return true }
|
|
let fetchedAt = Date(timeIntervalSince1970: timestamp)
|
|
let ttlExpiry = fetchedAt.addingTimeInterval(3600) // 1 hour TTL
|
|
return Date() > ttlExpiry
|
|
}
|
|
|
|
private func showNotification(content: [String: Any]) async throws {
|
|
let notificationContent = UNMutableNotificationContent()
|
|
notificationContent.title = "Daily Notification"
|
|
notificationContent.body = content["content"] as? String ?? "Your daily update is ready"
|
|
notificationContent.sound = .default
|
|
|
|
let request = UNNotificationRequest(
|
|
identifier: "daily-notification-\(Date().timeIntervalSince1970)",
|
|
content: notificationContent,
|
|
trigger: nil // Immediate delivery
|
|
)
|
|
|
|
try await notificationCenter.add(request)
|
|
print("DNP-NOTIFY-DISPLAY: Notification displayed")
|
|
}
|
|
|
|
private func recordHistory(kind: String, outcome: String) async throws {
|
|
let context = persistenceController.container.viewContext
|
|
|
|
let history = History(context: context)
|
|
history.id = "\(kind)_\(Date().timeIntervalSince1970)"
|
|
history.kind = kind
|
|
history.occurredAt = Date()
|
|
history.outcome = outcome
|
|
|
|
try context.save()
|
|
}
|
|
}
|