feat(dev): add notification debug panel and native pending inspector

Add a dev-only Notification Debug Panel at /dev/notifications for testing
predictive refresh and WAKEUP_PING without a backend.

- Gate route and Advanced Settings entry on import.meta.env.DEV
- NotificationDebugService drives mock refresh, flood test, clear, and
  wake simulation via existing handleCapacitorPushNotificationReceived and
  applyNotificationRefreshPayload (shared with refreshNotifications)
- Add NotificationInspector Capacitor plugin: iOS lists pending
  UNNotificationRequest identifiers and next trigger; Android stub returns
  empty pending for safe registration
This commit is contained in:
Jose Olarte III
2026-05-07 18:52:59 +08:00
parent 320e55912b
commit fd0b8ce6d0
11 changed files with 596 additions and 51 deletions

View File

@@ -29,6 +29,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
attempts += 1
if registerSharedImagePlugin() {
print("[AppDelegate] ✅ Plugin registration successful on attempt \(attempts)")
_ = registerNotificationInspectorPlugin()
} else if attempts < maxAttempts {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(attempts) * 0.5) {
tryRegister()
@@ -64,6 +65,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
return true
}
@discardableResult
private func registerNotificationInspectorPlugin() -> Bool {
guard let window = self.window,
let bridgeVC = window.rootViewController as? CAPBridgeViewController,
let bridge = bridgeVC.bridge else {
return false
}
let pluginInstance = NotificationInspectorPlugin()
bridge.registerPluginInstance(pluginInstance)
print("[AppDelegate] ✅ Registered NotificationInspectorPlugin (exposed as 'NotificationInspector')")
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.

View File

@@ -0,0 +1,51 @@
import Foundation
import Capacitor
import UserNotifications
@objc(NotificationInspector)
public class NotificationInspectorPlugin: CAPPlugin, CAPBridgedPlugin {
public var identifier: String { "NotificationInspector" }
public var jsName: String { "NotificationInspector" }
public var pluginMethods: [CAPPluginMethod] {
[
CAPPluginMethod(#selector(getPendingNotifications(_:)), returnType: .promise)
]
}
@objc public func getPendingNotifications(_ call: CAPPluginCall) {
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
let pending: [[String: Any]] = requests.map { req in
var nextTriggerMs: NSNumber? = nil
var triggerType: String? = nil
if let trigger = req.trigger as? UNCalendarNotificationTrigger {
triggerType = "calendar"
if let next = trigger.nextTriggerDate() {
nextTriggerMs = NSNumber(value: Int64(next.timeIntervalSince1970 * 1000))
}
} else if let trigger = req.trigger as? UNTimeIntervalNotificationTrigger {
triggerType = "timeInterval"
if let next = trigger.nextTriggerDate() {
nextTriggerMs = NSNumber(value: Int64(next.timeIntervalSince1970 * 1000))
}
} else if req.trigger != nil {
triggerType = "other"
} else {
triggerType = nil
}
var obj: [String: Any] = [
"identifier": req.identifier
]
obj["nextTriggerDate"] = nextTriggerMs ?? NSNull()
obj["triggerType"] = triggerType ?? NSNull()
return obj
}
call.resolve([
"pending": pending
])
}
}
}