react-native-nitro-geolocation 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/margelo/nitro/nitrogeolocation/GetCurrentPosition.kt +4 -4
- package/android/src/main/java/com/margelo/nitro/nitrogeolocation/NitroGeolocation.kt +642 -50
- package/android/src/main/java/com/margelo/nitro/nitrogeolocation/NitroGeolocationCompat.kt +81 -0
- package/android/src/main/java/com/margelo/nitro/nitrogeolocation/WatchPosition.kt +4 -4
- package/ios/LocationManager.swift +4 -4
- package/ios/NitroGeolocation.swift +543 -68
- package/ios/NitroGeolocationCompat.swift +109 -0
- package/nitrogen/generated/android/c++/JAuthorizationLevel.hpp +61 -0
- package/nitrogen/generated/android/c++/JAuthorizationLevelInternal.hpp +3 -4
- package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -1
- package/nitrogen/generated/android/c++/JFunc_void_GeolocationError.hpp +2 -1
- package/nitrogen/generated/android/c++/JFunc_void_GeolocationResponse.hpp +6 -1
- package/nitrogen/generated/android/c++/JFunc_void_LocationError.hpp +78 -0
- package/nitrogen/generated/android/c++/JGeolocationCoordinates.hpp +25 -17
- package/nitrogen/generated/android/c++/JGeolocationError.hpp +5 -1
- package/nitrogen/generated/android/c++/JGeolocationOptions.hpp +5 -1
- package/nitrogen/generated/android/c++/JGeolocationResponse.hpp +9 -1
- package/nitrogen/generated/android/c++/JHybridNitroGeolocationCompatSpec.cpp +109 -0
- package/nitrogen/generated/android/c++/JHybridNitroGeolocationCompatSpec.hpp +70 -0
- package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.cpp +98 -42
- package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.hpp +7 -5
- package/nitrogen/generated/android/c++/JLocationError.hpp +61 -0
- package/nitrogen/generated/android/c++/JLocationProvider.hpp +61 -0
- package/nitrogen/generated/android/c++/JLocationProviderInternal.hpp +3 -4
- package/nitrogen/generated/android/c++/JLocationRequestOptions.hpp +81 -0
- package/nitrogen/generated/android/c++/JModernGeolocationConfiguration.hpp +73 -0
- package/nitrogen/generated/android/c++/JNullableDouble.cpp +26 -0
- package/nitrogen/generated/android/c++/JNullableDouble.hpp +69 -0
- package/nitrogen/generated/android/c++/JPermissionStatus.hpp +64 -0
- package/nitrogen/generated/android/c++/JRNConfigurationInternal.hpp +5 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AuthorizationLevel.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AuthorizationLevelInternal.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void.kt +0 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_GeolocationError.kt +0 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_GeolocationResponse.kt +0 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_LocationError.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationCoordinates.kt +34 -25
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationError.kt +27 -18
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationOptions.kt +33 -24
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationResponse.kt +18 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HybridNitroGeolocationCompatSpec.kt +92 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HybridNitroGeolocationSpec.kt +17 -17
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationError.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationProvider.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationProviderInternal.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationRequestOptions.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/ModernGeolocationConfiguration.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/NullableDouble.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/PermissionStatus.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/RNConfigurationInternal.kt +24 -15
- package/nitrogen/generated/android/nitrogeolocation+autolinking.cmake +3 -0
- package/nitrogen/generated/android/nitrogeolocationOnLoad.cpp +14 -2
- package/nitrogen/generated/ios/NitroGeolocation+autolinking.rb +1 -1
- package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.cpp +55 -13
- package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.hpp +325 -68
- package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Umbrella.hpp +26 -0
- package/nitrogen/generated/ios/NitroGeolocationAutolinking.mm +8 -0
- package/nitrogen/generated/ios/NitroGeolocationAutolinking.swift +15 -0
- package/nitrogen/generated/ios/c++/HybridNitroGeolocationCompatSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroGeolocationCompatSpecSwift.hpp +130 -0
- package/nitrogen/generated/ios/c++/HybridNitroGeolocationSpecSwift.hpp +47 -26
- package/nitrogen/generated/ios/swift/AuthorizationLevel.swift +44 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +1 -1
- package/nitrogen/generated/ios/swift/Func_void_GeolocationError.swift +1 -1
- package/nitrogen/generated/ios/swift/Func_void_GeolocationResponse.swift +1 -1
- package/nitrogen/generated/ios/swift/Func_void_LocationError.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_PermissionStatus.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/GeolocationCoordinates.swift +132 -93
- package/nitrogen/generated/ios/swift/GeolocationError.swift +11 -40
- package/nitrogen/generated/ios/swift/GeolocationOptions.swift +29 -98
- package/nitrogen/generated/ios/swift/GeolocationResponse.swift +5 -16
- package/nitrogen/generated/ios/swift/HybridNitroGeolocationCompatSpec.swift +61 -0
- package/nitrogen/generated/ios/swift/HybridNitroGeolocationCompatSpec_cxx.swift +244 -0
- package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec.swift +13 -5
- package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec_cxx.swift +66 -64
- package/nitrogen/generated/ios/swift/LocationError.swift +35 -0
- package/nitrogen/generated/ios/swift/LocationProvider.swift +44 -0
- package/nitrogen/generated/ios/swift/LocationRequestOptions.swift +116 -0
- package/nitrogen/generated/ios/swift/ModernGeolocationConfiguration.swift +83 -0
- package/nitrogen/generated/ios/swift/NullableDouble.swift +18 -0
- package/nitrogen/generated/ios/swift/PermissionStatus.swift +48 -0
- package/nitrogen/generated/ios/swift/RNConfigurationInternal.swift +16 -50
- package/nitrogen/generated/shared/c++/AuthorizationLevel.hpp +80 -0
- package/nitrogen/generated/shared/c++/GeolocationCoordinates.hpp +45 -27
- package/nitrogen/generated/shared/c++/GeolocationError.hpp +32 -16
- package/nitrogen/generated/shared/c++/GeolocationOptions.hpp +38 -22
- package/nitrogen/generated/shared/c++/GeolocationResponse.hpp +23 -7
- package/nitrogen/generated/shared/c++/HybridNitroGeolocationCompatSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridNitroGeolocationCompatSpec.hpp +79 -0
- package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.cpp +4 -3
- package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.hpp +22 -16
- package/nitrogen/generated/shared/c++/LocationError.hpp +87 -0
- package/nitrogen/generated/shared/c++/LocationProvider.hpp +80 -0
- package/nitrogen/generated/shared/c++/LocationRequestOptions.hpp +107 -0
- package/nitrogen/generated/shared/c++/ModernGeolocationConfiguration.hpp +100 -0
- package/nitrogen/generated/shared/c++/PermissionStatus.hpp +84 -0
- package/nitrogen/generated/shared/c++/RNConfigurationInternal.hpp +29 -13
- package/package.json +16 -7
- package/src/GeolocationClient.ts +116 -0
- package/src/NitroGeolocation.nitro.ts +177 -30
- package/src/NitroGeolocationCompat.nitro.ts +41 -0
- package/src/NitroGeolocationModule.ts +6 -0
- package/src/compat/clearWatch.ts +9 -0
- package/src/{getCurrentPosition.ts → compat/getCurrentPosition.ts} +7 -3
- package/src/compat/index.tsx +34 -0
- package/src/compat/requestAuthorization.ts +9 -0
- package/src/{setRNConfiguration.ts → compat/setRNConfiguration.ts} +6 -4
- package/src/{stopObserving.ts → compat/stopObserving.ts} +4 -2
- package/src/{watchPosition.ts → compat/watchPosition.ts} +7 -6
- package/src/components/GeolocationProvider.tsx +91 -0
- package/src/components/index.ts +13 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useCheckPermission.ts +46 -0
- package/src/hooks/useGetCurrentPosition.ts +44 -0
- package/src/hooks/useRequestPermission.ts +38 -0
- package/src/hooks/useWatchPosition.ts +127 -0
- package/src/index.tsx +72 -27
- package/src/types.ts +32 -4
- package/src/utils/cache.ts +25 -0
- package/src/utils/config.ts +93 -0
- package/src/utils/errors.ts +82 -0
- package/src/utils/index.ts +27 -0
- package/src/utils/provider.ts +61 -0
- package/src/utils/quality.ts +98 -0
- package/src/clearWatch.ts +0 -13
- package/src/requestAuthorization.ts +0 -9
|
@@ -1,109 +1,584 @@
|
|
|
1
|
+
import Foundation
|
|
1
2
|
import CoreLocation
|
|
3
|
+
import NitroModules
|
|
2
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Swift Error wrapper for LocationError struct.
|
|
7
|
+
*/
|
|
8
|
+
private struct GeolocationErrorWrapper: Error, LocalizedError {
|
|
9
|
+
let locationError: LocationError
|
|
10
|
+
|
|
11
|
+
var errorDescription: String? {
|
|
12
|
+
return locationError.message
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var localizedDescription: String {
|
|
16
|
+
return locationError.message
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
init(code: Int, message: String) {
|
|
20
|
+
self.locationError = LocationError(
|
|
21
|
+
code: Double(code),
|
|
22
|
+
message: message
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* LocationManager Delegate class to handle CLLocationManager callbacks.
|
|
29
|
+
*/
|
|
30
|
+
private class LocationManagerDelegate: NSObject, CLLocationManagerDelegate {
|
|
31
|
+
weak var geolocation: NitroGeolocation?
|
|
32
|
+
|
|
33
|
+
init(geolocation: NitroGeolocation) {
|
|
34
|
+
self.geolocation = geolocation
|
|
35
|
+
super.init()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
39
|
+
geolocation?.handleAuthorizationChange(manager)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
43
|
+
geolocation?.handleLocationUpdate(locations)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
47
|
+
geolocation?.handleLocationError(error)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Modern Geolocation implementation with Promise-based API.
|
|
53
|
+
*
|
|
54
|
+
* Key features:
|
|
55
|
+
* - Promise-based permission and getCurrentPosition
|
|
56
|
+
* - Token-based watch subscriptions (functions are first-class!)
|
|
57
|
+
* - WatchPositionResult discriminated union
|
|
58
|
+
* - Automatic subscription management
|
|
59
|
+
*/
|
|
3
60
|
class NitroGeolocation: HybridNitroGeolocationSpec {
|
|
61
|
+
// MARK: - Types
|
|
62
|
+
|
|
63
|
+
private struct ParsedOptions {
|
|
64
|
+
let timeout: Double
|
|
65
|
+
let maximumAge: Double
|
|
66
|
+
let accuracy: CLLocationAccuracy
|
|
67
|
+
let distanceFilter: CLLocationDistance
|
|
68
|
+
let useSignificantChanges: Bool
|
|
69
|
+
|
|
70
|
+
static let DEFAULT_TIMEOUT: Double = 10 * 60 * 1000 // 10 minutes in ms
|
|
71
|
+
static let DEFAULT_MAXIMUM_AGE: Double = 0
|
|
72
|
+
|
|
73
|
+
static func parse(from options: LocationRequestOptions?) -> ParsedOptions {
|
|
74
|
+
let timeout = options?.timeout ?? DEFAULT_TIMEOUT
|
|
75
|
+
let maximumAge = options?.maximumAge ?? DEFAULT_MAXIMUM_AGE
|
|
76
|
+
let enableHighAccuracy = options?.enableHighAccuracy ?? false
|
|
77
|
+
let accuracy = enableHighAccuracy
|
|
78
|
+
? kCLLocationAccuracyBest
|
|
79
|
+
: kCLLocationAccuracyHundredMeters
|
|
80
|
+
let distanceFilter = options?.distanceFilter ?? kCLDistanceFilterNone
|
|
81
|
+
let useSignificantChanges = options?.useSignificantChanges ?? false
|
|
82
|
+
|
|
83
|
+
return ParsedOptions(
|
|
84
|
+
timeout: timeout,
|
|
85
|
+
maximumAge: maximumAge,
|
|
86
|
+
accuracy: accuracy,
|
|
87
|
+
distanceFilter: distanceFilter,
|
|
88
|
+
useSignificantChanges: useSignificantChanges
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Watch subscription storage (first-class functions!)
|
|
94
|
+
private struct WatchSubscription {
|
|
95
|
+
let token: String
|
|
96
|
+
let success: (GeolocationResponse) -> Void
|
|
97
|
+
let error: ((LocationError) -> Void)?
|
|
98
|
+
let options: ParsedOptions
|
|
99
|
+
}
|
|
100
|
+
|
|
4
101
|
// MARK: - Properties
|
|
5
102
|
|
|
6
|
-
private var configuration:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
103
|
+
private var configuration: ModernGeolocationConfiguration?
|
|
104
|
+
private var locationManager: CLLocationManager?
|
|
105
|
+
private var locationManagerDelegate: LocationManagerDelegate?
|
|
106
|
+
private var lastLocation: CLLocation?
|
|
107
|
+
private var usingSignificantChanges: Bool = false
|
|
108
|
+
|
|
109
|
+
// Permission promise resolvers
|
|
110
|
+
private var pendingPermissionResolvers: [(Result<PermissionStatus, Error>) -> Void] = []
|
|
111
|
+
|
|
112
|
+
// getCurrentPosition promise resolvers with timeout
|
|
113
|
+
private var pendingPositionRequests: [UUID: PositionRequest] = [:]
|
|
114
|
+
|
|
115
|
+
private struct PositionRequest {
|
|
116
|
+
let id: UUID
|
|
117
|
+
let resolver: (Result<GeolocationResponse, Error>) -> Void
|
|
118
|
+
let options: ParsedOptions
|
|
119
|
+
var timer: DispatchSourceTimer?
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Watch subscriptions (token -> callback)
|
|
123
|
+
private var watchSubscriptions: [String: WatchSubscription] = [:]
|
|
124
|
+
|
|
125
|
+
// Error codes
|
|
126
|
+
private let PERMISSION_DENIED = 1
|
|
127
|
+
private let POSITION_UNAVAILABLE = 2
|
|
128
|
+
private let TIMEOUT = 3
|
|
129
|
+
|
|
130
|
+
// MARK: - Configuration
|
|
131
|
+
|
|
132
|
+
func setConfiguration(config: ModernGeolocationConfiguration) {
|
|
133
|
+
self.configuration = config
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// MARK: - Permission API (Promise-based)
|
|
12
137
|
|
|
13
|
-
|
|
138
|
+
func checkPermission() throws -> Promise<PermissionStatus> {
|
|
139
|
+
return Promise.async {
|
|
140
|
+
let status = CLLocationManager.authorizationStatus()
|
|
141
|
+
return self.mapCLAuthorizationStatus(status)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func requestPermission() throws -> Promise<PermissionStatus> {
|
|
146
|
+
let promise = Promise<PermissionStatus>()
|
|
147
|
+
|
|
148
|
+
self.initializeLocationManagerIfNeeded()
|
|
149
|
+
|
|
150
|
+
let currentStatus = CLLocationManager.authorizationStatus()
|
|
151
|
+
|
|
152
|
+
// Already determined
|
|
153
|
+
if currentStatus != .notDetermined {
|
|
154
|
+
let status = self.mapCLAuthorizationStatus(currentStatus)
|
|
155
|
+
promise.resolve(withResult: status)
|
|
156
|
+
return promise
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Queue resolver
|
|
160
|
+
self.pendingPermissionResolvers.append { result in
|
|
161
|
+
switch result {
|
|
162
|
+
case .success(let status):
|
|
163
|
+
promise.resolve(withResult: status)
|
|
164
|
+
case .failure(let error):
|
|
165
|
+
promise.reject(withError: error)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Request permission
|
|
170
|
+
let authLevel = self.determineAuthorizationLevel()
|
|
171
|
+
self.requestSystemPermission(for: authLevel)
|
|
172
|
+
|
|
173
|
+
return promise
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// MARK: - Get Current Position (Promise-based)
|
|
177
|
+
|
|
178
|
+
func getCurrentPosition(options: LocationRequestOptions?) throws -> Promise<GeolocationResponse> {
|
|
179
|
+
let promise = Promise<GeolocationResponse>()
|
|
180
|
+
|
|
181
|
+
// Check permission
|
|
182
|
+
let status = CLLocationManager.authorizationStatus()
|
|
183
|
+
if status == .denied || status == .restricted {
|
|
184
|
+
let message = status == .restricted
|
|
185
|
+
? "This application is not authorized to use location services"
|
|
186
|
+
: "User denied access to location services."
|
|
187
|
+
let error = GeolocationErrorWrapper(
|
|
188
|
+
code: self.PERMISSION_DENIED,
|
|
189
|
+
message: message
|
|
190
|
+
)
|
|
191
|
+
promise.reject(withError: error)
|
|
192
|
+
return promise
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if !CLLocationManager.locationServicesEnabled() {
|
|
196
|
+
let error = GeolocationErrorWrapper(
|
|
197
|
+
code: self.POSITION_UNAVAILABLE,
|
|
198
|
+
message: "Location services disabled."
|
|
199
|
+
)
|
|
200
|
+
promise.reject(withError: error)
|
|
201
|
+
return promise
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
self.initializeLocationManagerIfNeeded()
|
|
14
205
|
|
|
15
|
-
|
|
206
|
+
let parsedOptions = ParsedOptions.parse(from: options)
|
|
16
207
|
|
|
17
|
-
|
|
18
|
-
|
|
208
|
+
// Check cached location
|
|
209
|
+
if let cached = self.lastLocation,
|
|
210
|
+
self.isCachedLocationValid(cached, options: parsedOptions) {
|
|
211
|
+
let position = self.locationToPosition(cached)
|
|
212
|
+
promise.resolve(withResult: position)
|
|
213
|
+
return promise
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Create position request
|
|
217
|
+
let id = UUID()
|
|
218
|
+
var request = PositionRequest(
|
|
219
|
+
id: id,
|
|
220
|
+
resolver: { result in
|
|
221
|
+
switch result {
|
|
222
|
+
case .success(let response):
|
|
223
|
+
promise.resolve(withResult: response)
|
|
224
|
+
case .failure(let error):
|
|
225
|
+
promise.reject(withError: error)
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
options: parsedOptions,
|
|
229
|
+
timer: nil
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
// Setup timeout
|
|
233
|
+
let timer = DispatchSource.makeTimerSource(queue: .main)
|
|
234
|
+
timer.schedule(deadline: .now() + parsedOptions.timeout / 1000.0)
|
|
235
|
+
timer.setEventHandler { [weak self] in
|
|
236
|
+
self?.handlePositionTimeout(requestId: id)
|
|
237
|
+
}
|
|
238
|
+
timer.resume()
|
|
239
|
+
request.timer = timer
|
|
240
|
+
|
|
241
|
+
self.pendingPositionRequests[id] = request
|
|
242
|
+
|
|
243
|
+
// Update configuration and start monitoring
|
|
244
|
+
self.updateLocationManagerConfiguration()
|
|
245
|
+
self.startMonitoring()
|
|
246
|
+
|
|
247
|
+
return promise
|
|
19
248
|
}
|
|
20
249
|
|
|
21
|
-
|
|
22
|
-
throws
|
|
23
|
-
{
|
|
24
|
-
let authType = determineAuthorizationType()
|
|
25
|
-
let skipPermissionRequests = configuration.skipPermissionRequests
|
|
26
|
-
let enableBackgroundLocationUpdates = configuration.enableBackgroundLocationUpdates ?? false
|
|
250
|
+
// MARK: - Watch Position (Callback-based with tokens)
|
|
27
251
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
252
|
+
func watchPosition(
|
|
253
|
+
success: @escaping (GeolocationResponse) -> Void,
|
|
254
|
+
error: ((LocationError) -> Void)?,
|
|
255
|
+
options: LocationRequestOptions?
|
|
256
|
+
) -> String {
|
|
257
|
+
let token = UUID().uuidString
|
|
258
|
+
let parsedOptions = ParsedOptions.parse(from: options)
|
|
259
|
+
|
|
260
|
+
let subscription = WatchSubscription(
|
|
261
|
+
token: token,
|
|
32
262
|
success: success,
|
|
33
|
-
error: error
|
|
263
|
+
error: error,
|
|
264
|
+
options: parsedOptions
|
|
34
265
|
)
|
|
266
|
+
|
|
267
|
+
watchSubscriptions[token] = subscription
|
|
268
|
+
|
|
269
|
+
initializeLocationManagerIfNeeded()
|
|
270
|
+
updateLocationManagerConfiguration()
|
|
271
|
+
startMonitoring()
|
|
272
|
+
|
|
273
|
+
return token
|
|
35
274
|
}
|
|
36
275
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
options: GeolocationOptions?
|
|
40
|
-
) throws {
|
|
41
|
-
// Fast path: check cached location immediately (no dispatch overhead!)
|
|
42
|
-
let parsedOptions = LocationManager.ParsedOptions.parse(from: options)
|
|
276
|
+
func unwatch(token: String) {
|
|
277
|
+
watchSubscriptions.removeValue(forKey: token)
|
|
43
278
|
|
|
44
|
-
if
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
279
|
+
// Stop monitoring if no more subscriptions or pending requests
|
|
280
|
+
if watchSubscriptions.isEmpty && pendingPositionRequests.isEmpty {
|
|
281
|
+
stopMonitoring()
|
|
48
282
|
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func stopObserving() {
|
|
286
|
+
watchSubscriptions.removeAll()
|
|
49
287
|
|
|
50
|
-
//
|
|
51
|
-
|
|
288
|
+
// Stop monitoring if no pending requests
|
|
289
|
+
if pendingPositionRequests.isEmpty {
|
|
290
|
+
stopMonitoring()
|
|
291
|
+
}
|
|
52
292
|
}
|
|
53
293
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
294
|
+
// MARK: - Location Manager Callbacks
|
|
295
|
+
|
|
296
|
+
fileprivate func handleAuthorizationChange(_ manager: CLLocationManager) {
|
|
297
|
+
let status = getCurrentAuthorizationStatus(from: manager)
|
|
298
|
+
let mappedStatus = mapCLAuthorizationStatus(status)
|
|
299
|
+
|
|
300
|
+
// Resolve pending permission requests
|
|
301
|
+
for resolver in pendingPermissionResolvers {
|
|
302
|
+
resolver(.success(mappedStatus))
|
|
303
|
+
}
|
|
304
|
+
pendingPermissionResolvers.removeAll()
|
|
305
|
+
|
|
306
|
+
// If authorized, start monitoring
|
|
307
|
+
if status == .authorizedAlways || status == .authorizedWhenInUse {
|
|
308
|
+
if !pendingPositionRequests.isEmpty || !watchSubscriptions.isEmpty {
|
|
309
|
+
startMonitoring()
|
|
310
|
+
}
|
|
311
|
+
}
|
|
59
312
|
}
|
|
60
313
|
|
|
61
|
-
|
|
62
|
-
|
|
314
|
+
fileprivate func handleLocationUpdate(_ locations: [CLLocation]) {
|
|
315
|
+
guard let location = locations.last else { return }
|
|
316
|
+
|
|
317
|
+
lastLocation = location
|
|
318
|
+
let position = locationToPosition(location)
|
|
319
|
+
|
|
320
|
+
// 1. Resolve all pending getCurrentPosition requests
|
|
321
|
+
for (id, request) in pendingPositionRequests {
|
|
322
|
+
request.timer?.cancel()
|
|
323
|
+
request.resolver(.success(position))
|
|
324
|
+
}
|
|
325
|
+
pendingPositionRequests.removeAll()
|
|
326
|
+
|
|
327
|
+
// 2. Notify all watch subscriptions (success)
|
|
328
|
+
for (_, subscription) in watchSubscriptions {
|
|
329
|
+
subscription.success(position)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 3. Stop monitoring if no more subscriptions or pending requests
|
|
333
|
+
if watchSubscriptions.isEmpty && pendingPositionRequests.isEmpty {
|
|
334
|
+
stopMonitoring()
|
|
335
|
+
}
|
|
63
336
|
}
|
|
64
337
|
|
|
65
|
-
|
|
66
|
-
|
|
338
|
+
fileprivate func handleLocationError(_ error: Error) {
|
|
339
|
+
let locationError: LocationError
|
|
340
|
+
let errorWrapper: GeolocationErrorWrapper
|
|
341
|
+
|
|
342
|
+
if let clError = error as? CLError {
|
|
343
|
+
switch clError.code {
|
|
344
|
+
case .denied:
|
|
345
|
+
locationError = createLocationError(
|
|
346
|
+
code: PERMISSION_DENIED,
|
|
347
|
+
message: "User denied access to location services."
|
|
348
|
+
)
|
|
349
|
+
errorWrapper = GeolocationErrorWrapper(
|
|
350
|
+
code: PERMISSION_DENIED,
|
|
351
|
+
message: "User denied access to location services."
|
|
352
|
+
)
|
|
353
|
+
case .locationUnknown:
|
|
354
|
+
// Temporarily unavailable, keep trying
|
|
355
|
+
return
|
|
356
|
+
default:
|
|
357
|
+
locationError = createLocationError(
|
|
358
|
+
code: POSITION_UNAVAILABLE,
|
|
359
|
+
message: "Unable to retrieve location: \(error.localizedDescription)"
|
|
360
|
+
)
|
|
361
|
+
errorWrapper = GeolocationErrorWrapper(
|
|
362
|
+
code: POSITION_UNAVAILABLE,
|
|
363
|
+
message: "Unable to retrieve location: \(error.localizedDescription)"
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
locationError = createLocationError(
|
|
368
|
+
code: POSITION_UNAVAILABLE,
|
|
369
|
+
message: "Unable to retrieve location: \(error.localizedDescription)"
|
|
370
|
+
)
|
|
371
|
+
errorWrapper = GeolocationErrorWrapper(
|
|
372
|
+
code: POSITION_UNAVAILABLE,
|
|
373
|
+
message: "Unable to retrieve location: \(error.localizedDescription)"
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 1. Reject all pending getCurrentPosition requests
|
|
378
|
+
for (_, request) in pendingPositionRequests {
|
|
379
|
+
request.timer?.cancel()
|
|
380
|
+
request.resolver(.failure(errorWrapper))
|
|
381
|
+
}
|
|
382
|
+
pendingPositionRequests.removeAll()
|
|
383
|
+
|
|
384
|
+
// 2. Notify all watch subscriptions (error)
|
|
385
|
+
for (_, subscription) in watchSubscriptions {
|
|
386
|
+
subscription.error?(locationError)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
stopMonitoring()
|
|
67
390
|
}
|
|
68
391
|
|
|
69
|
-
// MARK: -
|
|
392
|
+
// MARK: - Helper Functions
|
|
393
|
+
|
|
394
|
+
private func initializeLocationManagerIfNeeded() {
|
|
395
|
+
guard locationManager == nil else { return }
|
|
70
396
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
397
|
+
if Thread.isMainThread {
|
|
398
|
+
locationManager = CLLocationManager()
|
|
399
|
+
locationManagerDelegate = LocationManagerDelegate(geolocation: self)
|
|
400
|
+
locationManager?.delegate = locationManagerDelegate
|
|
401
|
+
} else {
|
|
402
|
+
DispatchQueue.main.sync {
|
|
403
|
+
locationManager = CLLocationManager()
|
|
404
|
+
locationManagerDelegate = LocationManagerDelegate(geolocation: self)
|
|
405
|
+
locationManager?.delegate = locationManagerDelegate
|
|
406
|
+
}
|
|
74
407
|
}
|
|
408
|
+
}
|
|
75
409
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
410
|
+
private func updateLocationManagerConfiguration() {
|
|
411
|
+
guard let manager = locationManager else { return }
|
|
412
|
+
|
|
413
|
+
// Merge configurations from all pending requests and watches
|
|
414
|
+
var bestAccuracy = kCLLocationAccuracyHundredMeters
|
|
415
|
+
var smallestDistanceFilter = kCLDistanceFilterNone
|
|
416
|
+
var shouldUseSignificantChanges = false
|
|
417
|
+
|
|
418
|
+
for (_, request) in pendingPositionRequests {
|
|
419
|
+
bestAccuracy = min(bestAccuracy, request.options.accuracy)
|
|
420
|
+
smallestDistanceFilter = min(smallestDistanceFilter, request.options.distanceFilter)
|
|
421
|
+
shouldUseSignificantChanges = shouldUseSignificantChanges || request.options.useSignificantChanges
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
for (_, subscription) in watchSubscriptions {
|
|
425
|
+
bestAccuracy = min(bestAccuracy, subscription.options.accuracy)
|
|
426
|
+
smallestDistanceFilter = min(smallestDistanceFilter, subscription.options.distanceFilter)
|
|
427
|
+
shouldUseSignificantChanges = shouldUseSignificantChanges || subscription.options.useSignificantChanges
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
manager.desiredAccuracy = bestAccuracy
|
|
431
|
+
manager.distanceFilter = smallestDistanceFilter
|
|
432
|
+
|
|
433
|
+
// Update significant changes mode if changed
|
|
434
|
+
if shouldUseSignificantChanges != usingSignificantChanges {
|
|
435
|
+
stopMonitoring()
|
|
436
|
+
usingSignificantChanges = shouldUseSignificantChanges
|
|
437
|
+
startMonitoring()
|
|
83
438
|
}
|
|
84
439
|
}
|
|
85
440
|
|
|
86
|
-
private func
|
|
87
|
-
if
|
|
441
|
+
private func startMonitoring() {
|
|
442
|
+
if usingSignificantChanges {
|
|
443
|
+
locationManager?.startMonitoringSignificantLocationChanges()
|
|
444
|
+
} else {
|
|
445
|
+
locationManager?.startUpdatingLocation()
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private func stopMonitoring() {
|
|
450
|
+
if usingSignificantChanges {
|
|
451
|
+
locationManager?.stopMonitoringSignificantLocationChanges()
|
|
452
|
+
} else {
|
|
453
|
+
locationManager?.stopUpdatingLocation()
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private func isCachedLocationValid(_ location: CLLocation, options: ParsedOptions) -> Bool {
|
|
458
|
+
// maximumAge is infinity
|
|
459
|
+
if options.maximumAge.isInfinite {
|
|
460
|
+
return true
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Check age
|
|
464
|
+
let age = Date().timeIntervalSince(location.timestamp) * 1000 // ms
|
|
465
|
+
return age < options.maximumAge
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private func handlePositionTimeout(requestId: UUID) {
|
|
469
|
+
guard let request = pendingPositionRequests.removeValue(forKey: requestId) else {
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
request.timer?.cancel()
|
|
474
|
+
|
|
475
|
+
let timeoutSeconds = request.options.timeout / 1000.0
|
|
476
|
+
let message = String(format: "Unable to fetch location within %.1fs.", timeoutSeconds)
|
|
477
|
+
let error = GeolocationErrorWrapper(code: TIMEOUT, message: message)
|
|
478
|
+
|
|
479
|
+
request.resolver(.failure(error))
|
|
480
|
+
|
|
481
|
+
// Stop monitoring if no more watches or pending requests
|
|
482
|
+
if watchSubscriptions.isEmpty && pendingPositionRequests.isEmpty {
|
|
483
|
+
stopMonitoring()
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private func determineAuthorizationLevel() -> AuthorizationLevel {
|
|
488
|
+
if let config = configuration,
|
|
489
|
+
let authLevel = config.authorizationLevel {
|
|
490
|
+
return authLevel
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Auto-detect from Info.plist
|
|
494
|
+
return determineAuthorizationLevelFromInfoPlist()
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private func determineAuthorizationLevelFromInfoPlist() -> AuthorizationLevel {
|
|
498
|
+
let hasAlwaysKey = Bundle.main.object(forInfoDictionaryKey: "NSLocationAlwaysAndWhenInUseUsageDescription") != nil
|
|
499
|
+
let hasWhenInUseKey = Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil
|
|
500
|
+
|
|
501
|
+
if hasAlwaysKey && hasWhenInUseKey {
|
|
88
502
|
return .always
|
|
89
|
-
} else if
|
|
90
|
-
return .
|
|
503
|
+
} else if hasWhenInUseKey {
|
|
504
|
+
return .wheninuse
|
|
91
505
|
}
|
|
92
|
-
|
|
506
|
+
|
|
507
|
+
return .wheninuse // Default
|
|
93
508
|
}
|
|
94
509
|
|
|
95
|
-
private func
|
|
510
|
+
private func requestSystemPermission(for type: AuthorizationLevel) {
|
|
96
511
|
switch type {
|
|
97
512
|
case .always:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
case .
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
513
|
+
locationManager?.requestAlwaysAuthorization()
|
|
514
|
+
if configuration?.enableBackgroundLocationUpdates == true {
|
|
515
|
+
enableBackgroundLocationUpdatesIfNeeded()
|
|
516
|
+
}
|
|
517
|
+
case .wheninuse:
|
|
518
|
+
locationManager?.requestWhenInUseAuthorization()
|
|
519
|
+
case .auto:
|
|
520
|
+
let detected = determineAuthorizationLevelFromInfoPlist()
|
|
521
|
+
requestSystemPermission(for: detected)
|
|
107
522
|
}
|
|
108
523
|
}
|
|
524
|
+
|
|
525
|
+
private func enableBackgroundLocationUpdatesIfNeeded() {
|
|
526
|
+
guard let backgroundModes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String],
|
|
527
|
+
backgroundModes.contains("location") else {
|
|
528
|
+
return
|
|
529
|
+
}
|
|
530
|
+
locationManager?.allowsBackgroundLocationUpdates = true
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private func getCurrentAuthorizationStatus(from manager: CLLocationManager) -> CLAuthorizationStatus {
|
|
534
|
+
if #available(iOS 14.0, *) {
|
|
535
|
+
return manager.authorizationStatus
|
|
536
|
+
} else {
|
|
537
|
+
return CLLocationManager.authorizationStatus()
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private func mapCLAuthorizationStatus(_ status: CLAuthorizationStatus) -> PermissionStatus {
|
|
542
|
+
switch status {
|
|
543
|
+
case .authorizedAlways, .authorizedWhenInUse:
|
|
544
|
+
return .granted
|
|
545
|
+
case .denied:
|
|
546
|
+
return .denied
|
|
547
|
+
case .restricted:
|
|
548
|
+
return .restricted
|
|
549
|
+
case .notDetermined:
|
|
550
|
+
return .undetermined
|
|
551
|
+
@unknown default:
|
|
552
|
+
return .undetermined
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private func locationToPosition(_ location: CLLocation) -> GeolocationResponse {
|
|
557
|
+
let altitude = location.verticalAccuracy < 0 ? 0.0 : location.altitude
|
|
558
|
+
let altitudeAccuracy = location.verticalAccuracy < 0 ? 0.0 : location.verticalAccuracy
|
|
559
|
+
let heading = location.course >= 0 ? location.course : -1.0
|
|
560
|
+
let speed = location.speed >= 0 ? location.speed : 0.0
|
|
561
|
+
|
|
562
|
+
let coords = GeolocationCoordinates(
|
|
563
|
+
latitude: location.coordinate.latitude,
|
|
564
|
+
longitude: location.coordinate.longitude,
|
|
565
|
+
altitude: .second(altitude),
|
|
566
|
+
accuracy: location.horizontalAccuracy,
|
|
567
|
+
altitudeAccuracy: .second(altitudeAccuracy),
|
|
568
|
+
heading: .second(heading),
|
|
569
|
+
speed: .second(speed)
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
return GeolocationResponse(
|
|
573
|
+
coords: coords,
|
|
574
|
+
timestamp: location.timestamp.timeIntervalSince1970 * 1000
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private func createLocationError(code: Int, message: String) -> LocationError {
|
|
579
|
+
return LocationError(
|
|
580
|
+
code: Double(code),
|
|
581
|
+
message: message
|
|
582
|
+
)
|
|
583
|
+
}
|
|
109
584
|
}
|