react-native-nitro-geolocation 0.1.1 → 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.
Files changed (128) hide show
  1. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/GetCurrentPosition.kt +4 -4
  2. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/NitroGeolocation.kt +642 -50
  3. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/NitroGeolocationCompat.kt +81 -0
  4. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/WatchPosition.kt +4 -4
  5. package/ios/LocationManager.swift +113 -128
  6. package/ios/NitroGeolocation.swift +546 -61
  7. package/ios/NitroGeolocationCompat.swift +109 -0
  8. package/nitrogen/generated/android/c++/JAuthorizationLevel.hpp +61 -0
  9. package/nitrogen/generated/android/c++/JAuthorizationLevelInternal.hpp +3 -4
  10. package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -1
  11. package/nitrogen/generated/android/c++/JFunc_void_GeolocationError.hpp +2 -1
  12. package/nitrogen/generated/android/c++/JFunc_void_GeolocationResponse.hpp +6 -1
  13. package/nitrogen/generated/android/c++/JFunc_void_LocationError.hpp +78 -0
  14. package/nitrogen/generated/android/c++/JGeolocationCoordinates.hpp +25 -17
  15. package/nitrogen/generated/android/c++/JGeolocationError.hpp +5 -1
  16. package/nitrogen/generated/android/c++/JGeolocationOptions.hpp +5 -1
  17. package/nitrogen/generated/android/c++/JGeolocationResponse.hpp +9 -1
  18. package/nitrogen/generated/android/c++/JHybridNitroGeolocationCompatSpec.cpp +109 -0
  19. package/nitrogen/generated/android/c++/JHybridNitroGeolocationCompatSpec.hpp +70 -0
  20. package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.cpp +98 -42
  21. package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.hpp +7 -5
  22. package/nitrogen/generated/android/c++/JLocationError.hpp +61 -0
  23. package/nitrogen/generated/android/c++/JLocationProvider.hpp +61 -0
  24. package/nitrogen/generated/android/c++/JLocationProviderInternal.hpp +3 -4
  25. package/nitrogen/generated/android/c++/JLocationRequestOptions.hpp +81 -0
  26. package/nitrogen/generated/android/c++/JModernGeolocationConfiguration.hpp +73 -0
  27. package/nitrogen/generated/android/c++/JNullableDouble.cpp +26 -0
  28. package/nitrogen/generated/android/c++/JNullableDouble.hpp +69 -0
  29. package/nitrogen/generated/android/c++/JPermissionStatus.hpp +64 -0
  30. package/nitrogen/generated/android/c++/JRNConfigurationInternal.hpp +5 -1
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AuthorizationLevel.kt +24 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AuthorizationLevelInternal.kt +2 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void.kt +0 -1
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_GeolocationError.kt +0 -1
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_GeolocationResponse.kt +0 -1
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_LocationError.kt +80 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationCoordinates.kt +34 -25
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationError.kt +27 -18
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationOptions.kt +33 -24
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationResponse.kt +18 -9
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HybridNitroGeolocationCompatSpec.kt +92 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HybridNitroGeolocationSpec.kt +17 -17
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationError.kt +41 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationProvider.kt +24 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationProviderInternal.kt +2 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationRequestOptions.kt +56 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/ModernGeolocationConfiguration.kt +47 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/NullableDouble.kt +59 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/PermissionStatus.kt +25 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/RNConfigurationInternal.kt +24 -15
  51. package/nitrogen/generated/android/nitrogeolocation+autolinking.cmake +3 -0
  52. package/nitrogen/generated/android/nitrogeolocationOnLoad.cpp +14 -2
  53. package/nitrogen/generated/ios/NitroGeolocation+autolinking.rb +1 -1
  54. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.cpp +55 -13
  55. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.hpp +325 -68
  56. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Umbrella.hpp +26 -0
  57. package/nitrogen/generated/ios/NitroGeolocationAutolinking.mm +8 -0
  58. package/nitrogen/generated/ios/NitroGeolocationAutolinking.swift +15 -0
  59. package/nitrogen/generated/ios/c++/HybridNitroGeolocationCompatSpecSwift.cpp +11 -0
  60. package/nitrogen/generated/ios/c++/HybridNitroGeolocationCompatSpecSwift.hpp +130 -0
  61. package/nitrogen/generated/ios/c++/HybridNitroGeolocationSpecSwift.hpp +47 -26
  62. package/nitrogen/generated/ios/swift/AuthorizationLevel.swift +44 -0
  63. package/nitrogen/generated/ios/swift/Func_void.swift +1 -1
  64. package/nitrogen/generated/ios/swift/Func_void_GeolocationError.swift +1 -1
  65. package/nitrogen/generated/ios/swift/Func_void_GeolocationResponse.swift +1 -1
  66. package/nitrogen/generated/ios/swift/Func_void_LocationError.swift +47 -0
  67. package/nitrogen/generated/ios/swift/Func_void_PermissionStatus.swift +47 -0
  68. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  69. package/nitrogen/generated/ios/swift/GeolocationCoordinates.swift +132 -93
  70. package/nitrogen/generated/ios/swift/GeolocationError.swift +11 -40
  71. package/nitrogen/generated/ios/swift/GeolocationOptions.swift +29 -98
  72. package/nitrogen/generated/ios/swift/GeolocationResponse.swift +5 -16
  73. package/nitrogen/generated/ios/swift/HybridNitroGeolocationCompatSpec.swift +61 -0
  74. package/nitrogen/generated/ios/swift/HybridNitroGeolocationCompatSpec_cxx.swift +244 -0
  75. package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec.swift +13 -5
  76. package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec_cxx.swift +66 -64
  77. package/nitrogen/generated/ios/swift/LocationError.swift +35 -0
  78. package/nitrogen/generated/ios/swift/LocationProvider.swift +44 -0
  79. package/nitrogen/generated/ios/swift/LocationRequestOptions.swift +116 -0
  80. package/nitrogen/generated/ios/swift/ModernGeolocationConfiguration.swift +83 -0
  81. package/nitrogen/generated/ios/swift/NullableDouble.swift +18 -0
  82. package/nitrogen/generated/ios/swift/PermissionStatus.swift +48 -0
  83. package/nitrogen/generated/ios/swift/RNConfigurationInternal.swift +16 -50
  84. package/nitrogen/generated/shared/c++/AuthorizationLevel.hpp +80 -0
  85. package/nitrogen/generated/shared/c++/GeolocationCoordinates.hpp +45 -27
  86. package/nitrogen/generated/shared/c++/GeolocationError.hpp +32 -16
  87. package/nitrogen/generated/shared/c++/GeolocationOptions.hpp +38 -22
  88. package/nitrogen/generated/shared/c++/GeolocationResponse.hpp +23 -7
  89. package/nitrogen/generated/shared/c++/HybridNitroGeolocationCompatSpec.cpp +26 -0
  90. package/nitrogen/generated/shared/c++/HybridNitroGeolocationCompatSpec.hpp +79 -0
  91. package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.cpp +4 -3
  92. package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.hpp +22 -16
  93. package/nitrogen/generated/shared/c++/LocationError.hpp +87 -0
  94. package/nitrogen/generated/shared/c++/LocationProvider.hpp +80 -0
  95. package/nitrogen/generated/shared/c++/LocationRequestOptions.hpp +107 -0
  96. package/nitrogen/generated/shared/c++/ModernGeolocationConfiguration.hpp +100 -0
  97. package/nitrogen/generated/shared/c++/PermissionStatus.hpp +84 -0
  98. package/nitrogen/generated/shared/c++/RNConfigurationInternal.hpp +29 -13
  99. package/package.json +16 -7
  100. package/src/GeolocationClient.ts +116 -0
  101. package/src/NitroGeolocation.nitro.ts +177 -30
  102. package/src/NitroGeolocationCompat.nitro.ts +41 -0
  103. package/src/NitroGeolocationModule.ts +6 -0
  104. package/src/compat/clearWatch.ts +9 -0
  105. package/src/{getCurrentPosition.ts → compat/getCurrentPosition.ts} +7 -3
  106. package/src/compat/index.tsx +34 -0
  107. package/src/compat/requestAuthorization.ts +9 -0
  108. package/src/{setRNConfiguration.ts → compat/setRNConfiguration.ts} +6 -4
  109. package/src/{stopObserving.ts → compat/stopObserving.ts} +4 -2
  110. package/src/{watchPosition.ts → compat/watchPosition.ts} +7 -6
  111. package/src/components/GeolocationProvider.tsx +91 -0
  112. package/src/components/index.ts +13 -0
  113. package/src/hooks/index.ts +9 -0
  114. package/src/hooks/useCheckPermission.ts +46 -0
  115. package/src/hooks/useGetCurrentPosition.ts +44 -0
  116. package/src/hooks/useRequestPermission.ts +38 -0
  117. package/src/hooks/useWatchPosition.ts +127 -0
  118. package/src/index.tsx +72 -27
  119. package/src/types.ts +32 -4
  120. package/src/utils/cache.ts +25 -0
  121. package/src/utils/config.ts +93 -0
  122. package/src/utils/errors.ts +82 -0
  123. package/src/utils/index.ts +27 -0
  124. package/src/utils/provider.ts +61 -0
  125. package/src/utils/quality.ts +98 -0
  126. package/README.md +0 -318
  127. package/src/clearWatch.ts +0 -13
  128. package/src/requestAuthorization.ts +0 -9
