From 80c088b8be3491ffe9d77d375203261ebcde21a1 Mon Sep 17 00:00:00 2001 From: Bini Barazani Date: Tue, 21 Jul 2026 15:05:15 +0300 Subject: [PATCH] fix(ios): honor maximumAge in getCurrentPosition instead of returning stale cached positions --- .../GeolocationCallbackManager.swift | 7 ++++ .../GeolocationConstants.swift | 9 +++++ .../GeolocationPlugin/GeolocationPlugin.swift | 36 ++++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/ios/Sources/GeolocationPlugin/GeolocationCallbackManager.swift b/ios/Sources/GeolocationPlugin/GeolocationCallbackManager.swift index bad47b0..26858e9 100644 --- a/ios/Sources/GeolocationPlugin/GeolocationCallbackManager.swift +++ b/ios/Sources/GeolocationPlugin/GeolocationCallbackManager.swift @@ -25,6 +25,7 @@ final class GeolocationCallbackManager { private(set) var locationCallbacks: [CAPPluginCall] private(set) var watchCallbacks: [String: CAPPluginCall] private(set) var timeout: Int? + private(set) var maximumAge: Double? private let capacitorBridge: CAPBridgeProtocol? private var allCallbackGroups: [GeolocationCallbackGroup] { @@ -55,6 +56,7 @@ final class GeolocationCallbackManager { locationCallbacks.append(call) let timeout = call.getInt(Constants.Arguments.timeout) self.timeout = timeout + self.maximumAge = call.getDouble(Constants.Arguments.maximumAge) ?? 0 } func addWatchCallback(_ watchId: String, capacitorCall call: CAPPluginCall) { @@ -107,6 +109,11 @@ final class GeolocationCallbackManager { createPluginResult(status: .success(position.toJSObject())) } + func sendWatchSuccess(with position: IONGLOCPositionModel) { + guard let watchGroup = allCallbackGroups.first(where: { $0.type == .watch }) else { return } + send(.success(position.toJSObject()), to: watchGroup) + } + func sendError(_ call: CAPPluginCall, error: GeolocationError) { let errorModel = error.toCodeMessagePair() call.reject(errorModel.1, errorModel.0) diff --git a/ios/Sources/GeolocationPlugin/GeolocationConstants.swift b/ios/Sources/GeolocationPlugin/GeolocationConstants.swift index 6664760..c35efca 100644 --- a/ios/Sources/GeolocationPlugin/GeolocationConstants.swift +++ b/ios/Sources/GeolocationPlugin/GeolocationConstants.swift @@ -2,9 +2,18 @@ enum Constants { enum Arguments { static let enableHighAccuracy = "enableHighAccuracy" static let id = "id" + static let maximumAge = "maximumAge" static let timeout = "timeout" } + enum SingleLocationRequest { + /// A position younger than this is treated as a live fix rather than a cached one, + /// regardless of `maximumAge`. CLLocation timestamps lag delivery by the acquisition + /// time, so `maximumAge: 0` (the default, meaning "no cached positions") must still + /// accept freshly acquired fixes. + static let freshnessThresholdInMilliseconds: Double = 5000 + } + enum AuthorisationStatus { enum ResultKey { static let location = "location" diff --git a/ios/Sources/GeolocationPlugin/GeolocationPlugin.swift b/ios/Sources/GeolocationPlugin/GeolocationPlugin.swift index c9252d5..bd61054 100644 --- a/ios/Sources/GeolocationPlugin/GeolocationPlugin.swift +++ b/ios/Sources/GeolocationPlugin/GeolocationPlugin.swift @@ -162,7 +162,7 @@ private extension GeolocationPlugin { } } .sink(receiveValue: { [weak self] position in - self?.callbackManager?.sendSuccess(with: position) + self?.handleReceivedPosition(position) }) timeoutCancellable = locationService?.locationTimeoutPublisher @@ -175,6 +175,40 @@ private extension GeolocationPlugin { }) } + /// Routes a position received from the location service to the pending callbacks, + /// enforcing `maximumAge` for `getCurrentPosition` calls: a cached position older + /// than `maximumAge` (e.g. the system's last known location from before the device + /// moved) must not satisfy them, so they are kept pending while a fresh position + /// is acquired. Watch callbacks always receive the update. + func handleReceivedPosition(_ position: IONGLOCPositionModel) { + guard let callbackManager else { return } + let hasSingleLocationRequests = !callbackManager.locationCallbacks.isEmpty + + if hasSingleLocationRequests, isStaleForSingleLocationRequests(position) { + callbackManager.sendWatchSuccess(with: position) + if callbackManager.watchCallbacks.isEmpty { + // No watch is keeping monitoring alive, so request continuous updates to + // obtain a fresh position; also re-arms the timeout timer, which was + // cancelled when the stale position was delivered. + locationService?.startMonitoringLocation(options: IONGLOCRequestOptionsModel(timeout: callbackManager.timeout)) + } + return + } + + callbackManager.sendSuccess(with: position) + + if hasSingleLocationRequests, callbackManager.watchCallbacks.isEmpty { + // Stop any monitoring started above solely to satisfy the single requests. + locationService?.stopMonitoringLocation() + } + } + + func isStaleForSingleLocationRequests(_ position: IONGLOCPositionModel) -> Bool { + let ageInMilliseconds = Date().timeIntervalSince1970 * 1000 - position.timestamp + let maximumAge = callbackManager?.maximumAge ?? 0 + return ageInMilliseconds > max(maximumAge, Constants.SingleLocationRequest.freshnessThresholdInMilliseconds) + } + func requestLocationAuthorisation(type requestType: IONGLOCAuthorisationRequestType) { DispatchQueue.global(qos: .background).async { guard self.checkIfLocationServicesAreEnabled() else { return }