feat(ios): add local notification diagnostics for debug panels
Expose bridge-safe pending, delivered, and settings snapshots from UNUserNotificationCenter so apps can inspect local notification state without native date objects or large payloads. - Add getDeliveredNotifications and getNotificationSettings on iOS - Harden getPendingNotifications with triggerTimestamp/triggerDateIso - Always resolve with serializable values; omit userInfo from delivered - Register new Capacitor methods and TypeScript definitions/web stubs
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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<void>;
|
||||
checkStatus(): Promise<NotificationStatus>;
|
||||
|
||||
// iOS local notification diagnostics (bridge-safe, no backend concepts)
|
||||
getPendingNotifications(): Promise<PendingNotificationsResult>;
|
||||
getDeliveredNotifications(): Promise<DeliveredNotificationsResult>;
|
||||
getNotificationSettings(): Promise<NotificationCenterSettingsSnapshot>;
|
||||
getNotificationPermissionStatus(): Promise<NotificationPermissionStatus>;
|
||||
requestNotificationPermission(): Promise<{ granted: boolean }>;
|
||||
getBackgroundTaskStatus(): Promise<BackgroundTaskStatus>;
|
||||
openNotificationSettings(): Promise<void>;
|
||||
openBackgroundAppRefreshSettings(): Promise<void>;
|
||||
|
||||
// New dual scheduling methods
|
||||
scheduleContentFetch(config: ContentFetchConfig): Promise<void>;
|
||||
|
||||
37
src/web.ts
37
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<PendingNotificationsResult> {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
async getDeliveredNotifications(): Promise<DeliveredNotificationsResult> {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
async getNotificationSettings(): Promise<NotificationCenterSettingsSnapshot> {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
async getNotificationPermissionStatus(): Promise<NotificationPermissionStatus> {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
async requestNotificationPermission(): Promise<{ granted: boolean }> {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
async getBackgroundTaskStatus(): Promise<BackgroundTaskStatus> {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
async openNotificationSettings(): Promise<void> {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
async openBackgroundAppRefreshSettings(): Promise<void> {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
async isChannelEnabled(): Promise<{ enabled: boolean; channelId: string }> {
|
||||
this.throwNotSupported();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user