@@ -1,99 +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: RNConfigurationInternal = RNConfigurationInternal(
7
- skipPermissionRequests: false,
8
- authorizationLevel: nil,
9
- enableBackgroundLocationUpdates: nil,
10
- locationProvider: nil
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
- private let locationManager = LocationManager()
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
- // MARK: - Public API
206
+ let parsedOptions = ParsedOptions.parse(from: options)
16
207
 
17
- public func setRNConfiguration(config: RNConfigurationInternal) throws {
18
- configuration = config
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
- public func requestAuthorization(success: (() -> Void)?, error: ((GeolocationError) -> Void)?)
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
- locationManager.requestAuthorization(
29
- authType: authType,
30
- skipPermissionRequests: skipPermissionRequests,
31
- enableBackgroundLocationUpdates: enableBackgroundLocationUpdates,
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
- public func getCurrentPosition(
38
- success: @escaping (GeolocationResponse) -> Void, error: ((GeolocationError) -> Void)?,
39
- options: GeolocationOptions?
40
- ) throws {
41
- locationManager.getCurrentPosition(success: success, error: error, options: options)
276
+ func unwatch(token: String) {
277
+ watchSubscriptions.removeValue(forKey: token)
278
+
279
+ // Stop monitoring if no more subscriptions or pending requests
280
+ if watchSubscriptions.isEmpty && pendingPositionRequests.isEmpty {
281
+ stopMonitoring()
282
+ }
42
283
  }
43
284
 
44
- public func watchPosition(
45
- success: @escaping (GeolocationResponse) -> Void, error: ((GeolocationError) -> Void)?,
46
- options: GeolocationOptions?
47
- ) throws -> Double {
48
- return locationManager.watchPosition(success: success, error: error, options: options)
285
+ func stopObserving() {
286
+ watchSubscriptions.removeAll()
287
+
288
+ // Stop monitoring if no pending requests
289
+ if pendingPositionRequests.isEmpty {
290
+ stopMonitoring()
291
+ }
49
292
  }
50
293
 
51
- public func clearWatch(watchId: Double) throws {
52
- locationManager.clearWatch(watchId: watchId)
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
+ }
53
312
  }
54
313
 
55
- public func stopObserving() throws {
56
- locationManager.stopObserving()
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
+ }
57
336
  }
58
337
 
59
- // MARK: - Authorization Helpers
338
+ fileprivate func handleLocationError(_ error: Error) {
339
+ let locationError: LocationError
340
+ let errorWrapper: GeolocationErrorWrapper
60
341
 
61
- private func determineAuthorizationType() -> LocationManager.AuthorizationType {
62
- guard let authLevel = configuration.authorizationLevel else {
63
- return determineAuthorizationTypeFromInfoPlist()
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
+ )
64
375
  }
65
376
 
66
- switch authLevel {
67
- case .always:
68
- return .always
69
- case .wheninuse:
70
- return .whenInUse
71
- case .auto:
72
- return determineAuthorizationTypeFromInfoPlist()
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()
390
+ }
391
+
392
+ // MARK: - Helper Functions
393
+
394
+ private func initializeLocationManagerIfNeeded() {
395
+ guard locationManager == nil else { return }
396
+
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
+ }
73
407
  }
74
408
  }
75
409
 
76
- private func determineAuthorizationTypeFromInfoPlist() -> LocationManager.AuthorizationType {
77
- if hasInfoPlistKey(for: .always) {
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()
438
+ }
439
+ }
440
+
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 {
78
502
  return .always
79
- } else if hasInfoPlistKey(for: .whenInUse) {
80
- return .whenInUse
503
+ } else if hasWhenInUseKey {
504
+ return .wheninuse
81
505
  }
82
- return .none
506
+
507
+ return .wheninuse // Default
83
508
  }
84
509
 
85
- private func hasInfoPlistKey(for type: LocationManager.AuthorizationType) -> Bool {
510
+ private func requestSystemPermission(for type: AuthorizationLevel) {
86
511
  switch type {
87
512
  case .always:
88
- return Bundle.main.object(forInfoDictionaryKey: "NSLocationAlwaysUsageDescription")
89
- != nil
90
- || Bundle.main.object(
91
- forInfoDictionaryKey: "NSLocationAlwaysAndWhenInUseUsageDescription") != nil
92
- case .whenInUse:
93
- return Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription")
94
- != nil
95
- case .none:
96
- return false
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)
97
522
  }
98
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
+ }
99
584
  }