Files
daily-notification-plugin/ios/Plugin/DailyNotificationBackgroundTasks.swift
Matthew Raymer 8ded555a21 fix(ios): resolve compilation errors and enable successful build
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.
2025-11-04 22:22:02 -08:00

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()
}
}