You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
173 lines
6.5 KiB
173 lines
6.5 KiB
//
|
|
// 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 {
|
|
|
|
private 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
private 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")
|
|
}
|
|
|
|
private 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()
|
|
}
|
|
}
|
|
|