Files
daily-notification-plugin/ios/Plugin/DailyNotificationBackgroundTasks.swift
Server 5844b92e18 feat(ios): implement Phase 1 permission methods and fix build issues
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
2025-11-13 05:14:24 -08:00

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