Implement checkPermissionStatus() and requestNotificationPermissions() methods for iOS plugin, matching Android functionality. Fix compilation errors across plugin files and add comprehensive build/test infrastructure. Key Changes: - Add checkPermissionStatus() and requestNotificationPermissions() methods - Fix 13+ categories of Swift compilation errors (type conversions, logger API, access control, async/await, etc.) - Create DailyNotificationScheduler, DailyNotificationStorage, DailyNotificationStateActor, and DailyNotificationErrorCodes components - Fix CoreData initialization to handle missing model gracefully for Phase 1 - Add iOS test app build script with simulator auto-detection - Update directive with lessons learned from build and permission work Build Status: ✅ BUILD SUCCEEDED Test App: ✅ Ready for iOS Simulator testing Files Modified: - doc/directives/0003-iOS-Android-Parity-Directive.md (lessons learned) - ios/Plugin/DailyNotificationPlugin.swift (Phase 1 methods) - ios/Plugin/DailyNotificationModel.swift (CoreData fix) - 11+ other plugin files (compilation fixes) Files Added: - ios/Plugin/DailyNotificationScheduler.swift - ios/Plugin/DailyNotificationStorage.swift - ios/Plugin/DailyNotificationStateActor.swift - ios/Plugin/DailyNotificationErrorCodes.swift - scripts/build-ios-test-app.sh - scripts/setup-ios-test-app.sh - test-apps/ios-test-app/ (full test app) - Multiple Phase 1 documentation files
185 lines
7.0 KiB
Swift
185 lines
7.0 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 {
|
|
// Phase 1: History recording is not yet implemented
|
|
// TODO: Phase 2 - Implement history with CoreData
|
|
print("DNP-HISTORY: \(kind) - \(outcome) (Phase 2 - not implemented)")
|
|
}
|
|
}
|