react-native-nitro-geolocation 1.1.3 → 1.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 (186) hide show
  1. package/README.md +316 -0
  2. package/android/build.gradle +6 -0
  3. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/AndroidAccuracy.kt +105 -0
  4. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/AndroidHeadingManager.kt +313 -0
  5. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/AndroidLocationSettings.kt +313 -0
  6. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/GetCurrentPosition.kt +46 -45
  7. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/LocationMetadata.kt +26 -0
  8. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/LocationValues.kt +31 -0
  9. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/NitroGeolocation.kt +1025 -140
  10. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/NitroGeolocationCompat.kt +11 -11
  11. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/RequestAuthorization.kt +6 -6
  12. package/android/src/main/java/com/margelo/nitro/nitrogeolocation/WatchPosition.kt +46 -45
  13. package/ios/CLLocation+GeolocationMetadata.swift +32 -0
  14. package/ios/LocationManager.swift +205 -51
  15. package/ios/NitroGeolocation.swift +949 -110
  16. package/ios/NitroGeolocationCompat.swift +7 -7
  17. package/nitrogen/generated/android/c++/JAccuracyAuthorization.hpp +61 -0
  18. package/nitrogen/generated/android/c++/JAndroidAccuracyPreset.hpp +64 -0
  19. package/nitrogen/generated/android/c++/JAndroidGranularity.hpp +61 -0
  20. package/nitrogen/generated/android/c++/{JRNConfigurationInternal.hpp → JCompatGeolocationConfigurationInternal.hpp} +10 -10
  21. package/nitrogen/generated/android/c++/{JGeolocationError.hpp → JCompatGeolocationError.hpp} +10 -10
  22. package/nitrogen/generated/android/c++/JCompatGeolocationOptions.hpp +105 -0
  23. package/nitrogen/generated/android/c++/JCompatGeolocationResponse.hpp +67 -0
  24. package/nitrogen/generated/android/c++/JFunc_void_AccuracyAuthorization.hpp +77 -0
  25. package/nitrogen/generated/android/c++/JFunc_void_CompatGeolocationError.hpp +78 -0
  26. package/nitrogen/generated/android/c++/JFunc_void_CompatGeolocationResponse.hpp +84 -0
  27. package/nitrogen/generated/android/c++/JFunc_void_GeolocationResponse.hpp +2 -0
  28. package/nitrogen/generated/android/c++/JFunc_void_Heading.hpp +78 -0
  29. package/nitrogen/generated/android/c++/JFunc_void_LocationProviderStatus.hpp +78 -0
  30. package/nitrogen/generated/android/c++/JFunc_void_PermissionStatus.hpp +77 -0
  31. package/nitrogen/generated/android/c++/JFunc_void_std__vector_GeocodedLocation_.hpp +97 -0
  32. package/nitrogen/generated/android/c++/JFunc_void_std__vector_ReverseGeocodedAddress_.hpp +98 -0
  33. package/nitrogen/generated/android/c++/JGeocodedLocation.hpp +65 -0
  34. package/nitrogen/generated/android/c++/JGeocodingCoordinates.hpp +61 -0
  35. package/nitrogen/generated/android/c++/{JModernGeolocationConfiguration.hpp → JGeolocationConfiguration.hpp} +10 -10
  36. package/nitrogen/generated/android/c++/JGeolocationResponse.hpp +13 -3
  37. package/nitrogen/generated/android/c++/JHeading.hpp +69 -0
  38. package/nitrogen/generated/android/c++/JHeadingOptions.hpp +57 -0
  39. package/nitrogen/generated/android/c++/JHybridNitroGeolocationCompatSpec.cpp +46 -30
  40. package/nitrogen/generated/android/c++/JHybridNitroGeolocationCompatSpec.hpp +4 -4
  41. package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.cpp +169 -33
  42. package/nitrogen/generated/android/c++/JHybridNitroGeolocationSpec.hpp +14 -3
  43. package/nitrogen/generated/android/c++/JIOSAccuracyPreset.hpp +73 -0
  44. package/nitrogen/generated/android/c++/JIOSActivityType.hpp +67 -0
  45. package/nitrogen/generated/android/c++/JLocationAccuracyOptions.hpp +65 -0
  46. package/nitrogen/generated/android/c++/JLocationAvailability.hpp +62 -0
  47. package/nitrogen/generated/android/c++/JLocationProviderStatus.hpp +77 -0
  48. package/nitrogen/generated/android/c++/JLocationProviderUsed.hpp +67 -0
  49. package/nitrogen/generated/android/c++/JLocationRequestOptions.hpp +49 -3
  50. package/nitrogen/generated/android/c++/{JGeolocationOptions.hpp → JLocationSettingsOptions.hpp} +28 -22
  51. package/nitrogen/generated/android/c++/JReverseGeocodedAddress.hpp +82 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AccuracyAuthorization.kt +24 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AndroidAccuracyPreset.kt +25 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/AndroidGranularity.kt +24 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/{RNConfigurationInternal.kt → CompatGeolocationConfigurationInternal.kt} +5 -5
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/{GeolocationError.kt → CompatGeolocationError.kt} +5 -5
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/CompatGeolocationOptions.kt +68 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/CompatGeolocationResponse.kt +41 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_AccuracyAuthorization.kt +80 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/{Func_void_GeolocationError.kt → Func_void_CompatGeolocationError.kt} +9 -9
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_CompatGeolocationResponse.kt +80 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_Heading.kt +80 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_LocationProviderStatus.kt +80 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_PermissionStatus.kt +80 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_std__vector_GeocodedLocation_.kt +80 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Func_void_std__vector_ReverseGeocodedAddress_.kt +80 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeocodedLocation.kt +44 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeocodingCoordinates.kt +41 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/{ModernGeolocationConfiguration.kt → GeolocationConfiguration.kt} +5 -5
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/GeolocationResponse.kt +9 -3
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/Heading.kt +47 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HeadingOptions.kt +38 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HybridNitroGeolocationCompatSpec.kt +7 -7
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/HybridNitroGeolocationSpec.kt +92 -3
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/IOSAccuracyPreset.kt +28 -0
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/IOSActivityType.kt +26 -0
  77. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationAccuracyOptions.kt +41 -0
  78. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationAvailability.kt +41 -0
  79. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationProviderStatus.kt +53 -0
  80. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationProviderUsed.kt +26 -0
  81. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/LocationRequestOptions.kt +30 -3
  82. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/{GeolocationOptions.kt → LocationSettingsOptions.kt} +11 -11
  83. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrogeolocation/ReverseGeocodedAddress.kt +56 -0
  84. package/nitrogen/generated/android/nitrogeolocationOnLoad.cpp +18 -4
  85. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.cpp +76 -12
  86. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Bridge.hpp +519 -77
  87. package/nitrogen/generated/ios/NitroGeolocation-Swift-Cxx-Umbrella.hpp +61 -12
  88. package/nitrogen/generated/ios/c++/HybridNitroGeolocationCompatSpecSwift.hpp +28 -16
  89. package/nitrogen/generated/ios/c++/HybridNitroGeolocationSpecSwift.hpp +131 -13
  90. package/nitrogen/generated/ios/swift/AccuracyAuthorization.swift +44 -0
  91. package/nitrogen/generated/ios/swift/AndroidAccuracyPreset.swift +48 -0
  92. package/nitrogen/generated/ios/swift/AndroidGranularity.swift +44 -0
  93. package/nitrogen/generated/ios/swift/{RNConfigurationInternal.swift → CompatGeolocationConfigurationInternal.swift} +5 -5
  94. package/nitrogen/generated/ios/swift/{GeolocationError.swift → CompatGeolocationError.swift} +5 -5
  95. package/nitrogen/generated/ios/swift/CompatGeolocationOptions.swift +208 -0
  96. package/nitrogen/generated/ios/swift/CompatGeolocationResponse.swift +34 -0
  97. package/nitrogen/generated/ios/swift/Func_void_AccuracyAuthorization.swift +46 -0
  98. package/nitrogen/generated/ios/swift/Func_void_CompatGeolocationError.swift +46 -0
  99. package/nitrogen/generated/ios/swift/Func_void_CompatGeolocationResponse.swift +46 -0
  100. package/nitrogen/generated/ios/swift/{Func_void_GeolocationError.swift → Func_void_Heading.swift} +11 -11
  101. package/nitrogen/generated/ios/swift/Func_void_LocationAvailability.swift +46 -0
  102. package/nitrogen/generated/ios/swift/Func_void_LocationProviderStatus.swift +46 -0
  103. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  104. package/nitrogen/generated/ios/swift/Func_void_std__vector_GeocodedLocation_.swift +46 -0
  105. package/nitrogen/generated/ios/swift/Func_void_std__vector_ReverseGeocodedAddress_.swift +46 -0
  106. package/nitrogen/generated/ios/swift/GeocodedLocation.swift +52 -0
  107. package/nitrogen/generated/ios/swift/GeocodingCoordinates.swift +34 -0
  108. package/nitrogen/generated/ios/swift/{ModernGeolocationConfiguration.swift → GeolocationConfiguration.swift} +5 -5
  109. package/nitrogen/generated/ios/swift/GeolocationResponse.swift +31 -2
  110. package/nitrogen/generated/ios/swift/Heading.swift +70 -0
  111. package/nitrogen/generated/ios/swift/HeadingOptions.swift +42 -0
  112. package/nitrogen/generated/ios/swift/HybridNitroGeolocationCompatSpec.swift +4 -4
  113. package/nitrogen/generated/ios/swift/HybridNitroGeolocationCompatSpec_cxx.swift +28 -28
  114. package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec.swift +14 -3
  115. package/nitrogen/generated/ios/swift/HybridNitroGeolocationSpec_cxx.swift +318 -15
  116. package/nitrogen/generated/ios/swift/IOSAccuracyPreset.swift +60 -0
  117. package/nitrogen/generated/ios/swift/IOSActivityType.swift +52 -0
  118. package/nitrogen/generated/ios/swift/LocationAccuracyOptions.swift +46 -0
  119. package/nitrogen/generated/ios/swift/LocationAvailability.swift +47 -0
  120. package/nitrogen/generated/ios/swift/LocationProviderStatus.swift +106 -0
  121. package/nitrogen/generated/ios/swift/LocationProviderUsed.swift +52 -0
  122. package/nitrogen/generated/ios/swift/LocationRequestOptions.swift +142 -1
  123. package/nitrogen/generated/ios/swift/{GeolocationOptions.swift → LocationSettingsOptions.swift} +39 -46
  124. package/nitrogen/generated/ios/swift/ReverseGeocodedAddress.swift +150 -0
  125. package/nitrogen/generated/shared/c++/AccuracyAuthorization.hpp +80 -0
  126. package/nitrogen/generated/shared/c++/AndroidAccuracyPreset.hpp +84 -0
  127. package/nitrogen/generated/shared/c++/AndroidGranularity.hpp +80 -0
  128. package/nitrogen/generated/shared/c++/{RNConfigurationInternal.hpp → CompatGeolocationConfigurationInternal.hpp} +11 -11
  129. package/nitrogen/generated/shared/c++/{GeolocationError.hpp → CompatGeolocationError.hpp} +11 -11
  130. package/nitrogen/generated/shared/c++/CompatGeolocationOptions.hpp +128 -0
  131. package/nitrogen/generated/shared/c++/CompatGeolocationResponse.hpp +88 -0
  132. package/nitrogen/generated/shared/c++/GeocodedLocation.hpp +91 -0
  133. package/nitrogen/generated/shared/c++/GeocodingCoordinates.hpp +87 -0
  134. package/nitrogen/generated/shared/c++/{ModernGeolocationConfiguration.hpp → GeolocationConfiguration.hpp} +11 -11
  135. package/nitrogen/generated/shared/c++/GeolocationResponse.hpp +14 -2
  136. package/nitrogen/generated/shared/c++/Heading.hpp +95 -0
  137. package/nitrogen/generated/shared/c++/HeadingOptions.hpp +83 -0
  138. package/nitrogen/generated/shared/c++/HybridNitroGeolocationCompatSpec.hpp +16 -16
  139. package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.cpp +11 -0
  140. package/nitrogen/generated/shared/c++/HybridNitroGeolocationSpec.hpp +51 -12
  141. package/nitrogen/generated/shared/c++/IOSAccuracyPreset.hpp +96 -0
  142. package/nitrogen/generated/shared/c++/IOSActivityType.hpp +88 -0
  143. package/nitrogen/generated/shared/c++/LocationAccuracyOptions.hpp +92 -0
  144. package/nitrogen/generated/shared/c++/LocationAvailability.hpp +88 -0
  145. package/nitrogen/generated/shared/c++/LocationProviderStatus.hpp +103 -0
  146. package/nitrogen/generated/shared/c++/LocationProviderUsed.hpp +88 -0
  147. package/nitrogen/generated/shared/c++/LocationRequestOptions.hpp +47 -3
  148. package/nitrogen/generated/shared/c++/{GeolocationOptions.hpp → LocationSettingsOptions.hpp} +26 -24
  149. package/nitrogen/generated/shared/c++/ReverseGeocodedAddress.hpp +108 -0
  150. package/package.json +1 -1
  151. package/src/NitroGeolocation.nitro.ts +291 -17
  152. package/src/NitroGeolocationCompat.nitro.ts +12 -12
  153. package/src/api/geocode.ts +18 -0
  154. package/src/api/getAccuracyAuthorization.ts +12 -0
  155. package/src/api/getCurrentPosition.ts +5 -3
  156. package/src/api/getHeading.ts +13 -0
  157. package/src/api/getLastKnownPosition.ts +28 -0
  158. package/src/api/getLocationAvailability.ts +11 -0
  159. package/src/api/getProviderStatus.ts +16 -0
  160. package/src/api/hasServicesEnabled.ts +13 -0
  161. package/src/api/index.ts +11 -0
  162. package/src/api/requestLocationSettings.ts +29 -0
  163. package/src/api/requestPermission.ts +3 -1
  164. package/src/api/requestTemporaryFullAccuracy.ts +21 -0
  165. package/src/api/reverseGeocode.ts +23 -0
  166. package/src/api/setConfiguration.ts +8 -4
  167. package/src/api/watchHeading.ts +19 -0
  168. package/src/api/watchPosition.ts +2 -2
  169. package/src/compat/getCurrentPosition.ts +7 -7
  170. package/src/compat/index.tsx +5 -5
  171. package/src/compat/requestAuthorization.ts +2 -2
  172. package/src/compat/setRNConfiguration.ts +7 -5
  173. package/src/compat/watchPosition.ts +7 -7
  174. package/src/devtools/getCurrentPosition.ts +5 -3
  175. package/src/devtools/index.ts +1 -1
  176. package/src/devtools/watchPosition.ts +6 -7
  177. package/src/hooks/useWatchPosition.ts +2 -2
  178. package/src/index.tsx +35 -6
  179. package/src/publicTypes.ts +96 -0
  180. package/src/types.ts +113 -37
  181. package/src/utils/errors.test.ts +65 -0
  182. package/src/utils/errors.ts +45 -18
  183. package/src/utils/index.ts +2 -2
  184. package/src/utils/provider.test.ts +172 -1
  185. package/src/utils/provider.ts +50 -5
  186. package/nitrogen/generated/android/c++/JFunc_void_GeolocationError.hpp +0 -78
