diff --git a/CHANGELOG.md b/CHANGELOG.md index f7e139f9f2c..fa240adb599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- iOS/Watch: add an Apple Watch companion MVP with watch inbox UI, watch notification relay handling, and gateway command surfaces for watch status/send flows. (#20054) Thanks @mbelinky. - Gateway/CLI: add paired-device hygiene flows with `device.pair.remove`, plus `openclaw devices remove` and guarded `openclaw devices clear --yes [--pending]` commands for removing paired entries and optionally rejecting pending requests. (#20057) Thanks @mbelinky. ### Fixes diff --git a/apps/ios/Sources/Services/WatchMessagingService.swift b/apps/ios/Sources/Services/WatchMessagingService.swift index 320fa19bac0..8332fb5882d 100644 --- a/apps/ios/Sources/Services/WatchMessagingService.swift +++ b/apps/ios/Sources/Services/WatchMessagingService.swift @@ -92,13 +92,15 @@ final class WatchMessagingService: NSObject, WatchMessagingServicing, @unchecked ] if snapshot.reachable { - session.sendMessage(payload, replyHandler: nil) { error in + do { + try await self.sendReachableMessage(payload, with: session) + return WatchNotificationSendResult( + deliveredImmediately: true, + queuedForDelivery: false, + transport: "sendMessage") + } catch { Self.logger.error("watch sendMessage failed: \(error.localizedDescription, privacy: .public)") } - return WatchNotificationSendResult( - deliveredImmediately: true, - queuedForDelivery: false, - transport: "sendMessage") } _ = session.transferUserInfo(payload) @@ -108,6 +110,16 @@ final class WatchMessagingService: NSObject, WatchMessagingServicing, @unchecked transport: "transferUserInfo") } + private func sendReachableMessage(_ payload: [String: Any], with session: WCSession) async throws { + try await withCheckedThrowingContinuation { continuation in + session.sendMessage(payload, replyHandler: { _ in + continuation.resume() + }, errorHandler: { error in + continuation.resume(throwing: error) + }) + } + } + private func ensureActivated() async { guard let session = self.session else { return } if session.activationState == .activated { return } diff --git a/apps/ios/WatchExtension/Sources/WatchConnectivityReceiver.swift b/apps/ios/WatchExtension/Sources/WatchConnectivityReceiver.swift index e3ed450ff0c..9a128049c3c 100644 --- a/apps/ios/WatchExtension/Sources/WatchConnectivityReceiver.swift +++ b/apps/ios/WatchExtension/Sources/WatchConnectivityReceiver.swift @@ -61,6 +61,21 @@ extension WatchConnectivityReceiver: WCSessionDelegate { } } + func session( + _: WCSession, + didReceiveMessage message: [String: Any], + replyHandler: @escaping ([String: Any]) -> Void) + { + guard let incoming = Self.parseNotificationPayload(message) else { + replyHandler(["ok": false]) + return + } + Task { @MainActor in + self.store.consume(message: incoming, transport: "sendMessage") + replyHandler(["ok": true]) + } + } + func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any]) { guard let incoming = Self.parseNotificationPayload(userInfo) else { return } Task { @MainActor in