diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ab6212beb3..4675e2729db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
- iOS/Talk: harden mobile talk config handling by ignoring redacted/env-placeholder API keys, support secure local keychain override, improve accessibility motion/contrast behavior in status UI, and tighten ATS to local-network allowance. (#18163) Thanks @mbelinky.
- iOS/Gateway: stabilize connect/discovery state handling, add onboarding reset recovery in Settings, and fix iOS gateway-controller coverage for command-surface and last-connection persistence behavior. (#18164) Thanks @mbelinky.
- iOS/Onboarding: add QR-first onboarding wizard with setup-code deep link support, pairing/auth issue guidance, and device-pair QR generation improvements for Telegram/Web/TUI fallback flows. (#18162) Thanks @mbelinky and @Marvae.
+- iOS/Location: restore the significant location monitor implementation (service hooks + protocol surface + ATS key alignment) after merge drift so iOS builds compile again. (#18260) Thanks @ngutman.
## 2026.2.15
diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist
index 562692bdb11..3182e43d30a 100644
--- a/apps/ios/Sources/Info.plist
+++ b/apps/ios/Sources/Info.plist
@@ -24,7 +24,7 @@
20260216
NSAppTransportSecurity
- NSAllowsLocalNetworking
+ NSAllowsArbitraryLoadsInWebContent
NSBonjourServices
diff --git a/apps/ios/Sources/Location/LocationService.swift b/apps/ios/Sources/Location/LocationService.swift
index 99265d02e89..f1f0f69ed7f 100644
--- a/apps/ios/Sources/Location/LocationService.swift
+++ b/apps/ios/Sources/Location/LocationService.swift
@@ -12,6 +12,10 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
private var authContinuation: CheckedContinuation?
private var locationContinuation: CheckedContinuation?
+ private var updatesContinuation: AsyncStream.Continuation?
+ private var isStreaming = false
+ private var significantLocationCallback: (@Sendable (CLLocation) -> Void)?
+ private var isMonitoringSignificantChanges = false
override init() {
super.init()
@@ -104,6 +108,56 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
}
}
+ func startLocationUpdates(
+ desiredAccuracy: OpenClawLocationAccuracy,
+ significantChangesOnly: Bool) -> AsyncStream
+ {
+ self.stopLocationUpdates()
+
+ self.manager.desiredAccuracy = Self.accuracyValue(desiredAccuracy)
+ self.manager.pausesLocationUpdatesAutomatically = true
+ self.manager.allowsBackgroundLocationUpdates = true
+
+ self.isStreaming = true
+ if significantChangesOnly {
+ self.manager.startMonitoringSignificantLocationChanges()
+ } else {
+ self.manager.startUpdatingLocation()
+ }
+
+ return AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in
+ self.updatesContinuation = continuation
+ continuation.onTermination = { @Sendable _ in
+ Task { @MainActor in
+ self.stopLocationUpdates()
+ }
+ }
+ }
+ }
+
+ func stopLocationUpdates() {
+ guard self.isStreaming else { return }
+ self.isStreaming = false
+ self.manager.stopUpdatingLocation()
+ self.manager.stopMonitoringSignificantLocationChanges()
+ self.updatesContinuation?.finish()
+ self.updatesContinuation = nil
+ }
+
+ func startMonitoringSignificantLocationChanges(onUpdate: @escaping @Sendable (CLLocation) -> Void) {
+ self.significantLocationCallback = onUpdate
+ guard !self.isMonitoringSignificantChanges else { return }
+ self.isMonitoringSignificantChanges = true
+ self.manager.startMonitoringSignificantLocationChanges()
+ }
+
+ func stopMonitoringSignificantLocationChanges() {
+ guard self.isMonitoringSignificantChanges else { return }
+ self.isMonitoringSignificantChanges = false
+ self.significantLocationCallback = nil
+ self.manager.stopMonitoringSignificantLocationChanges()
+ }
+
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus
Task { @MainActor in
@@ -117,12 +171,22 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let locs = locations
Task { @MainActor in
- guard let cont = self.locationContinuation else { return }
- self.locationContinuation = nil
- if let latest = locs.last {
- cont.resume(returning: latest)
- } else {
- cont.resume(throwing: Error.unavailable)
+ // Resolve the one-shot continuation first (if any).
+ if let cont = self.locationContinuation {
+ self.locationContinuation = nil
+ if let latest = locs.last {
+ cont.resume(returning: latest)
+ } else {
+ cont.resume(throwing: Error.unavailable)
+ }
+ // Don't return — also forward to significant-change callback below
+ // so both consumers receive updates when both are active.
+ }
+ if let callback = self.significantLocationCallback, let latest = locs.last {
+ callback(latest)
+ }
+ if let latest = locs.last, let updates = self.updatesContinuation {
+ updates.yield(latest)
}
}
}
diff --git a/apps/ios/Sources/Location/SignificantLocationMonitor.swift b/apps/ios/Sources/Location/SignificantLocationMonitor.swift
new file mode 100644
index 00000000000..f12a157dc69
--- /dev/null
+++ b/apps/ios/Sources/Location/SignificantLocationMonitor.swift
@@ -0,0 +1,38 @@
+import CoreLocation
+import Foundation
+import OpenClawKit
+
+/// Monitors significant location changes and pushes `location.update`
+/// events to the gateway so the severance hook can determine whether
+/// the user is at their configured work location.
+@MainActor
+enum SignificantLocationMonitor {
+ static func startIfNeeded(
+ locationService: any LocationServicing,
+ locationMode: OpenClawLocationMode,
+ gateway: GatewayNodeSession
+ ) {
+ guard locationMode == .always else { return }
+ let status = locationService.authorizationStatus()
+ guard status == .authorizedAlways else { return }
+ locationService.startMonitoringSignificantLocationChanges { location in
+ struct Payload: Codable {
+ var lat: Double
+ var lon: Double
+ var accuracyMeters: Double
+ var source: String?
+ }
+ let payload = Payload(
+ lat: location.coordinate.latitude,
+ lon: location.coordinate.longitude,
+ accuracyMeters: location.horizontalAccuracy,
+ source: "ios-significant-location")
+ guard let data = try? JSONEncoder().encode(payload),
+ let json = String(data: data, encoding: .utf8)
+ else { return }
+ Task { @MainActor in
+ await gateway.sendEvent(event: "location.update", payloadJSON: json)
+ }
+ }
+ }
+}
diff --git a/apps/ios/Sources/Services/NodeServiceProtocols.swift b/apps/ios/Sources/Services/NodeServiceProtocols.swift
index 002c87ad9ca..5ed6f8cfd88 100644
--- a/apps/ios/Sources/Services/NodeServiceProtocols.swift
+++ b/apps/ios/Sources/Services/NodeServiceProtocols.swift
@@ -28,6 +28,12 @@ protocol LocationServicing: Sendable {
desiredAccuracy: OpenClawLocationAccuracy,
maxAgeMs: Int?,
timeoutMs: Int?) async throws -> CLLocation
+ func startLocationUpdates(
+ desiredAccuracy: OpenClawLocationAccuracy,
+ significantChangesOnly: Bool) -> AsyncStream
+ func stopLocationUpdates()
+ func startMonitoringSignificantLocationChanges(onUpdate: @escaping @Sendable (CLLocation) -> Void)
+ func stopMonitoringSignificantLocationChanges()
}
protocol DeviceStatusServicing: Sendable {
diff --git a/apps/ios/Sources/Status/StatusActivityBuilder.swift b/apps/ios/Sources/Status/StatusActivityBuilder.swift
index a335e2f4643..381b3d2b9e8 100644
--- a/apps/ios/Sources/Status/StatusActivityBuilder.swift
+++ b/apps/ios/Sources/Status/StatusActivityBuilder.swift
@@ -1,6 +1,7 @@
import SwiftUI
enum StatusActivityBuilder {
+ @MainActor
static func build(
appModel: NodeAppModel,
voiceWakeEnabled: Bool,