@@ -2,28 +2,6 @@ import Foundation
2
2
  import CoreLocation
3
3
  import NitroModules
4
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
5
  /**
28
6
  * LocationManager Delegate class to handle CLLocationManager callbacks.
29
7
  */
@@ -46,13 +24,21 @@ private class LocationManagerDelegate: NSObject, CLLocationManagerDelegate {
46
24
  func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
47
25
  geolocation?.handleLocationError(error)
48
26
  }
27
+
28
+ func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
29
+ geolocation?.handleHeadingUpdate(newHeading)
30
+ }
31
+
32
+ func locationManagerShouldDisplayHeadingCalibration(_ manager: CLLocationManager) -> Bool {
33
+ return false
34
+ }
49
35
  }
50
36
 
51
37
  /**
52
- * Modern Geolocation implementation with Promise-based API.
38
+ * Geolocation implementation for the native Modern API contract.
53
39
  *
54
40
  * Key features:
55
- * - Promise-based permission and getCurrentPosition
41
+ * - Callback-based native permission and getCurrentPosition for structured errors
56
42
  * - Token-based watch subscriptions (functions are first-class!)
57
43
  * - WatchPositionResult discriminated union
58
44
  * - Automatic subscription management
@@ -66,17 +52,24 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
66
52
  let accuracy: CLLocationAccuracy
67
53
  let distanceFilter: CLLocationDistance
68
54
  let useSignificantChanges: Bool
55
+ let activityType: CLActivityType?
56
+ let pausesLocationUpdatesAutomatically: Bool?
57
+ let showsBackgroundLocationIndicator: Bool?
69
58
 
70
59
  static let DEFAULT_TIMEOUT: Double = 10 * 60 * 1000 // 10 minutes in ms
71
60
  static let DEFAULT_MAXIMUM_AGE: Double = 0
72
61
 
73
- static func parse(from options: LocationRequestOptions?) -> ParsedOptions {
62
+ static func parse(
63
+ from options: LocationRequestOptions?,
64
+ defaultMaximumAge: Double = DEFAULT_MAXIMUM_AGE
65
+ ) -> ParsedOptions {
74
66
  let timeout = options?.timeout ?? DEFAULT_TIMEOUT
75
- let maximumAge = options?.maximumAge ?? DEFAULT_MAXIMUM_AGE
67
+ let maximumAge = options?.maximumAge ?? defaultMaximumAge
76
68
  let enableHighAccuracy = options?.enableHighAccuracy ?? false
77
- let accuracy = enableHighAccuracy
78
- ? kCLLocationAccuracyBest
79
- : kCLLocationAccuracyHundredMeters
69
+ let accuracy = resolveAccuracy(
70
+ preset: options?.accuracy?.ios,
71
+ enableHighAccuracy: enableHighAccuracy
72
+ )
80
73
  let distanceFilter = options?.distanceFilter ?? kCLDistanceFilterNone
81
74
  let useSignificantChanges = options?.useSignificantChanges ?? false
82
75
 
@@ -85,9 +78,63 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
85
78
  maximumAge: maximumAge,
86
79
  accuracy: accuracy,
87
80
  distanceFilter: distanceFilter,
88
- useSignificantChanges: useSignificantChanges
81
+ useSignificantChanges: useSignificantChanges,
82
+ activityType: resolveActivityType(options?.activityType),
83
+ pausesLocationUpdatesAutomatically: options?.pausesLocationUpdatesAutomatically,
84
+ showsBackgroundLocationIndicator: options?.showsBackgroundLocationIndicator
89
85
  )
90
86
  }
87
+
88
+ static func parseLastKnown(from options: LocationRequestOptions?) -> ParsedOptions {
89
+ return parse(from: options, defaultMaximumAge: Double.infinity)
90
+ }
91
+
92
+ private static func resolveAccuracy(
93
+ preset: IOSAccuracyPreset?,
94
+ enableHighAccuracy: Bool
95
+ ) -> CLLocationAccuracy {
96
+ guard let preset else {
97
+ return enableHighAccuracy
98
+ ? kCLLocationAccuracyBest
99
+ : kCLLocationAccuracyHundredMeters
100
+ }
101
+
102
+ switch preset {
103
+ case .bestfornavigation:
104
+ return kCLLocationAccuracyBestForNavigation
105
+ case .best:
106
+ return kCLLocationAccuracyBest
107
+ case .nearesttenmeters:
108
+ return kCLLocationAccuracyNearestTenMeters
109
+ case .hundredmeters:
110
+ return kCLLocationAccuracyHundredMeters
111
+ case .kilometer:
112
+ return kCLLocationAccuracyKilometer
113
+ case .threekilometers:
114
+ return kCLLocationAccuracyThreeKilometers
115
+ case .reduced:
116
+ return kCLLocationAccuracyReduced
117
+ }
118
+ }
119
+
120
+ private static func resolveActivityType(_ activityType: IOSActivityType?) -> CLActivityType? {
121
+ guard let activityType else {
122
+ return nil
123
+ }
124
+
125
+ switch activityType {
126
+ case .other:
127
+ return .other
128
+ case .automotivenavigation:
129
+ return .automotiveNavigation
130
+ case .fitness:
131
+ return .fitness
132
+ case .othernavigation:
133
+ return .otherNavigation
134
+ case .airborne:
135
+ return .airborne
136
+ }
137
+ }
91
138
  }
92
139
 
93
140
  // Watch subscription storage (first-class functions!)
@@ -98,23 +145,49 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
98
145
  let options: ParsedOptions
99
146
  }
100
147
 
148
+ private struct ParsedHeadingOptions {
149
+ let headingFilter: CLLocationDegrees
150
+
151
+ static func parse(from options: HeadingOptions?) -> ParsedHeadingOptions {
152
+ return ParsedHeadingOptions(
153
+ headingFilter: options?.headingFilter ?? 0
154
+ )
155
+ }
156
+ }
157
+
158
+ private struct HeadingRequest {
159
+ let id: UUID
160
+ let success: (Heading) -> Void
161
+ let error: (LocationError) -> Void
162
+ var timer: DispatchSourceTimer?
163
+ }
164
+
165
+ private struct HeadingSubscription {
166
+ let token: String
167
+ let success: (Heading) -> Void
168
+ let error: ((LocationError) -> Void)?
169
+ let options: ParsedHeadingOptions
170
+ var lastDeliveredHeading: Double?
171
+ }
172
+
101
173
  // MARK: - Properties
102
174
 
103
- private var configuration: ModernGeolocationConfiguration?
175
+ private var configuration: GeolocationConfiguration?
104
176
  private var locationManager: CLLocationManager?
105
177
  private var locationManagerDelegate: LocationManagerDelegate?
106
178
  private var lastLocation: CLLocation?
107
179
  private var usingSignificantChanges: Bool = false
108
180
 
109
- // Permission promise resolvers
110
- private var pendingPermissionResolvers: [(Result<PermissionStatus, Error>) -> Void] = []
181
+ // Permission callbacks
182
+ private var pendingPermissionResolvers: [(PermissionStatus) -> Void] = []
111
183
 
112
184
  // getCurrentPosition promise resolvers with timeout
113
185
  private var pendingPositionRequests: [UUID: PositionRequest] = [:]
114
186
 
115
187
  private struct PositionRequest {
116
188
  let id: UUID
117
- let resolver: (Result<GeolocationResponse, Error>) -> Void
189
+ let success: (GeolocationResponse) -> Void
190
+ let error: (LocationError) -> Void
118
191
  let options: ParsedOptions
119
192
  var timer: DispatchSourceTimer?
120
193
  }
@@ -122,18 +195,27 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
122
195
  // Watch subscriptions (token -> callback)
123
196
  private var watchSubscriptions: [String: WatchSubscription] = [:]
124
197
 
198
+ // Heading requests/subscriptions
199
+ private var pendingHeadingRequests: [UUID: HeadingRequest] = [:]
200
+ private var headingSubscriptions: [String: HeadingSubscription] = [:]
201
+ private var activeGeocoders: [UUID: CLGeocoder] = [:]
202
+
125
203
  // Error codes
204
+ private let INTERNAL_ERROR = -1
126
205
  private let PERMISSION_DENIED = 1
127
206
  private let POSITION_UNAVAILABLE = 2
128
207
  private let TIMEOUT = 3
208
+ private let PLAY_SERVICE_NOT_AVAILABLE = 4
209
+ private let SETTINGS_NOT_SATISFIED = 5
210
+ private let DEFAULT_HEADING_TIMEOUT_MS: Double = 10_000
129
211
 
130
212
  // MARK: - Configuration
131
213
 
132
- func setConfiguration(config: ModernGeolocationConfiguration) {
214
+ func setConfiguration(config: GeolocationConfiguration) {
133
215
  self.configuration = config
134
216
  }
135
217
 
136
- // MARK: - Permission API (Promise-based)
218
+ // MARK: - Permission API
137
219
 
138
220
  func checkPermission() throws -> Promise<PermissionStatus> {
139
221
  return Promise.async {
@@ -142,9 +224,10 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
142
224
  }
143
225
  }
144
226
 
145
- func requestPermission() throws -> Promise<PermissionStatus> {
146
- let promise = Promise<PermissionStatus>()
147
-
227
+ func requestPermission(
228
+ success: @escaping (PermissionStatus) -> Void,
229
+ error: ((LocationError) -> Void)?
230
+ ) throws -> Void {
148
231
  self.initializeLocationManagerIfNeeded()
149
232
 
150
233
  let currentStatus = CLLocationManager.authorizationStatus()
@@ -152,53 +235,152 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
152
235
  // Already determined
153
236
  if currentStatus != .notDetermined {
154
237
  let status = self.mapCLAuthorizationStatus(currentStatus)
155
- promise.resolve(withResult: status)
156
- return promise
238
+ success(status)
239
+ return
157
240
  }
158
241
 
159
242
  // 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
- }
243
+ self.pendingPermissionResolvers.append(success)
168
244
 
169
245
  // Request permission
170
246
  let authLevel = self.determineAuthorizationLevel()
171
247
  self.requestSystemPermission(for: authLevel)
248
+ }
249
+
250
+ // MARK: - Provider/Settings API
251
+
252
+ func hasServicesEnabled() throws -> Promise<Bool> {
253
+ return Promise.async {
254
+ return CLLocationManager.locationServicesEnabled()
255
+ }
256
+ }
257
+
258
+ func getProviderStatus() throws -> Promise<LocationProviderStatus> {
259
+ return Promise.async {
260
+ return self.createLocationProviderStatus()
261
+ }
262
+ }
263
+
264
+ func getLocationAvailability() throws -> Promise<LocationAvailability> {
265
+ return Promise.async {
266
+ guard CLLocationManager.locationServicesEnabled() else {
267
+ return LocationAvailability(
268
+ available: false,
269
+ reason: "locationServicesDisabled"
270
+ )
271
+ }
272
+
273
+ let status = CLLocationManager.authorizationStatus()
274
+ switch status {
275
+ case .authorizedAlways, .authorizedWhenInUse:
276
+ return LocationAvailability(available: true, reason: nil)
277
+ case .notDetermined:
278
+ return LocationAvailability(
279
+ available: false,
280
+ reason: "permissionUndetermined"
281
+ )
282
+ case .denied:
283
+ return LocationAvailability(
284
+ available: false,
285
+ reason: "permissionDenied"
286
+ )
287
+ case .restricted:
288
+ return LocationAvailability(
289
+ available: false,
290
+ reason: "permissionRestricted"
291
+ )
292
+ @unknown default:
293
+ return LocationAvailability(
294
+ available: false,
295
+ reason: "authorizationUnknown"
296
+ )
297
+ }
298
+ }
299
+ }
172
300
 
173
- return promise
301
+ func requestLocationSettings(
302
+ success: @escaping (LocationProviderStatus) -> Void,
303
+ error: ((LocationError) -> Void)?,
304
+ options: LocationSettingsOptions?
305
+ ) throws -> Void {
306
+ success(createLocationProviderStatus())
307
+ }
308
+
309
+ func getAccuracyAuthorization() throws -> Promise<AccuracyAuthorization> {
310
+ return Promise.async {
311
+ return self.getCurrentAccuracyAuthorizationOnMain()
312
+ }
174
313
  }
175
314
 
176
- // MARK: - Get Current Position (Promise-based)
315
+ func requestTemporaryFullAccuracy(
316
+ purposeKey: String,
317
+ success: @escaping (AccuracyAuthorization) -> Void,
318
+ error: ((LocationError) -> Void)?
319
+ ) throws -> Void {
320
+ let trimmedPurposeKey = purposeKey.trimmingCharacters(in: .whitespacesAndNewlines)
321
+ guard !trimmedPurposeKey.isEmpty else {
322
+ error?(createLocationError(
323
+ code: INTERNAL_ERROR,
324
+ message: "purposeKey must not be empty."
325
+ ))
326
+ return
327
+ }
328
+
329
+ initializeLocationManagerIfNeeded()
330
+
331
+ DispatchQueue.main.async {
332
+ guard #available(iOS 14.0, *), let manager = self.locationManager else {
333
+ success(.unknown)
334
+ return
335
+ }
336
+
337
+ if manager.accuracyAuthorization == .fullAccuracy {
338
+ success(.full)
339
+ return
340
+ }
341
+
342
+ manager.requestTemporaryFullAccuracyAuthorization(
343
+ withPurposeKey: trimmedPurposeKey
344
+ ) { requestError in
345
+ if let requestError {
346
+ error?(self.createLocationError(
347
+ code: self.INTERNAL_ERROR,
348
+ message: "Unable to request temporary full accuracy: \(requestError.localizedDescription)"
349
+ ))
350
+ return
351
+ }
352
+
353
+ success(self.getCurrentAccuracyAuthorization())
354
+ }
355
+ }
356
+ }
177
357
 
178
- func getCurrentPosition(options: LocationRequestOptions?) throws -> Promise<GeolocationResponse> {
179
- let promise = Promise<GeolocationResponse>()
358
+ // MARK: - Get Current Position
180
359
 
360
+ func getCurrentPosition(
361
+ success: @escaping (GeolocationResponse) -> Void,
362
+ error: ((LocationError) -> Void)?,
363
+ options: LocationRequestOptions?
364
+ ) throws -> Void {
181
365
  // Check permission
182
366
  let status = CLLocationManager.authorizationStatus()
183
367
  if status == .denied || status == .restricted {
184
368
  let message = status == .restricted
185
369
  ? "This application is not authorized to use location services"
186
370
  : "User denied access to location services."
187
- let error = GeolocationErrorWrapper(
371
+ error?(createLocationError(
188
372
  code: self.PERMISSION_DENIED,
189
373
  message: message
190
- )
191
- promise.reject(withError: error)
192
- return promise
374
+ ))
375
+ return
193
376
  }
194
377
 
195
378
  if !CLLocationManager.locationServicesEnabled() {
196
- let error = GeolocationErrorWrapper(
197
- code: self.POSITION_UNAVAILABLE,
379
+ error?(createLocationError(
380
+ code: self.SETTINGS_NOT_SATISFIED,
198
381
  message: "Location services disabled."
199
- )
200
- promise.reject(withError: error)
201
- return promise
382
+ ))
383
+ return
202
384
  }
203
385
 
204
386
  self.initializeLocationManagerIfNeeded()
@@ -206,24 +388,20 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
206
388
  let parsedOptions = ParsedOptions.parse(from: options)
207
389
 
208
390
  // Check cached location
209
- if let cached = self.lastLocation,
210
- self.isCachedLocationValid(cached, options: parsedOptions) {
391
+ if let cached = self.getBestCachedLocation(options: parsedOptions) {
392
+ self.lastLocation = cached
211
393
  let position = self.locationToPosition(cached)
212
- promise.resolve(withResult: position)
213
- return promise
394
+ success(position)
395
+ return
214
396
  }
215
397
 
216
398
  // Create position request
217
399
  let id = UUID()
218
400
  var request = PositionRequest(
219
401
  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
- }
402
+ success: success,
403
+ error: { locationError in
404
+ error?(locationError)
227
405
  },
228
406
  options: parsedOptions,
229
407
  timer: nil
@@ -243,8 +421,203 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
243
421
  // Update configuration and start monitoring
244
422
  self.updateLocationManagerConfiguration()
245
423
  self.startMonitoring()
424
+ }
246
425
 
247
- return promise
426
+ func getLastKnownPosition(
427
+ success: @escaping (GeolocationResponse) -> Void,
428
+ error: ((LocationError) -> Void)?,
429
+ options: LocationRequestOptions?
430
+ ) throws -> Void {
431
+ let status = CLLocationManager.authorizationStatus()
432
+ if status == .denied || status == .restricted {
433
+ let message = status == .restricted
434
+ ? "This application is not authorized to use location services"
435
+ : "User denied access to location services."
436
+ error?(createLocationError(
437
+ code: self.PERMISSION_DENIED,
438
+ message: message
439
+ ))
440
+ return
441
+ }
442
+
443
+ let parsedOptions = ParsedOptions.parseLastKnown(from: options)
444
+ guard let cached = self.getBestCachedLocation(options: parsedOptions) else {
445
+ error?(createLocationError(
446
+ code: self.POSITION_UNAVAILABLE,
447
+ message: "No cached location available."
448
+ ))
449
+ return
450
+ }
451
+
452
+ self.lastLocation = cached
453
+ success(self.locationToPosition(cached))
454
+ }
455
+
456
+ // MARK: - Geocoding
457
+
458
+ func geocode(
459
+ address: String,
460
+ success: @escaping ([GeocodedLocation]) -> Void,
461
+ error: ((LocationError) -> Void)?
462
+ ) throws -> Void {
463
+ let query = address.trimmingCharacters(in: .whitespacesAndNewlines)
464
+ guard !query.isEmpty else {
465
+ error?(createLocationError(
466
+ code: INTERNAL_ERROR,
467
+ message: "address must not be empty."
468
+ ))
469
+ return
470
+ }
471
+
472
+ DispatchQueue.main.async {
473
+ let id = UUID()
474
+ let geocoder = CLGeocoder()
475
+ self.activeGeocoders[id] = geocoder
476
+
477
+ geocoder.geocodeAddressString(query) { [weak self] placemarks, geocodeError in
478
+ guard let self else { return }
479
+
480
+ DispatchQueue.main.async {
481
+ self.activeGeocoders.removeValue(forKey: id)
482
+
483
+ if let geocodeError {
484
+ if self.isNoGeocoderResult(geocodeError) {
485
+ success([])
486
+ return
487
+ }
488
+
489
+ error?(self.createGeocoderError(
490
+ geocodeError,
491
+ messagePrefix: "Unable to geocode address"
492
+ ))
493
+ return
494
+ }
495
+
496
+ let locations = (placemarks ?? []).compactMap {
497
+ self.placemarkToGeocodedLocation($0)
498
+ }
499
+ success(locations)
500
+ }
501
+ }
502
+ }
503
+ }
504
+
505
+ func reverseGeocode(
506
+ coords: GeocodingCoordinates,
507
+ success: @escaping ([ReverseGeocodedAddress]) -> Void,
508
+ error: ((LocationError) -> Void)?
509
+ ) throws -> Void {
510
+ if let validationError = validateGeocodingCoordinates(coords) {
511
+ error?(validationError)
512
+ return
513
+ }
514
+
515
+ DispatchQueue.main.async {
516
+ let id = UUID()
517
+ let geocoder = CLGeocoder()
518
+ self.activeGeocoders[id] = geocoder
519
+
520
+ let location = CLLocation(
521
+ latitude: coords.latitude,
522
+ longitude: coords.longitude
523
+ )
524
+
525
+ geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, geocodeError in
526
+ guard let self else { return }
527
+
528
+ DispatchQueue.main.async {
529
+ self.activeGeocoders.removeValue(forKey: id)
530
+
531
+ if let geocodeError {
532
+ if self.isNoGeocoderResult(geocodeError) {
533
+ success([])
534
+ return
535
+ }
536
+
537
+ error?(self.createGeocoderError(
538
+ geocodeError,
539
+ messagePrefix: "Unable to reverse geocode coordinates"
540
+ ))
541
+ return
542
+ }
543
+
544
+ let addresses = (placemarks ?? []).map {
545
+ self.placemarkToReverseGeocodedAddress($0)
546
+ }
547
+ success(addresses)
548
+ }
549
+ }
550
+ }
551
+ }
552
+
553
+ // MARK: - Heading
554
+
555
+ func getHeading(
556
+ success: @escaping (Heading) -> Void,
557
+ error: ((LocationError) -> Void)?
558
+ ) throws -> Void {
559
+ guard validateHeadingAvailability(error: error) else { return }
560
+
561
+ initializeLocationManagerIfNeeded()
562
+
563
+ let id = UUID()
564
+ var request = HeadingRequest(
565
+ id: id,
566
+ success: success,
567
+ error: { headingError in
568
+ error?(headingError)
569
+ },
570
+ timer: nil
571
+ )
572
+
573
+ let timer = DispatchSource.makeTimerSource(queue: .main)
574
+ timer.schedule(deadline: .now() + DEFAULT_HEADING_TIMEOUT_MS / 1000.0)
575
+ timer.setEventHandler { [weak self] in
576
+ self?.handleHeadingTimeout(requestId: id)
577
+ }
578
+ timer.resume()
579
+ request.timer = timer
580
+
581
+ pendingHeadingRequests[id] = request
582
+ updateHeadingConfiguration()
583
+ startHeadingMonitoring()
584
+ }
585
+
586
+ func watchHeading(
587
+ success: @escaping (Heading) -> Void,
588
+ error: ((LocationError) -> Void)?,
589
+ options: HeadingOptions?
590
+ ) throws -> String {
591
+ let token = UUID().uuidString
592
+ let parsedOptions = ParsedHeadingOptions.parse(from: options)
593
+
594
+ if !parsedOptions.headingFilter.isFinite || parsedOptions.headingFilter < 0 {
595
+ error?(createLocationError(
596
+ code: INTERNAL_ERROR,
597
+ message: "headingFilter must be a finite number greater than or equal to 0."
598
+ ))
599
+ return token
600
+ }
601
+
602
+ guard validateHeadingAvailability(error: error) else {
603
+ return token
604
+ }
605
+
606
+ let subscription = HeadingSubscription(
607
+ token: token,
608
+ success: success,
609
+ error: error,
610
+ options: parsedOptions,
611
+ lastDeliveredHeading: nil
612
+ )
613
+
614
+ headingSubscriptions[token] = subscription
615
+
616
+ initializeLocationManagerIfNeeded()
617
+ updateHeadingConfiguration()
618
+ startHeadingMonitoring()
619
+
620
+ return token
248
621
  }
249
622
 
250
623
  // MARK: - Watch Position (Callback-based with tokens)
@@ -275,19 +648,37 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
275
648
 
276
649
  func unwatch(token: String) {
277
650
  watchSubscriptions.removeValue(forKey: token)
651
+ headingSubscriptions.removeValue(forKey: token)
278
652
 
279
653
  // Stop monitoring if no more subscriptions or pending requests
280
654
  if watchSubscriptions.isEmpty && pendingPositionRequests.isEmpty {
281
655
  stopMonitoring()
656
+ } else {
657
+ updateLocationManagerConfiguration()
658
+ }
659
+
660
+ if headingSubscriptions.isEmpty && pendingHeadingRequests.isEmpty {
661
+ stopHeadingMonitoring()
662
+ } else {
663
+ updateHeadingConfiguration()
282
664
  }
283
665
  }
284
666
 
285
667
  func stopObserving() {
286
668
  watchSubscriptions.removeAll()
669
+ headingSubscriptions.removeAll()
287
670
 
288
671
  // Stop monitoring if no pending requests
289
672
  if pendingPositionRequests.isEmpty {
290
673
  stopMonitoring()
674
+ } else {
675
+ updateLocationManagerConfiguration()
676
+ }
677
+
678
+ if pendingHeadingRequests.isEmpty {
679
+ stopHeadingMonitoring()
680
+ } else {
681
+ updateHeadingConfiguration()
291
682
  }
292
683
  }
293
684
 
@@ -299,7 +690,7 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
299
690
 
300
691
  // Resolve pending permission requests
301
692
  for resolver in pendingPermissionResolvers {
302
- resolver(.success(mappedStatus))
693
+ resolver(mappedStatus)
303
694
  }
304
695
  pendingPermissionResolvers.removeAll()
305
696
 
@@ -320,7 +711,7 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
320
711
  // 1. Resolve all pending getCurrentPosition requests
321
712
  for (id, request) in pendingPositionRequests {
322
713
  request.timer?.cancel()
323
- request.resolver(.success(position))
714
+ request.success(position)
324
715
  }
325
716
  pendingPositionRequests.removeAll()
326
717
 
@@ -332,12 +723,13 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
332
723
  // 3. Stop monitoring if no more subscriptions or pending requests
333
724
  if watchSubscriptions.isEmpty && pendingPositionRequests.isEmpty {
334
725
  stopMonitoring()
726
+ } else {
727
+ updateLocationManagerConfiguration()
335
728
  }
336
729
  }
337
730
 
338
731
  fileprivate func handleLocationError(_ error: Error) {
339
732
  let locationError: LocationError
340
- let errorWrapper: GeolocationErrorWrapper
341
733
 
342
734
  if let clError = error as? CLError {
343
735
  switch clError.code {
@@ -346,10 +738,6 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
346
738
  code: PERMISSION_DENIED,
347
739
  message: "User denied access to location services."
348
740
  )
349
- errorWrapper = GeolocationErrorWrapper(
350
- code: PERMISSION_DENIED,
351
- message: "User denied access to location services."
352
- )
353
741
  case .locationUnknown:
354
742
  // Temporarily unavailable, keep trying
355
743
  return
@@ -358,26 +746,18 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
358
746
  code: POSITION_UNAVAILABLE,
359
747
  message: "Unable to retrieve location: \(error.localizedDescription)"
360
748
  )
361
- errorWrapper = GeolocationErrorWrapper(
362
- code: POSITION_UNAVAILABLE,
363
- message: "Unable to retrieve location: \(error.localizedDescription)"
364
- )
365
749
  }
366
750
  } else {
367
751
  locationError = createLocationError(
368
752
  code: POSITION_UNAVAILABLE,
369
753
  message: "Unable to retrieve location: \(error.localizedDescription)"
370
754
  )
371
- errorWrapper = GeolocationErrorWrapper(
372
- code: POSITION_UNAVAILABLE,
373
- message: "Unable to retrieve location: \(error.localizedDescription)"
374
- )
375
755
  }
376
756
 
377
757
  // 1. Reject all pending getCurrentPosition requests
378
758
  for (_, request) in pendingPositionRequests {
379
759
  request.timer?.cancel()
380
- request.resolver(.failure(errorWrapper))
760
+ request.error(locationError)
381
761
  }
382
762
  pendingPositionRequests.removeAll()
383
763
 
@@ -386,9 +766,45 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
386
766
  subscription.error?(locationError)
387
767
  }
388
768
 
769
+ notifyHeadingConsumersOfLocationError(locationError)
389
770
  stopMonitoring()
390
771
  }
391
772
 
773
+ fileprivate func handleHeadingUpdate(_ clHeading: CLHeading) {
774
+ let heading = headingToResponse(clHeading)
775
+
776
+ for (id, request) in Array(pendingHeadingRequests) {
777
+ request.timer?.cancel()
778
+ request.success(heading)
779
+ pendingHeadingRequests.removeValue(forKey: id)
780
+ }
781
+
782
+ for (token, subscription) in Array(headingSubscriptions) {
783
+ let shouldDeliver: Bool
784
+ if let lastDeliveredHeading = subscription.lastDeliveredHeading {
785
+ shouldDeliver = angularDistance(
786
+ lastDeliveredHeading,
787
+ heading.magneticHeading
788
+ ) >= subscription.options.headingFilter
789
+ } else {
790
+ shouldDeliver = true
791
+ }
792
+
793
+ if shouldDeliver {
794
+ var nextSubscription = subscription
795
+ nextSubscription.lastDeliveredHeading = heading.magneticHeading
796
+ headingSubscriptions[token] = nextSubscription
797
+ nextSubscription.success(heading)
798
+ }
799
+ }
800
+
801
+ if pendingHeadingRequests.isEmpty && headingSubscriptions.isEmpty {
802
+ stopHeadingMonitoring()
803
+ } else {
804
+ updateHeadingConfiguration()
805
+ }
806
+ }
807
+
392
808
  // MARK: - Helper Functions
393
809
 
394
810
  private func initializeLocationManagerIfNeeded() {
@@ -407,28 +823,190 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
407
823
  }
408
824
  }
409
825
 
826
+ private func validateHeadingAvailability(
827
+ error: ((LocationError) -> Void)?
828
+ ) -> Bool {
829
+ let status = CLLocationManager.authorizationStatus()
830
+ if status == .denied || status == .restricted {
831
+ let message = status == .restricted
832
+ ? "This application is not authorized to use location services"
833
+ : "User denied access to location services."
834
+ error?(createLocationError(
835
+ code: PERMISSION_DENIED,
836
+ message: message
837
+ ))
838
+ return false
839
+ }
840
+
841
+ if !CLLocationManager.locationServicesEnabled() {
842
+ error?(createLocationError(
843
+ code: SETTINGS_NOT_SATISFIED,
844
+ message: "Location services disabled."
845
+ ))
846
+ return false
847
+ }
848
+
849
+ if !CLLocationManager.headingAvailable() {
850
+ error?(createLocationError(
851
+ code: POSITION_UNAVAILABLE,
852
+ message: "Heading is not available on this device."
853
+ ))
854
+ return false
855
+ }
856
+
857
+ return true
858
+ }
859
+
860
+ private func updateHeadingConfiguration() {
861
+ guard let manager = locationManager else { return }
862
+
863
+ var smallestHeadingFilter: CLLocationDegrees?
864
+ for (_, subscription) in headingSubscriptions {
865
+ smallestHeadingFilter = mergeHeadingFilter(
866
+ smallestHeadingFilter,
867
+ subscription.options.headingFilter
868
+ )
869
+ }
870
+
871
+ manager.headingFilter = smallestHeadingFilter ?? kCLHeadingFilterNone
872
+ }
873
+
874
+ private func startHeadingMonitoring() {
875
+ locationManager?.startUpdatingHeading()
876
+ }
877
+
878
+ private func stopHeadingMonitoring() {
879
+ locationManager?.stopUpdatingHeading()
880
+ }
881
+
882
+ private func mergeHeadingFilter(
883
+ _ current: CLLocationDegrees?,
884
+ _ next: CLLocationDegrees
885
+ ) -> CLLocationDegrees {
886
+ guard let current else {
887
+ return next
888
+ }
889
+
890
+ if current == kCLHeadingFilterNone || next == kCLHeadingFilterNone {
891
+ return kCLHeadingFilterNone
892
+ }
893
+
894
+ return min(current, next)
895
+ }
896
+
897
+ private func handleHeadingTimeout(requestId: UUID) {
898
+ guard let request = pendingHeadingRequests.removeValue(forKey: requestId) else {
899
+ return
900
+ }
901
+
902
+ request.timer?.cancel()
903
+ let timeoutSeconds = DEFAULT_HEADING_TIMEOUT_MS / 1000.0
904
+ let message = String(format: "Unable to fetch heading within %.1fs.", timeoutSeconds)
905
+ request.error(createLocationError(code: TIMEOUT, message: message))
906
+
907
+ if pendingHeadingRequests.isEmpty && headingSubscriptions.isEmpty {
908
+ stopHeadingMonitoring()
909
+ } else {
910
+ updateHeadingConfiguration()
911
+ }
912
+ }
913
+
914
+ private func notifyHeadingConsumersOfLocationError(_ locationError: LocationError) {
915
+ guard !pendingHeadingRequests.isEmpty || !headingSubscriptions.isEmpty else {
916
+ return
917
+ }
918
+
919
+ for (_, request) in pendingHeadingRequests {
920
+ request.timer?.cancel()
921
+ request.error(locationError)
922
+ }
923
+ pendingHeadingRequests.removeAll()
924
+
925
+ for (_, subscription) in headingSubscriptions {
926
+ subscription.error?(locationError)
927
+ }
928
+ headingSubscriptions.removeAll()
929
+
930
+ stopHeadingMonitoring()
931
+ }
932
+
933
+ private func headingToResponse(_ clHeading: CLHeading) -> Heading {
934
+ let trueHeading = clHeading.trueHeading >= 0
935
+ ? clHeading.trueHeading
936
+ : nil
937
+ let accuracy = clHeading.headingAccuracy >= 0
938
+ ? clHeading.headingAccuracy
939
+ : nil
940
+
941
+ return Heading(
942
+ magneticHeading: normalizeHeading(clHeading.magneticHeading),
943
+ trueHeading: trueHeading.map(normalizeHeading),
944
+ accuracy: accuracy,
945
+ timestamp: clHeading.timestamp.timeIntervalSince1970 * 1000
946
+ )
947
+ }
948
+
949
+ private func angularDistance(_ first: Double, _ second: Double) -> Double {
950
+ let distance = abs(first - second).truncatingRemainder(dividingBy: 360)
951
+ return distance > 180 ? 360 - distance : distance
952
+ }
953
+
954
+ private func normalizeHeading(_ value: Double) -> Double {
955
+ let normalized = value.truncatingRemainder(dividingBy: 360)
956
+ return normalized < 0 ? normalized + 360 : normalized
957
+ }
958
+
410
959
  private func updateLocationManagerConfiguration() {
411
960
  guard let manager = locationManager else { return }
412
961
 
413
962
  // Merge configurations from all pending requests and watches
414
- var bestAccuracy = kCLLocationAccuracyHundredMeters
415
- var smallestDistanceFilter = kCLDistanceFilterNone
963
+ var bestAccuracy: CLLocationAccuracy?
964
+ var smallestDistanceFilter: CLLocationDistance?
965
+ var activityType: CLActivityType?
966
+ var pausesLocationUpdatesAutomatically: Bool?
967
+ var showsBackgroundLocationIndicator = false
416
968
  var shouldUseSignificantChanges = false
417
969
 
418
970
  for (_, request) in pendingPositionRequests {
419
- bestAccuracy = min(bestAccuracy, request.options.accuracy)
420
- smallestDistanceFilter = min(smallestDistanceFilter, request.options.distanceFilter)
971
+ bestAccuracy = mergeAccuracy(bestAccuracy, request.options.accuracy)
972
+ smallestDistanceFilter = mergeDistanceFilter(
973
+ smallestDistanceFilter,
974
+ request.options.distanceFilter
975
+ )
976
+ activityType = mergeActivityType(activityType, request.options.activityType)
977
+ pausesLocationUpdatesAutomatically = mergePausesLocationUpdatesAutomatically(
978
+ pausesLocationUpdatesAutomatically,
979
+ request.options.pausesLocationUpdatesAutomatically
980
+ )
981
+ showsBackgroundLocationIndicator = showsBackgroundLocationIndicator ||
982
+ (request.options.showsBackgroundLocationIndicator ?? false)
421
983
  shouldUseSignificantChanges = shouldUseSignificantChanges || request.options.useSignificantChanges
422
984
  }
423
985
 
424
986
  for (_, subscription) in watchSubscriptions {
425
- bestAccuracy = min(bestAccuracy, subscription.options.accuracy)
426
- smallestDistanceFilter = min(smallestDistanceFilter, subscription.options.distanceFilter)
987
+ bestAccuracy = mergeAccuracy(bestAccuracy, subscription.options.accuracy)
988
+ smallestDistanceFilter = mergeDistanceFilter(
989
+ smallestDistanceFilter,
990
+ subscription.options.distanceFilter
991
+ )
992
+ activityType = mergeActivityType(activityType, subscription.options.activityType)
993
+ pausesLocationUpdatesAutomatically = mergePausesLocationUpdatesAutomatically(
994
+ pausesLocationUpdatesAutomatically,
995
+ subscription.options.pausesLocationUpdatesAutomatically
996
+ )
997
+ showsBackgroundLocationIndicator = showsBackgroundLocationIndicator ||
998
+ (subscription.options.showsBackgroundLocationIndicator ?? false)
427
999
  shouldUseSignificantChanges = shouldUseSignificantChanges || subscription.options.useSignificantChanges
428
1000
  }
429
1001
 
430
- manager.desiredAccuracy = bestAccuracy
431
- manager.distanceFilter = smallestDistanceFilter
1002
+ manager.desiredAccuracy = bestAccuracy ?? kCLLocationAccuracyHundredMeters
1003
+ manager.distanceFilter = smallestDistanceFilter ?? kCLDistanceFilterNone
1004
+ manager.activityType = activityType ?? .other
1005
+ manager.pausesLocationUpdatesAutomatically = pausesLocationUpdatesAutomatically ?? true
1006
+
1007
+ if #available(iOS 11.0, *) {
1008
+ manager.showsBackgroundLocationIndicator = showsBackgroundLocationIndicator
1009
+ }
432
1010
 
433
1011
  // Update significant changes mode if changed
434
1012
  if shouldUseSignificantChanges != usingSignificantChanges {
@@ -438,6 +1016,79 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
438
1016
  }
439
1017
  }
440
1018
 
1019
+ private func mergeAccuracy(
1020
+ _ current: CLLocationAccuracy?,
1021
+ _ next: CLLocationAccuracy
1022
+ ) -> CLLocationAccuracy {
1023
+ guard let current else {
1024
+ return next
1025
+ }
1026
+
1027
+ return min(current, next)
1028
+ }
1029
+
1030
+ private func mergeActivityType(
1031
+ _ current: CLActivityType?,
1032
+ _ next: CLActivityType?
1033
+ ) -> CLActivityType? {
1034
+ guard let next else {
1035
+ return current
1036
+ }
1037
+
1038
+ guard let current else {
1039
+ return next
1040
+ }
1041
+
1042
+ return activityTypeRank(next) > activityTypeRank(current) ? next : current
1043
+ }
1044
+
1045
+ private func activityTypeRank(_ activityType: CLActivityType) -> Int {
1046
+ switch activityType {
1047
+ case .other:
1048
+ return 0
1049
+ case .otherNavigation:
1050
+ return 1
1051
+ case .automotiveNavigation:
1052
+ return 2
1053
+ case .fitness:
1054
+ return 3
1055
+ case .airborne:
1056
+ return 4
1057
+ @unknown default:
1058
+ return 0
1059
+ }
1060
+ }
1061
+
1062
+ private func mergePausesLocationUpdatesAutomatically(
1063
+ _ current: Bool?,
1064
+ _ next: Bool?
1065
+ ) -> Bool? {
1066
+ guard let next else {
1067
+ return current
1068
+ }
1069
+
1070
+ guard let current else {
1071
+ return next
1072
+ }
1073
+
1074
+ return current && next
1075
+ }
1076
+
1077
+ private func mergeDistanceFilter(
1078
+ _ current: CLLocationDistance?,
1079
+ _ next: CLLocationDistance
1080
+ ) -> CLLocationDistance {
1081
+ guard let current else {
1082
+ return next
1083
+ }
1084
+
1085
+ if current == kCLDistanceFilterNone || next == kCLDistanceFilterNone {
1086
+ return kCLDistanceFilterNone
1087
+ }
1088
+
1089
+ return min(current, next)
1090
+ }
1091
+
441
1092
  private func startMonitoring() {
442
1093
  if usingSignificantChanges {
443
1094
  locationManager?.startMonitoringSignificantLocationChanges()
@@ -465,6 +1116,15 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
465
1116
  return age < options.maximumAge
466
1117
  }
467
1118
 
1119
+ private func getBestCachedLocation(options: ParsedOptions) -> CLLocation? {
1120
+ initializeLocationManagerIfNeeded()
1121
+
1122
+ return [lastLocation, locationManager?.location]
1123
+ .compactMap { $0 }
1124
+ .filter { isCachedLocationValid($0, options: options) }
1125
+ .max { $0.timestamp < $1.timestamp }
1126
+ }
1127
+
468
1128
  private func handlePositionTimeout(requestId: UUID) {
469
1129
  guard let request = pendingPositionRequests.removeValue(forKey: requestId) else {
470
1130
  return
@@ -474,13 +1134,15 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
474
1134
 
475
1135
  let timeoutSeconds = request.options.timeout / 1000.0
476
1136
  let message = String(format: "Unable to fetch location within %.1fs.", timeoutSeconds)
477
- let error = GeolocationErrorWrapper(code: TIMEOUT, message: message)
1137
+ let error = createLocationError(code: TIMEOUT, message: message)
478
1138
 
479
- request.resolver(.failure(error))
1139
+ request.error(error)
480
1140
 
481
1141
  // Stop monitoring if no more watches or pending requests
482
1142
  if watchSubscriptions.isEmpty && pendingPositionRequests.isEmpty {
483
1143
  stopMonitoring()
1144
+ } else {
1145
+ updateLocationManagerConfiguration()
484
1146
  }
485
1147
  }
486
1148
 
@@ -553,25 +1215,161 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
553
1215
  }
554
1216
  }
555
1217
 
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
1218
+ private func getCurrentAccuracyAuthorization() -> AccuracyAuthorization {
1219
+ guard #available(iOS 14.0, *) else {
1220
+ return .unknown
1221
+ }
561
1222
 
1223
+ let manager = locationManager ?? CLLocationManager()
1224
+ switch manager.accuracyAuthorization {
1225
+ case .fullAccuracy:
1226
+ return .full
1227
+ case .reducedAccuracy:
1228
+ return .reduced
1229
+ @unknown default:
1230
+ return .unknown
1231
+ }
1232
+ }
1233
+
1234
+ private func getCurrentAccuracyAuthorizationOnMain() -> AccuracyAuthorization {
1235
+ if Thread.isMainThread {
1236
+ return getCurrentAccuracyAuthorization()
1237
+ }
1238
+
1239
+ return DispatchQueue.main.sync {
1240
+ getCurrentAccuracyAuthorization()
1241
+ }
1242
+ }
1243
+
1244
+ private func locationToPosition(_ location: CLLocation) -> GeolocationResponse {
562
1245
  let coords = GeolocationCoordinates(
563
1246
  latitude: location.coordinate.latitude,
564
1247
  longitude: location.coordinate.longitude,
565
- altitude: .second(altitude),
1248
+ altitude: location.nitroGeolocationAltitude,
566
1249
  accuracy: location.horizontalAccuracy,
567
- altitudeAccuracy: .second(altitudeAccuracy),
568
- heading: .second(heading),
569
- speed: .second(speed)
1250
+ altitudeAccuracy: location.nitroGeolocationAltitudeAccuracy,
1251
+ heading: location.nitroGeolocationHeading,
1252
+ speed: location.nitroGeolocationSpeed
570
1253
  )
571
1254
 
572
1255
  return GeolocationResponse(
573
1256
  coords: coords,
574
- timestamp: location.timestamp.timeIntervalSince1970 * 1000
1257
+ timestamp: location.timestamp.timeIntervalSince1970 * 1000,
1258
+ mocked: location.nitroGeolocationMocked,
1259
+ provider: location.nitroGeolocationProvider
1260
+ )
1261
+ }
1262
+
1263
+ private func placemarkToGeocodedLocation(_ placemark: CLPlacemark) -> GeocodedLocation? {
1264
+ guard let location = placemark.location else {
1265
+ return nil
1266
+ }
1267
+
1268
+ let accuracy = location.horizontalAccuracy >= 0
1269
+ ? location.horizontalAccuracy
1270
+ : nil
1271
+
1272
+ return GeocodedLocation(
1273
+ latitude: location.coordinate.latitude,
1274
+ longitude: location.coordinate.longitude,
1275
+ accuracy: accuracy
1276
+ )
1277
+ }
1278
+
1279
+ private func placemarkToReverseGeocodedAddress(_ placemark: CLPlacemark) -> ReverseGeocodedAddress {
1280
+ return ReverseGeocodedAddress(
1281
+ country: placemark.country.nonEmptyTrimmed,
1282
+ region: placemark.administrativeArea.nonEmptyTrimmed,
1283
+ city: placemark.locality.nonEmptyTrimmed,
1284
+ district: placemark.subLocality.nonEmptyTrimmed,
1285
+ street: formatStreet(placemark),
1286
+ postalCode: placemark.postalCode.nonEmptyTrimmed,
1287
+ formattedAddress: formatAddress(placemark)
1288
+ )
1289
+ }
1290
+
1291
+ private func formatStreet(_ placemark: CLPlacemark) -> String? {
1292
+ return [
1293
+ placemark.subThoroughfare.nonEmptyTrimmed,
1294
+ placemark.thoroughfare.nonEmptyTrimmed
1295
+ ]
1296
+ .compactMap { $0 }
1297
+ .joined(separator: " ")
1298
+ .nonEmptyTrimmed
1299
+ }
1300
+
1301
+ private func formatAddress(_ placemark: CLPlacemark) -> String? {
1302
+ var parts: [String] = []
1303
+
1304
+ appendDistinct(placemark.name.nonEmptyTrimmed, to: &parts)
1305
+ appendDistinct(formatStreet(placemark), to: &parts)
1306
+ appendDistinct(placemark.subLocality.nonEmptyTrimmed, to: &parts)
1307
+ appendDistinct(placemark.locality.nonEmptyTrimmed, to: &parts)
1308
+ appendDistinct(placemark.administrativeArea.nonEmptyTrimmed, to: &parts)
1309
+ appendDistinct(placemark.postalCode.nonEmptyTrimmed, to: &parts)
1310
+ appendDistinct(placemark.country.nonEmptyTrimmed, to: &parts)
1311
+
1312
+ return parts.joined(separator: ", ").nonEmptyTrimmed
1313
+ }
1314
+
1315
+ private func appendDistinct(_ value: String?, to parts: inout [String]) {
1316
+ guard let value, !parts.contains(value) else {
1317
+ return
1318
+ }
1319
+
1320
+ parts.append(value)
1321
+ }
1322
+
1323
+ private func validateGeocodingCoordinates(_ coords: GeocodingCoordinates) -> LocationError? {
1324
+ if !coords.latitude.isFinite || coords.latitude < -90 || coords.latitude > 90 {
1325
+ return createLocationError(
1326
+ code: INTERNAL_ERROR,
1327
+ message: "latitude must be a finite number between -90 and 90."
1328
+ )
1329
+ }
1330
+
1331
+ if !coords.longitude.isFinite || coords.longitude < -180 || coords.longitude > 180 {
1332
+ return createLocationError(
1333
+ code: INTERNAL_ERROR,
1334
+ message: "longitude must be a finite number between -180 and 180."
1335
+ )
1336
+ }
1337
+
1338
+ return nil
1339
+ }
1340
+
1341
+ private func isNoGeocoderResult(_ error: Error) -> Bool {
1342
+ guard let clError = error as? CLError else {
1343
+ return false
1344
+ }
1345
+
1346
+ return clError.code == .geocodeFoundNoResult
1347
+ }
1348
+
1349
+ private func createGeocoderError(_ error: Error, messagePrefix: String) -> LocationError {
1350
+ if let clError = error as? CLError {
1351
+ switch clError.code {
1352
+ case .denied:
1353
+ return createLocationError(
1354
+ code: PERMISSION_DENIED,
1355
+ message: "\(messagePrefix): geocoder access denied."
1356
+ )
1357
+ case .network:
1358
+ return createLocationError(
1359
+ code: POSITION_UNAVAILABLE,
1360
+ message: "\(messagePrefix): network unavailable."
1361
+ )
1362
+ default:
1363
+ return createLocationError(
1364
+ code: POSITION_UNAVAILABLE,
1365
+ message: "\(messagePrefix): \(error.localizedDescription)"
1366
+ )
1367
+ }
1368
+ }
1369
+
1370
+ return createLocationError(
1371
+ code: POSITION_UNAVAILABLE,
1372
+ message: "\(messagePrefix): \(error.localizedDescription)"
575
1373
  )
576
1374
  }
577
1375
 
@@ -581,4 +1379,45 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
581
1379
  message: message
582
1380
  )
583
1381
  }
1382
+
1383
+ private func createLocationProviderStatus() -> LocationProviderStatus {
1384
+ return LocationProviderStatus(
1385
+ locationServicesEnabled: CLLocationManager.locationServicesEnabled(),
1386
+ backgroundModeEnabled: isLocationBackgroundModeEnabled(),
1387
+ gpsAvailable: nil,
1388
+ networkAvailable: nil,
1389
+ passiveAvailable: nil,
1390
+ googleLocationAccuracyEnabled: nil
1391
+ )
1392
+ }
1393
+
1394
+ private func isLocationBackgroundModeEnabled() -> Bool {
1395
+ guard let backgroundModes = Bundle.main.object(
1396
+ forInfoDictionaryKey: "UIBackgroundModes"
1397
+ ) as? [String] else {
1398
+ return false
1399
+ }
1400
+
1401
+ return backgroundModes.contains("location")
1402
+ }
1403
+ }
1404
+
1405
+ private extension Optional where Wrapped == String {
1406
+ var nonEmptyTrimmed: String? {
1407
+ guard let trimmed = self?.trimmingCharacters(in: .whitespacesAndNewlines) else {
1408
+ return nil
1409
+ }
1410
+
1411
+ return trimmed.isEmpty ? nil : trimmed
1412
+ }
1413
+ }
1414
+
1415
+ private extension String {
1416
+ var nonEmptyTrimmed: String? {
1417
+ return trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty
1418
+ }
1419
+
1420
+ var nilIfEmpty: String? {
1421
+ return isEmpty ? nil : self
1422
+ }
584
1423
  }