mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 10:55:31 -06:00
57e5ab7a87
* fix(apps): surface native action failures Make iOS gateway reconnect attempts return visible closed outcomes, route macOS gateway deep links through primary setup confirmation, and expose pending/sent/error notification test results across native and Dashboard settings. * chore(apps): refresh native i18n inventory * test(macos): isolate gateway cancel state * fix(ui): accept permission-only notification status * test(gateway): drain accepted agent run before fixture reset * fix(ui): notify clients after service worker claim * test(gateway): wait for accepted root work to drain * test(ui): isolate widget theme observer window * test(ui): await durable attachment draft before teardown * test(agents): use race-safe MCP process cleanup * test(ui): await cloud startup runtime before error probe * test(ui): align startup and teardown probes with current owners
114 lines
4.3 KiB
Swift
114 lines
4.3 KiB
Swift
import Foundation
|
|
import UserNotifications
|
|
import WebKit
|
|
|
|
enum DashboardNotificationsRequest: String {
|
|
case status
|
|
case requestPermission = "request-permission"
|
|
case sendTest = "send-test"
|
|
}
|
|
|
|
struct DashboardNotificationsSnapshot: Encodable, Equatable {
|
|
let permission: String
|
|
let test: TestNotificationOutcome?
|
|
}
|
|
|
|
@MainActor
|
|
final class DashboardNotificationsMessageHandler: NSObject, WKScriptMessageHandler {
|
|
weak var owner: DashboardWindowController?
|
|
|
|
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
self.owner?.receiveNotificationsMessage(message)
|
|
}
|
|
}
|
|
|
|
extension DashboardWindowController {
|
|
static let notificationsMessageHandlerName = "openclawNotifications"
|
|
|
|
static func notificationsRequest(from body: Any) -> DashboardNotificationsRequest? {
|
|
guard let payload = body as? [String: Any],
|
|
let type = payload["type"] as? String
|
|
else {
|
|
return nil
|
|
}
|
|
return DashboardNotificationsRequest(rawValue: type)
|
|
}
|
|
|
|
static func notificationsPermissionLabel(for status: UNAuthorizationStatus) -> String {
|
|
switch status {
|
|
case .authorized, .provisional:
|
|
"granted"
|
|
case .denied:
|
|
"denied"
|
|
case .notDetermined:
|
|
"notDetermined"
|
|
default:
|
|
// .ephemeral is unavailable by name on macOS and cannot occur here;
|
|
// map it and future cases to notDetermined so the UI offers the
|
|
// permission request instead of claiming access.
|
|
"notDetermined"
|
|
}
|
|
}
|
|
|
|
func receiveNotificationsMessage(_ message: WKScriptMessage) {
|
|
guard message.name == Self.notificationsMessageHandlerName,
|
|
message.webView === self.webView,
|
|
message.frameInfo.isMainFrame,
|
|
Self.isTrustedLinkSource(message.frameInfo.request.url, dashboardURL: self.currentURL)
|
|
else {
|
|
return
|
|
}
|
|
guard let request = Self.notificationsRequest(from: message.body) else { return }
|
|
|
|
switch request {
|
|
case .status:
|
|
Task { await self.refreshNotificationsPermission() }
|
|
case .requestPermission:
|
|
Task {
|
|
_ = await PermissionManager.ensure([.notifications], interactive: true)
|
|
await self.refreshNotificationsPermission()
|
|
}
|
|
case .sendTest:
|
|
Task {
|
|
guard self.notificationTestOutcome != .pending else { return }
|
|
self.notificationTestOutcome = .pending
|
|
await self.publishNotificationsStatus()
|
|
self.notificationTestOutcome = await TestNotificationAction.send()
|
|
await self.publishNotificationsStatus()
|
|
}
|
|
}
|
|
}
|
|
|
|
static func notificationsSnapshot(
|
|
permission: String,
|
|
testOutcome: TestNotificationOutcome?) -> DashboardNotificationsSnapshot
|
|
{
|
|
DashboardNotificationsSnapshot(permission: permission, test: testOutcome)
|
|
}
|
|
|
|
private func refreshNotificationsPermission() async {
|
|
guard PermissionManager.notificationCenterAvailable else { return }
|
|
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
|
self.notificationPermission = Self.notificationsPermissionLabel(for: settings.authorizationStatus)
|
|
await self.publishNotificationsStatus()
|
|
}
|
|
|
|
private func publishNotificationsStatus() async {
|
|
// Honest absence beats a fabricated status when the process is unbundled.
|
|
guard PermissionManager.notificationCenterAvailable else { return }
|
|
let snapshot = Self.notificationsSnapshot(
|
|
permission: self.notificationPermission,
|
|
testOutcome: self.notificationTestOutcome)
|
|
guard let data = try? JSONEncoder().encode(snapshot),
|
|
let json = String(data: data, encoding: .utf8)
|
|
else { return }
|
|
// Keep a global snapshot so late subscribers can read status without a bridge round-trip.
|
|
_ = try? await self.webView.evaluateJavaScript(
|
|
"""
|
|
window.__OPENCLAW_NATIVE_NOTIFICATIONS__ = \(json);
|
|
window.dispatchEvent(new CustomEvent('openclaw:native-notifications-status', \
|
|
{detail:window.__OPENCLAW_NATIVE_NOTIFICATIONS__}));
|
|
""")
|
|
}
|
|
}
|