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
|
* @param call Plugin call
|
||||||
*/
|
*/
|
||||||
@objc func getPendingNotifications(_ call: CAPPluginCall) {
|
@objc func getPendingNotifications(_ call: CAPPluginCall) {
|
||||||
Task {
|
diagnosticLog("getPendingNotifications called")
|
||||||
do {
|
notificationCenter.getPendingNotificationRequests { requests in
|
||||||
// Delegate to UNUserNotificationCenter for pending requests
|
var notifications: [[String: Any]] = []
|
||||||
let requests = try await notificationCenter.pendingNotificationRequests()
|
for request in requests {
|
||||||
|
let content = request.content
|
||||||
var notifications: [[String: Any]] = []
|
let (triggerTimestamp, triggerDateIso) = Self.pendingTriggerFields(from: request.trigger)
|
||||||
for request in requests {
|
var item: [String: Any] = [
|
||||||
let content = request.content
|
"identifier": request.identifier,
|
||||||
var triggerDate: Int64 = 0
|
"title": content.title,
|
||||||
|
"body": content.body,
|
||||||
if let calendarTrigger = request.trigger as? UNCalendarNotificationTrigger {
|
"triggerType": Self.pendingTriggerTypeString(from: request.trigger),
|
||||||
if let nextDate = calendarTrigger.nextTriggerDate() {
|
"repeats": request.trigger?.repeats ?? false
|
||||||
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
|
|
||||||
]
|
]
|
||||||
|
if let triggerTimestamp = triggerTimestamp {
|
||||||
DispatchQueue.main.async {
|
item["triggerTimestamp"] = triggerTimestamp
|
||||||
call.resolve(result)
|
item["triggerDate"] = triggerTimestamp
|
||||||
}
|
} else {
|
||||||
} catch {
|
item["triggerTimestamp"] = NSNull()
|
||||||
DispatchQueue.main.async {
|
item["triggerDate"] = NSNull()
|
||||||
call.reject("Failed to get pending notifications: \(error.localizedDescription)", "pending_notifications_failed")
|
|
||||||
}
|
}
|
||||||
|
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
|
// MARK: - CAPBridgedPlugin Conformance
|
||||||
// This extension makes the plugin conform to CAPBridgedPlugin protocol
|
// This extension makes the plugin conform to CAPBridgedPlugin protocol
|
||||||
// which is required for Capacitor to discover and register the plugin
|
// 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: "getNotificationPermissionStatus", returnType: CAPPluginReturnPromise))
|
||||||
methods.append(CAPPluginMethod(name: "requestNotificationPermission", returnType: CAPPluginReturnPromise))
|
methods.append(CAPPluginMethod(name: "requestNotificationPermission", returnType: CAPPluginReturnPromise))
|
||||||
methods.append(CAPPluginMethod(name: "getPendingNotifications", 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: "getBackgroundTaskStatus", returnType: CAPPluginReturnPromise))
|
||||||
methods.append(CAPPluginMethod(name: "openNotificationSettings", returnType: CAPPluginReturnPromise))
|
methods.append(CAPPluginMethod(name: "openNotificationSettings", returnType: CAPPluginReturnPromise))
|
||||||
methods.append(CAPPluginMethod(name: "openBackgroundAppRefreshSettings", returnType: CAPPluginReturnPromise))
|
methods.append(CAPPluginMethod(name: "openBackgroundAppRefreshSettings", returnType: CAPPluginReturnPromise))
|
||||||
|
|||||||
@@ -155,6 +155,84 @@ export interface PermissionStatusResult {
|
|||||||
allPermissionsGranted: boolean;
|
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
|
// Static Daily Reminder Interfaces
|
||||||
export interface DailyReminderOptions {
|
export interface DailyReminderOptions {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -538,6 +616,16 @@ export interface DailyNotificationPlugin {
|
|||||||
openChannelSettings(channelId?: string): Promise<void>;
|
openChannelSettings(channelId?: string): Promise<void>;
|
||||||
checkStatus(): Promise<NotificationStatus>;
|
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
|
// New dual scheduling methods
|
||||||
scheduleContentFetch(config: ContentFetchConfig): Promise<void>;
|
scheduleContentFetch(config: ContentFetchConfig): Promise<void>;
|
||||||
scheduleUserNotification(config: UserNotificationConfig): Promise<void>;
|
scheduleUserNotification(config: UserNotificationConfig): Promise<void>;
|
||||||
|
|||||||
37
src/web.ts
37
src/web.ts
@@ -22,6 +22,11 @@ import type {
|
|||||||
CreateCallbackInput,
|
CreateCallbackInput,
|
||||||
History,
|
History,
|
||||||
HistoryStats,
|
HistoryStats,
|
||||||
|
PendingNotificationsResult,
|
||||||
|
DeliveredNotificationsResult,
|
||||||
|
NotificationCenterSettingsSnapshot,
|
||||||
|
NotificationPermissionStatus,
|
||||||
|
BackgroundTaskStatus,
|
||||||
} from './definitions';
|
} from './definitions';
|
||||||
import { DailyNotificationError, ErrorCode } from './core/errors';
|
import { DailyNotificationError, ErrorCode } from './core/errors';
|
||||||
|
|
||||||
@@ -204,6 +209,38 @@ export class DailyNotificationWeb implements DailyNotificationPlugin {
|
|||||||
this.throwNotSupported();
|
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 }> {
|
async isChannelEnabled(): Promise<{ enabled: boolean; channelId: string }> {
|
||||||
this.throwNotSupported();
|
this.throwNotSupported();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user