diff --git a/ios/Plugin/DailyNotificationPlugin.swift b/ios/Plugin/DailyNotificationPlugin.swift index c0c3ccc..b8e62c5 100644 --- a/ios/Plugin/DailyNotificationPlugin.swift +++ b/ios/Plugin/DailyNotificationPlugin.swift @@ -2170,55 +2170,89 @@ public class DailyNotificationPlugin: CAPPlugin { } /** - * Get pending notifications (iOS-specific) - * + * Get pending notifications (iOS diagnostics) + * + * Exposes UNUserNotificationCenter pending requests with bridge-safe fields only. + * * @param call Plugin call */ @objc func getPendingNotifications(_ call: CAPPluginCall) { - Task { - do { - // Delegate to UNUserNotificationCenter for pending requests - 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 + diagnosticLog("getPendingNotifications called") + notificationCenter.getPendingNotificationRequests { requests in + var notifications: [[String: Any]] = [] + for request in requests { + let content = request.content + let (triggerTimestamp, triggerDateIso) = Self.pendingTriggerFields(from: request.trigger) + var item: [String: Any] = [ + "identifier": request.identifier, + "title": content.title, + "body": content.body, + "triggerType": Self.pendingTriggerTypeString(from: request.trigger), + "repeats": request.trigger?.repeats ?? false ] - - DispatchQueue.main.async { - call.resolve(result) - } - } catch { - DispatchQueue.main.async { - call.reject("Failed to get pending notifications: \(error.localizedDescription)", "pending_notifications_failed") + if let triggerTimestamp = triggerTimestamp { + item["triggerTimestamp"] = triggerTimestamp + item["triggerDate"] = triggerTimestamp + } else { + item["triggerTimestamp"] = NSNull() + item["triggerDate"] = NSNull() } + item["triggerDateIso"] = triggerDateIso ?? NSNull() + notifications.append(item) } + self.diagnosticLog("getPendingNotifications resolved count=\(notifications.count)") + call.resolve([ + "count": notifications.count, + "notifications": notifications + ]) + } + } + + /** + * Get delivered notification metadata (iOS diagnostics) + * + * Lightweight delivery snapshot; omits userInfo and attachment payloads. + * + * @param call Plugin call + */ + @objc func getDeliveredNotifications(_ call: CAPPluginCall) { + diagnosticLog("getDeliveredNotifications called") + notificationCenter.getDeliveredNotifications { delivered in + var notifications: [[String: Any]] = [] + for notification in delivered { + let request = notification.request + let content = request.content + let deliveredTimestamp = Int64(notification.date.timeIntervalSince1970 * 1000) + notifications.append([ + "identifier": request.identifier, + "deliveredTimestamp": deliveredTimestamp, + "deliveredDateIso": Self.iso8601String(from: notification.date) ?? NSNull(), + "title": content.title, + "body": content.body, + "categoryIdentifier": content.categoryIdentifier.isEmpty ? NSNull() : content.categoryIdentifier + ]) + } + self.diagnosticLog("getDeliveredNotifications resolved count=\(notifications.count)") + call.resolve([ + "count": notifications.count, + "notifications": notifications + ]) + } + } + + /** + * Get notification authorization and presentation settings (iOS diagnostics) + * + * Always resolves with serializable settings; does not require plugin scheduler init. + * + * @param call Plugin call + */ + @objc func getNotificationSettings(_ call: CAPPluginCall) { + diagnosticLog("getNotificationSettings called") + notificationCenter.getNotificationSettings { settings in + let result = Self.serializeNotificationSettings(settings) + self.diagnosticLog("getNotificationSettings resolved authorization=\(result["authorizationStatus"] ?? "unknown")") + call.resolve(result) } } @@ -2618,6 +2652,97 @@ public class DailyNotificationPlugin: CAPPlugin { } } +// MARK: - Diagnostics helpers +extension DailyNotificationPlugin { + private func diagnosticLog(_ message: String) { + NSLog("[DailyNotificationPlugin] %@", message) + } + + private static let iso8601Formatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter + }() + + static func iso8601String(from date: Date) -> String? { + iso8601Formatter.string(from: date) + } + + static func pendingTriggerFields(from trigger: UNNotificationTrigger?) -> (Int64?, String?) { + guard let trigger = trigger else { return (nil, nil) } + var nextDate: Date? + if let calendarTrigger = trigger as? UNCalendarNotificationTrigger { + nextDate = calendarTrigger.nextTriggerDate() + } else if let timeIntervalTrigger = trigger as? UNTimeIntervalNotificationTrigger { + nextDate = timeIntervalTrigger.nextTriggerDate() + } + guard let date = nextDate else { return (nil, nil) } + let timestamp = Int64(date.timeIntervalSince1970 * 1000) + return (timestamp, iso8601String(from: date)) + } + + static func pendingTriggerTypeString(from trigger: UNNotificationTrigger?) -> String { + guard let trigger = trigger else { return "unknown" } + if trigger is UNCalendarNotificationTrigger { return "calendar" } + if trigger is UNTimeIntervalNotificationTrigger { return "timeInterval" } + if trigger is UNLocationNotificationTrigger { return "location" } + return "unknown" + } + + static func authorizationStatusString(_ status: UNAuthorizationStatus) -> String { + if #available(iOS 14.0, *), status == .ephemeral { + return "ephemeral" + } + switch status { + case .notDetermined: return "notDetermined" + case .denied: return "denied" + case .authorized: return "authorized" + case .provisional: return "provisional" + @unknown default: return "unknown" + } + } + + static func notificationSettingString(_ setting: UNNotificationSetting) -> String { + switch setting { + case .notSupported: return "notSupported" + case .disabled: return "disabled" + case .enabled: return "enabled" + @unknown default: return "unknown" + } + } + + static func showPreviewsSettingString(_ setting: UNShowPreviewsSetting) -> String { + switch setting { + case .always: return "always" + case .whenAuthenticated: return "whenAuthenticated" + case .never: return "never" + @unknown default: return "unknown" + } + } + + static func serializeNotificationSettings(_ settings: UNNotificationSettings) -> [String: Any] { + var result: [String: Any] = [ + "authorizationStatus": authorizationStatusString(settings.authorizationStatus), + "notificationCenterSetting": notificationSettingString(settings.notificationCenterSetting), + "lockScreenSetting": notificationSettingString(settings.lockScreenSetting), + "carPlaySetting": notificationSettingString(settings.carPlaySetting), + "alertSetting": notificationSettingString(settings.alertSetting), + "badgeSetting": notificationSettingString(settings.badgeSetting), + "soundSetting": notificationSettingString(settings.soundSetting), + "criticalAlertSetting": notificationSettingString(settings.criticalAlertSetting), + "showPreviewsSetting": showPreviewsSettingString(settings.showPreviewsSetting), + ] + if #available(iOS 13.0, *) { + result["announcementSetting"] = notificationSettingString(settings.announcementSetting) + } + if #available(iOS 15.0, *) { + result["scheduledDeliverySetting"] = notificationSettingString(settings.scheduledDeliverySetting) + result["timeSensitiveSetting"] = notificationSettingString(settings.timeSensitiveSetting) + } + return result + } +} + // MARK: - CAPBridgedPlugin Conformance // This extension makes the plugin conform to CAPBridgedPlugin protocol // which is required for Capacitor to discover and register the plugin @@ -2651,6 +2776,8 @@ public class DailyNotificationPlugin: CAPPlugin { 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: "getDeliveredNotifications", returnType: CAPPluginReturnPromise)) + methods.append(CAPPluginMethod(name: "getNotificationSettings", returnType: CAPPluginReturnPromise)) methods.append(CAPPluginMethod(name: "getBackgroundTaskStatus", returnType: CAPPluginReturnPromise)) methods.append(CAPPluginMethod(name: "openNotificationSettings", returnType: CAPPluginReturnPromise)) methods.append(CAPPluginMethod(name: "openBackgroundAppRefreshSettings", returnType: CAPPluginReturnPromise)) diff --git a/src/definitions.ts b/src/definitions.ts index 162f15e..8a2bab7 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -155,6 +155,84 @@ export interface PermissionStatusResult { allPermissionsGranted: boolean; } +/** iOS UNUserNotificationCenter authorization flags (diagnostics) */ +export interface NotificationPermissionStatus { + authorized: boolean; + denied: boolean; + notDetermined: boolean; + provisional: boolean; +} + +/** Pending UNNotificationRequest snapshot for debug panels (iOS) */ +export interface PendingNotificationDiagnostic { + identifier: string; + triggerTimestamp: number | null; + triggerDateIso: string | null; + title?: string; + body?: string; + /** @deprecated Prefer triggerTimestamp */ + triggerDate?: number | null; + triggerType?: 'calendar' | 'timeInterval' | 'location' | 'unknown'; + repeats?: boolean; +} + +export interface PendingNotificationsResult { + count: number; + notifications: PendingNotificationDiagnostic[]; +} + +/** Delivered UNNotification metadata for debug panels (iOS) */ +export interface DeliveredNotificationMetadata { + identifier: string; + deliveredTimestamp: number; + deliveredDateIso: string | null; + title: string; + body: string; + categoryIdentifier: string | null; +} + +export interface DeliveredNotificationsResult { + count: number; + notifications: DeliveredNotificationMetadata[]; +} + +/** + * UNNotificationSettings snapshot for debug panels (iOS). + * Not the app-level {@link NotificationSettings} schedule preferences type. + */ +export interface NotificationCenterSettingsSnapshot { + authorizationStatus: string; + notificationCenterSetting: string; + lockScreenSetting: string; + carPlaySetting: string; + alertSetting: string; + badgeSetting: string; + soundSetting: string; + criticalAlertSetting: string; + showPreviewsSetting: string; + announcementSetting?: string; + scheduledDeliverySetting?: string; + timeSensitiveSetting?: string; +} + +/** @deprecated Use PendingNotificationDiagnostic */ +export interface PendingNotification { + identifier: string; + title: string; + body: string; + triggerDate: number; + triggerType: 'calendar' | 'timeInterval' | 'location'; + repeats: boolean; +} + +export interface BackgroundTaskStatus { + fetchTaskRegistered: boolean; + notifyTaskRegistered: boolean; + lastFetchExecution: number | null; + lastNotifyExecution: number | null; + backgroundRefreshEnabled: boolean | null; +} + // Static Daily Reminder Interfaces export interface DailyReminderOptions { id: string; @@ -537,6 +615,16 @@ export interface DailyNotificationPlugin { isChannelEnabled(channelId?: string): Promise<{ enabled: boolean; channelId: string }>; openChannelSettings(channelId?: string): Promise; checkStatus(): Promise; + + // iOS local notification diagnostics (bridge-safe, no backend concepts) + getPendingNotifications(): Promise; + getDeliveredNotifications(): Promise; + getNotificationSettings(): Promise; + getNotificationPermissionStatus(): Promise; + requestNotificationPermission(): Promise<{ granted: boolean }>; + getBackgroundTaskStatus(): Promise; + openNotificationSettings(): Promise; + openBackgroundAppRefreshSettings(): Promise; // New dual scheduling methods scheduleContentFetch(config: ContentFetchConfig): Promise; diff --git a/src/web.ts b/src/web.ts index 0413b13..1b42b48 100644 --- a/src/web.ts +++ b/src/web.ts @@ -22,6 +22,11 @@ import type { CreateCallbackInput, History, HistoryStats, + PendingNotificationsResult, + DeliveredNotificationsResult, + NotificationCenterSettingsSnapshot, + NotificationPermissionStatus, + BackgroundTaskStatus, } from './definitions'; import { DailyNotificationError, ErrorCode } from './core/errors'; @@ -204,6 +209,38 @@ export class DailyNotificationWeb implements DailyNotificationPlugin { this.throwNotSupported(); } + async getPendingNotifications(): Promise { + this.throwNotSupported(); + } + + async getDeliveredNotifications(): Promise { + this.throwNotSupported(); + } + + async getNotificationSettings(): Promise { + this.throwNotSupported(); + } + + async getNotificationPermissionStatus(): Promise { + this.throwNotSupported(); + } + + async requestNotificationPermission(): Promise<{ granted: boolean }> { + this.throwNotSupported(); + } + + async getBackgroundTaskStatus(): Promise { + this.throwNotSupported(); + } + + async openNotificationSettings(): Promise { + this.throwNotSupported(); + } + + async openBackgroundAppRefreshSettings(): Promise { + this.throwNotSupported(); + } + async isChannelEnabled(): Promise<{ enabled: boolean; channelId: string }> { this.throwNotSupported(); }