Files
daily-notification-plugin/ios/Plugin/DailyNotificationBackgroundTasks.swift
Matthew Raymer a070ec9f0b feat(ios): complete remaining Phase 2 enhancements
Implement CoreData history and clarify fetcher parameter usage.

Changes:
- DailyNotificationBackgroundTasks: Implement CoreData history recording
  - recordHistory(): Now uses PersistenceController and History.create()
  - Records kind and outcome to CoreData History entity
  - Removed TODO, fully implemented
- DailyNotificationPlugin: Clarify fetcher parameter
  - Updated comment: fetcher parameter is unused
  - fetchScheduler handles prefetch scheduling (already implemented)
- DailyNotificationReactivationManager: Clarify fetcher parameter
  - Updated comment: fetcher parameter is unused
  - fetchScheduler handles prefetch scheduling (already implemented)

Phase 2 Progress: 6 of 8 enhancements complete
-  Rolling window maintenance
-  TTL validation
-  Database statistics
-  Metrics recording
-  CoreData history (this commit)
-  Fetcher instances clarified (this commit)
-  NotificationContent properties (deliveryStatus, lastDeliveryAttempt) - requires model changes

Verification:
- TypeScript typecheck: PASS
- Tests: PASS (115 tests, 8 test suites)
- No linter errors
2025-12-24 07:32:43 +00:00

209 lines
7.7 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 {
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 {
// Phase 1: Use DailyNotificationStorage instead of CoreData
// Convert dictionary to NotificationContent and store via stateActor
guard let id = content["id"] as? String else {
throw NSError(domain: "DailyNotification", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing content ID"])
}
let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
let notificationContent = NotificationContent(
id: id,
title: content["title"] as? String,
body: content["content"] as? String ?? content["body"] as? String,
scheduledTime: currentTime, // Will be updated by scheduler
fetchedAt: currentTime,
url: content["url"] as? String,
payload: content,
etag: content["etag"] as? String
)
// Store via stateActor if available
if #available(iOS 13.0, *), let stateActor = stateActor {
await stateActor.saveNotificationContent(notificationContent)
} else if let storage = storage {
storage.saveNotificationContent(notificationContent)
}
print("DNP-CACHE-STORE: Content stored via DailyNotificationStorage")
}
private func getLatestContent() async throws -> [String: Any]? {
// Phase 1: Get from DailyNotificationStorage
if #available(iOS 13.0, *), let stateActor = stateActor {
// Get latest notification from storage
// For now, return nil - this will be implemented when needed
return nil
} else if let storage = storage {
// Access storage directly if stateActor not available
// For now, return nil - this will be implemented when needed
return nil
}
return nil
}
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 {
guard let context = PersistenceController.shared.viewContext else {
print("DNP-HISTORY: Cannot record history - CoreData not available")
return
}
let historyId = UUID().uuidString
let history = History.create(
in: context,
id: historyId,
refId: nil,
kind: kind,
occurredAt: Date(),
durationMs: 0,
outcome: outcome,
diagJson: nil
)
do {
if context.hasChanges {
try context.save()
print("DNP-HISTORY: Recorded \(kind) - \(outcome)")
}
} catch {
print("DNP-HISTORY: Failed to save history: \(error.localizedDescription)")
context.rollback()
throw error
}
}
}