feat(ios): add Core Data DAO layer and unit tests
Implement comprehensive data access layer for Core Data entities: - Add NotificationContentDAO, NotificationDeliveryDAO, and NotificationConfigDAO with full CRUD operations and query helpers - Add DailyNotificationDataConversions utility for type conversions (Date ↔ Int64, Int ↔ Int32, JSON, optional strings) - Update PersistenceController with entity verification and migration policies - Add comprehensive unit tests for all DAO classes and data conversions - Update Core Data model with NotificationContent, NotificationDelivery, and NotificationConfig entities (relationships and indexes) - Integrate ReactivationManager into DailyNotificationPlugin.load() DAO Features: - Create/Insert methods with dictionary support - Read/Query methods with predicates (by timesafariDid, notificationType, scheduledTime range, deliveryStatus, etc.) - Update methods (touch, updateDeliveryStatus, recordUserInteraction) - Delete methods (by ID, by key, delete all) - Relationship management (NotificationContent ↔ NotificationDelivery) - Cascade delete support Test Coverage: - 328 lines: DailyNotificationDataConversionsTests (time, numeric, string, JSON) - 490 lines: NotificationContentDAOTests (CRUD, queries, updates) - 415 lines: NotificationDeliveryDAOTests (CRUD, relationships, cascade delete) - 412 lines: NotificationConfigDAOTests (CRUD, queries, active filtering) All tests use in-memory Core Data stack for isolation and speed. Completes sections 4.4, 4.5, and 6.0 of iOS implementation checklist.
This commit is contained in:
@@ -35,6 +35,9 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
var storage: DailyNotificationStorage?
|
||||
var scheduler: DailyNotificationScheduler?
|
||||
|
||||
// Phase 1: Reactivation manager for recovery
|
||||
var reactivationManager: DailyNotificationReactivationManager?
|
||||
|
||||
// Phase 1: Concurrency actor for thread-safe state access
|
||||
@available(iOS 13.0, *)
|
||||
var stateActor: DailyNotificationStateActor?
|
||||
@@ -51,6 +54,13 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
storage = DailyNotificationStorage(databasePath: database.getPath())
|
||||
scheduler = DailyNotificationScheduler()
|
||||
|
||||
// Initialize reactivation manager for recovery
|
||||
reactivationManager = DailyNotificationReactivationManager(
|
||||
database: database,
|
||||
storage: storage!,
|
||||
scheduler: scheduler!
|
||||
)
|
||||
|
||||
// Initialize state actor for thread-safe access
|
||||
if #available(iOS 13.0, *) {
|
||||
stateActor = DailyNotificationStateActor(
|
||||
@@ -59,6 +69,9 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
)
|
||||
}
|
||||
|
||||
// Perform recovery on app launch (async, non-blocking)
|
||||
reactivationManager?.performRecovery()
|
||||
|
||||
NSLog("DNP-DEBUG: DailyNotificationPlugin.load() completed - initialization done")
|
||||
print("DNP-PLUGIN: Daily Notification Plugin loaded on iOS")
|
||||
}
|
||||
@@ -1259,6 +1272,202 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iOS-Specific Methods
|
||||
|
||||
/**
|
||||
* Get notification permission status (iOS-specific)
|
||||
*
|
||||
* Returns detailed permission status matching API.md specification
|
||||
*
|
||||
* @param call Plugin call
|
||||
*/
|
||||
@objc func getNotificationPermissionStatus(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
guard let scheduler = scheduler else {
|
||||
throw NSError(domain: "DailyNotificationPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "Scheduler not initialized"])
|
||||
}
|
||||
|
||||
let status = await scheduler.checkPermissionStatus()
|
||||
|
||||
let result: [String: Any] = [
|
||||
"authorized": status == .authorized,
|
||||
"denied": status == .denied,
|
||||
"notDetermined": status == .notDetermined,
|
||||
"provisional": status == .provisional
|
||||
]
|
||||
|
||||
DispatchQueue.main.async {
|
||||
call.resolve(result)
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
call.reject("Failed to get permission status: \(error.localizedDescription)", "permission_status_failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permission (iOS-specific)
|
||||
*
|
||||
* @param call Plugin call
|
||||
*/
|
||||
@objc func requestNotificationPermission(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
guard let scheduler = scheduler else {
|
||||
throw NSError(domain: "DailyNotificationPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "Scheduler not initialized"])
|
||||
}
|
||||
|
||||
let granted = await scheduler.requestPermissions()
|
||||
|
||||
let result: [String: Any] = [
|
||||
"granted": granted
|
||||
]
|
||||
|
||||
DispatchQueue.main.async {
|
||||
call.resolve(result)
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
call.reject("Failed to request permission: \(error.localizedDescription)", "permission_request_failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending notifications (iOS-specific)
|
||||
*
|
||||
* @param call Plugin call
|
||||
*/
|
||||
@objc func getPendingNotifications(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
let requests = try await notificationCenter.pendingNotificationRequests()
|
||||
|
||||
var notifications: [[String: Any]] = []
|
||||
for request in requests {
|
||||
let content = request.content
|
||||
var triggerDate: Int64 = 0
|
||||
|
||||
if let calendarTrigger = request.trigger as? UNCalendarNotificationTrigger {
|
||||
if let nextDate = calendarTrigger.nextTriggerDate() {
|
||||
triggerDate = Int64(nextDate.timeIntervalSince1970 * 1000)
|
||||
}
|
||||
} else if let timeIntervalTrigger = request.trigger as? UNTimeIntervalNotificationTrigger {
|
||||
if let nextDate = timeIntervalTrigger.nextTriggerDate() {
|
||||
triggerDate = Int64(nextDate.timeIntervalSince1970 * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
let notification: [String: Any] = [
|
||||
"identifier": request.identifier,
|
||||
"title": content.title,
|
||||
"body": content.body,
|
||||
"triggerDate": triggerDate,
|
||||
"triggerType": request.trigger is UNCalendarNotificationTrigger ? "calendar" : (request.trigger is UNTimeIntervalNotificationTrigger ? "timeInterval" : "location"),
|
||||
"repeats": request.trigger?.repeats ?? false
|
||||
]
|
||||
notifications.append(notification)
|
||||
}
|
||||
|
||||
let result: [String: Any] = [
|
||||
"count": notifications.count,
|
||||
"notifications": notifications
|
||||
]
|
||||
|
||||
DispatchQueue.main.async {
|
||||
call.resolve(result)
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
call.reject("Failed to get pending notifications: \(error.localizedDescription)", "pending_notifications_failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get background task status (iOS-specific)
|
||||
*
|
||||
* @param call Plugin call
|
||||
*/
|
||||
@objc func getBackgroundTaskStatus(_ call: CAPPluginCall) {
|
||||
let registeredIdentifiers = backgroundTaskScheduler.registeredTaskIdentifiers
|
||||
let fetchTaskRegistered = registeredIdentifiers.contains(fetchTaskIdentifier)
|
||||
let notifyTaskRegistered = registeredIdentifiers.contains(notifyTaskIdentifier)
|
||||
|
||||
// Note: Background App Refresh status cannot be checked programmatically
|
||||
// User must check in Settings app
|
||||
|
||||
let result: [String: Any] = [
|
||||
"fetchTaskRegistered": fetchTaskRegistered,
|
||||
"notifyTaskRegistered": notifyTaskRegistered,
|
||||
"lastFetchExecution": storage?.getLastSuccessfulRun() ?? NSNull(),
|
||||
"lastNotifyExecution": NSNull(), // TODO: Track notify execution
|
||||
"backgroundRefreshEnabled": NSNull() // Cannot check programmatically
|
||||
]
|
||||
|
||||
call.resolve(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open notification settings (iOS-specific)
|
||||
*
|
||||
* @param call Plugin call
|
||||
*/
|
||||
@objc func openNotificationSettings(_ call: CAPPluginCall) {
|
||||
if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
|
||||
if UIApplication.shared.canOpenURL(settingsUrl) {
|
||||
UIApplication.shared.open(settingsUrl) { success in
|
||||
DispatchQueue.main.async {
|
||||
if success {
|
||||
call.resolve()
|
||||
} else {
|
||||
call.reject("Failed to open notification settings", "open_settings_failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
call.reject("Cannot open settings URL", "open_settings_failed")
|
||||
}
|
||||
} else {
|
||||
call.reject("Invalid settings URL", "open_settings_failed")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Background App Refresh settings (iOS-specific)
|
||||
*
|
||||
* Note: iOS doesn't provide a direct URL to Background App Refresh settings.
|
||||
* This opens the app's settings page where user can find Background App Refresh.
|
||||
*
|
||||
* @param call Plugin call
|
||||
*/
|
||||
@objc func openBackgroundAppRefreshSettings(_ call: CAPPluginCall) {
|
||||
// iOS doesn't have a direct URL to Background App Refresh settings
|
||||
// Open app settings instead, where user can find Background App Refresh
|
||||
if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
|
||||
if UIApplication.shared.canOpenURL(settingsUrl) {
|
||||
UIApplication.shared.open(settingsUrl) { success in
|
||||
DispatchQueue.main.async {
|
||||
if success {
|
||||
call.resolve()
|
||||
} else {
|
||||
call.reject("Failed to open settings", "open_settings_failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
call.reject("Cannot open settings URL", "open_settings_failed")
|
||||
}
|
||||
} else {
|
||||
call.reject("Invalid settings URL", "open_settings_failed")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Channel Methods (iOS Parity with Android)
|
||||
|
||||
/**
|
||||
@@ -1494,6 +1703,14 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
methods.append(CAPPluginMethod(name: "checkPermissionStatus", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "requestNotificationPermissions", returnType: CAPPluginReturnPromise))
|
||||
|
||||
// iOS-specific methods
|
||||
methods.append(CAPPluginMethod(name: "getNotificationPermissionStatus", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "requestNotificationPermission", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "getPendingNotifications", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "getBackgroundTaskStatus", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "openNotificationSettings", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "openBackgroundAppRefreshSettings", returnType: CAPPluginReturnPromise))
|
||||
|
||||
// Channel methods (iOS parity with Android)
|
||||
methods.append(CAPPluginMethod(name: "isChannelEnabled", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "openChannelSettings", returnType: CAPPluginReturnPromise))
|
||||
|
||||
Reference in New Issue
Block a user