react-native-nitro-location-tracking 0.1.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 (101) hide show
  1. package/LICENSE +20 -0
  2. package/NitroLocationTracking.podspec +29 -0
  3. package/README.md +39 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +122 -0
  6. package/android/src/main/AndroidManifest.xml +18 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  8. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/ConnectionManager.kt +137 -0
  9. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/LocationEngine.kt +93 -0
  10. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/LocationForegroundService.kt +65 -0
  11. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NativeDBWriter.kt +80 -0
  12. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NitroLocationTracking.kt +180 -0
  13. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NitroLocationTrackingPackage.kt +22 -0
  14. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NotificationService.kt +75 -0
  15. package/ios/ConnectionManager.swift +144 -0
  16. package/ios/LocationEngine.swift +146 -0
  17. package/ios/NativeDBWriter.swift +121 -0
  18. package/ios/NitroLocationTracking.swift +127 -0
  19. package/ios/NotificationService.swift +31 -0
  20. package/lib/module/LocationSmoother.js +33 -0
  21. package/lib/module/LocationSmoother.js.map +1 -0
  22. package/lib/module/NitroLocationTracking.nitro.js +4 -0
  23. package/lib/module/NitroLocationTracking.nitro.js.map +1 -0
  24. package/lib/module/bearing.js +19 -0
  25. package/lib/module/bearing.js.map +1 -0
  26. package/lib/module/db.js +234 -0
  27. package/lib/module/db.js.map +1 -0
  28. package/lib/module/index.js +68 -0
  29. package/lib/module/index.js.map +1 -0
  30. package/lib/module/package.json +1 -0
  31. package/lib/typescript/package.json +1 -0
  32. package/lib/typescript/src/LocationSmoother.d.ts +19 -0
  33. package/lib/typescript/src/LocationSmoother.d.ts.map +1 -0
  34. package/lib/typescript/src/NitroLocationTracking.nitro.d.ts +59 -0
  35. package/lib/typescript/src/NitroLocationTracking.nitro.d.ts.map +1 -0
  36. package/lib/typescript/src/bearing.d.ts +9 -0
  37. package/lib/typescript/src/bearing.d.ts.map +1 -0
  38. package/lib/typescript/src/db.d.ts +1 -0
  39. package/lib/typescript/src/db.d.ts.map +1 -0
  40. package/lib/typescript/src/index.d.ts +21 -0
  41. package/lib/typescript/src/index.d.ts.map +1 -0
  42. package/nitro.json +17 -0
  43. package/nitrogen/generated/android/c++/JAccuracyLevel.hpp +61 -0
  44. package/nitrogen/generated/android/c++/JConnectionConfig.hpp +81 -0
  45. package/nitrogen/generated/android/c++/JConnectionState.hpp +61 -0
  46. package/nitrogen/generated/android/c++/JFunc_void_ConnectionState.hpp +77 -0
  47. package/nitrogen/generated/android/c++/JFunc_void_LocationData.hpp +77 -0
  48. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
  49. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  50. package/nitrogen/generated/android/c++/JHybridNitroLocationTrackingSpec.cpp +179 -0
  51. package/nitrogen/generated/android/c++/JHybridNitroLocationTrackingSpec.hpp +83 -0
  52. package/nitrogen/generated/android/c++/JLocationConfig.hpp +91 -0
  53. package/nitrogen/generated/android/c++/JLocationData.hpp +81 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/AccuracyLevel.kt +24 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/ConnectionConfig.kt +56 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/ConnectionState.kt +24 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_ConnectionState.kt +80 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_LocationData.kt +80 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_bool.kt +80 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/Func_void_std__string.kt +80 -0
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/HybridNitroLocationTrackingSpec.kt +146 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/LocationConfig.kt +62 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/LocationData.kt +56 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/nitrolocationtrackingOnLoad.kt +35 -0
  65. package/nitrogen/generated/android/nitrolocationtracking+autolinking.cmake +81 -0
  66. package/nitrogen/generated/android/nitrolocationtracking+autolinking.gradle +27 -0
  67. package/nitrogen/generated/android/nitrolocationtrackingOnLoad.cpp +52 -0
  68. package/nitrogen/generated/android/nitrolocationtrackingOnLoad.hpp +25 -0
  69. package/nitrogen/generated/ios/NitroLocationTracking+autolinking.rb +60 -0
  70. package/nitrogen/generated/ios/NitroLocationTracking-Swift-Cxx-Bridge.cpp +73 -0
  71. package/nitrogen/generated/ios/NitroLocationTracking-Swift-Cxx-Bridge.hpp +231 -0
  72. package/nitrogen/generated/ios/NitroLocationTracking-Swift-Cxx-Umbrella.hpp +61 -0
  73. package/nitrogen/generated/ios/NitroLocationTrackingAutolinking.mm +33 -0
  74. package/nitrogen/generated/ios/NitroLocationTrackingAutolinking.swift +26 -0
  75. package/nitrogen/generated/ios/c++/HybridNitroLocationTrackingSpecSwift.cpp +11 -0
  76. package/nitrogen/generated/ios/c++/HybridNitroLocationTrackingSpecSwift.hpp +206 -0
  77. package/nitrogen/generated/ios/swift/AccuracyLevel.swift +44 -0
  78. package/nitrogen/generated/ios/swift/ConnectionConfig.swift +59 -0
  79. package/nitrogen/generated/ios/swift/ConnectionState.swift +44 -0
  80. package/nitrogen/generated/ios/swift/Func_void_ConnectionState.swift +46 -0
  81. package/nitrogen/generated/ios/swift/Func_void_LocationData.swift +46 -0
  82. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  83. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  84. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  85. package/nitrogen/generated/ios/swift/HybridNitroLocationTrackingSpec.swift +72 -0
  86. package/nitrogen/generated/ios/swift/HybridNitroLocationTrackingSpec_cxx.swift +362 -0
  87. package/nitrogen/generated/ios/swift/LocationConfig.swift +69 -0
  88. package/nitrogen/generated/ios/swift/LocationData.swift +59 -0
  89. package/nitrogen/generated/shared/c++/AccuracyLevel.hpp +80 -0
  90. package/nitrogen/generated/shared/c++/ConnectionConfig.hpp +107 -0
  91. package/nitrogen/generated/shared/c++/ConnectionState.hpp +80 -0
  92. package/nitrogen/generated/shared/c++/HybridNitroLocationTrackingSpec.cpp +38 -0
  93. package/nitrogen/generated/shared/c++/HybridNitroLocationTrackingSpec.hpp +92 -0
  94. package/nitrogen/generated/shared/c++/LocationConfig.hpp +117 -0
  95. package/nitrogen/generated/shared/c++/LocationData.hpp +107 -0
  96. package/package.json +174 -0
  97. package/src/LocationSmoother.ts +46 -0
  98. package/src/NitroLocationTracking.nitro.ts +79 -0
  99. package/src/bearing.ts +22 -0
  100. package/src/db.ts +232 -0
  101. package/src/index.tsx +92 -0
@@ -0,0 +1,180 @@
1
+ package com.margelo.nitro.nitrolocationtracking
2
+
3
+ import android.util.Log
4
+ import com.facebook.proguard.annotations.DoNotStrip
5
+ import com.margelo.nitro.NitroModules
6
+ import com.margelo.nitro.core.Promise
7
+ import kotlin.coroutines.resume
8
+ import kotlin.coroutines.resumeWithException
9
+ import kotlin.coroutines.suspendCoroutine
10
+
11
+ @DoNotStrip
12
+ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
13
+
14
+ companion object {
15
+ private const val TAG = "NitroLocationTracking"
16
+ }
17
+
18
+ private var locationEngine: LocationEngine? = null
19
+ private var connectionManager = ConnectionManager()
20
+ private var dbWriter: NativeDBWriter? = null
21
+ private var notificationService: NotificationService? = null
22
+
23
+ private var locationCallback: ((LocationData) -> Unit)? = null
24
+ private var motionCallback: ((Boolean) -> Unit)? = null
25
+ private var connectionStateCallback: ((ConnectionState) -> Unit)? = null
26
+ private var messageCallback: ((String) -> Unit)? = null
27
+
28
+ private var locationConfig: LocationConfig? = null
29
+
30
+ private fun ensureInitialized(): Boolean {
31
+ if (locationEngine != null) return true
32
+ val context = NitroModules.applicationContext
33
+ if (context == null) {
34
+ Log.e(TAG, "NitroModules.applicationContext is null — cannot initialize")
35
+ return false
36
+ }
37
+ locationEngine = LocationEngine(context)
38
+ dbWriter = NativeDBWriter(context)
39
+ notificationService = NotificationService(context)
40
+ locationEngine?.dbWriter = dbWriter
41
+ connectionManager.dbWriter = dbWriter
42
+ Log.d(TAG, "Components initialized successfully")
43
+ return true
44
+ }
45
+
46
+ // === Location Engine ===
47
+
48
+ override fun configure(config: LocationConfig) {
49
+ locationConfig = config
50
+ ensureInitialized()
51
+ }
52
+
53
+ override fun startTracking() {
54
+ val config = locationConfig ?: run {
55
+ Log.w(TAG, "startTracking called but no config set — call configure() first")
56
+ return
57
+ }
58
+ if (!ensureInitialized()) {
59
+ Log.e(TAG, "startTracking failed — could not initialize components")
60
+ return
61
+ }
62
+ val engine = locationEngine ?: return
63
+ engine.onLocation = { data ->
64
+ locationCallback?.invoke(data)
65
+ }
66
+ engine.onMotionChange = { isMoving ->
67
+ motionCallback?.invoke(isMoving)
68
+ }
69
+ engine.start(config)
70
+
71
+ try {
72
+ notificationService?.startForegroundService(
73
+ config.foregroundNotificationTitle,
74
+ config.foregroundNotificationText
75
+ )
76
+ } catch (e: SecurityException) {
77
+ Log.w(TAG, "Could not start foreground service — missing runtime permissions: ${e.message}")
78
+ }
79
+ }
80
+
81
+ override fun stopTracking() {
82
+ locationEngine?.stop()
83
+ notificationService?.stopForegroundService()
84
+ }
85
+
86
+ override fun getCurrentLocation(): Promise<LocationData> {
87
+ return Promise.async {
88
+ suspendCoroutine { cont ->
89
+ if (!ensureInitialized()) {
90
+ cont.resumeWithException(Exception("Location engine not initialized — context unavailable"))
91
+ return@suspendCoroutine
92
+ }
93
+ val engine = locationEngine!!
94
+ engine.getCurrentLocation { data ->
95
+ if (data != null) {
96
+ cont.resume(data)
97
+ } else {
98
+ cont.resumeWithException(Exception("Failed to get location"))
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ override fun isTracking(): Boolean {
106
+ return locationEngine?.isTracking ?: false
107
+ }
108
+
109
+ override fun onLocation(callback: (location: LocationData) -> Unit) {
110
+ locationCallback = callback
111
+ }
112
+
113
+ override fun onMotionChange(callback: (isMoving: Boolean) -> Unit) {
114
+ motionCallback = callback
115
+ }
116
+
117
+ // === Connection Manager ===
118
+
119
+ override fun configureConnection(config: ConnectionConfig) {
120
+ connectionManager.configure(config)
121
+ }
122
+
123
+ override fun connectWebSocket() {
124
+ connectionManager.onStateChange = { state ->
125
+ connectionStateCallback?.invoke(state)
126
+ }
127
+ connectionManager.onMessage = { message ->
128
+ messageCallback?.invoke(message)
129
+ }
130
+ connectionManager.connect()
131
+ }
132
+
133
+ override fun disconnectWebSocket() {
134
+ connectionManager.disconnect()
135
+ }
136
+
137
+ override fun sendMessage(message: String) {
138
+ connectionManager.send(message)
139
+ }
140
+
141
+ override fun getConnectionState(): ConnectionState {
142
+ return connectionManager.getState()
143
+ }
144
+
145
+ override fun onConnectionStateChange(callback: (state: ConnectionState) -> Unit) {
146
+ connectionStateCallback = callback
147
+ }
148
+
149
+ override fun onMessage(callback: (message: String) -> Unit) {
150
+ messageCallback = callback
151
+ }
152
+
153
+ // === Sync Control ===
154
+
155
+ override fun forceSync(): Promise<Boolean> {
156
+ return Promise.async {
157
+ connectionManager.flushQueue()
158
+ }
159
+ }
160
+
161
+ // === Notifications ===
162
+
163
+ override fun showLocalNotification(title: String, body: String) {
164
+ ensureInitialized()
165
+ notificationService?.showLocalNotification(title, body)
166
+ }
167
+
168
+ override fun updateForegroundNotification(title: String, body: String) {
169
+ ensureInitialized()
170
+ notificationService?.updateForegroundNotification(title, body)
171
+ }
172
+
173
+ // === Lifecycle ===
174
+
175
+ override fun destroy() {
176
+ locationEngine?.stop()
177
+ connectionManager.disconnect()
178
+ notificationService?.stopForegroundService()
179
+ }
180
+ }
@@ -0,0 +1,22 @@
1
+ package com.margelo.nitro.nitrolocationtracking
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+
8
+ class NitroLocationTrackingPackage : BaseReactPackage() {
9
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
10
+ return null
11
+ }
12
+
13
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
14
+ return ReactModuleInfoProvider { HashMap() }
15
+ }
16
+
17
+ companion object {
18
+ init {
19
+ System.loadLibrary("nitrolocationtracking")
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,75 @@
1
+ package com.margelo.nitro.nitrolocationtracking
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.os.Build
8
+ import androidx.core.app.NotificationCompat
9
+ import androidx.core.app.NotificationManagerCompat
10
+
11
+ class NotificationService(private val context: Context) {
12
+
13
+ companion object {
14
+ const val LOCAL_CHANNEL_ID = "nitro_location_local"
15
+ private var notificationCounter = 0
16
+ }
17
+
18
+ init {
19
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
20
+ val channel = NotificationChannel(
21
+ LOCAL_CHANNEL_ID,
22
+ "Location Notifications",
23
+ NotificationManager.IMPORTANCE_HIGH
24
+ )
25
+ (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
26
+ .createNotificationChannel(channel)
27
+ }
28
+ }
29
+
30
+ fun showLocalNotification(title: String, body: String) {
31
+ val notification = NotificationCompat.Builder(context, LOCAL_CHANNEL_ID)
32
+ .setContentTitle(title)
33
+ .setContentText(body)
34
+ .setSmallIcon(android.R.drawable.ic_menu_mylocation)
35
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
36
+ .setAutoCancel(true)
37
+ .build()
38
+
39
+ try {
40
+ NotificationManagerCompat.from(context)
41
+ .notify(++notificationCounter, notification)
42
+ } catch (_: SecurityException) {
43
+ // Permission not granted
44
+ }
45
+ }
46
+
47
+ fun updateForegroundNotification(title: String, body: String) {
48
+ val intent = Intent(context, LocationForegroundService::class.java).apply {
49
+ action = LocationForegroundService.ACTION_UPDATE
50
+ putExtra("title", title)
51
+ putExtra("text", body)
52
+ }
53
+ context.startService(intent)
54
+ }
55
+
56
+ fun startForegroundService(title: String, text: String) {
57
+ val intent = Intent(context, LocationForegroundService::class.java).apply {
58
+ action = LocationForegroundService.ACTION_START
59
+ putExtra("title", title)
60
+ putExtra("text", text)
61
+ }
62
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
63
+ context.startForegroundService(intent)
64
+ } else {
65
+ context.startService(intent)
66
+ }
67
+ }
68
+
69
+ fun stopForegroundService() {
70
+ val intent = Intent(context, LocationForegroundService::class.java).apply {
71
+ action = LocationForegroundService.ACTION_STOP
72
+ }
73
+ context.startService(intent)
74
+ }
75
+ }
@@ -0,0 +1,144 @@
1
+ import Foundation
2
+
3
+ class ConnectionManager: NSObject, URLSessionWebSocketDelegate {
4
+ private var webSocket: URLSessionWebSocketTask?
5
+ private var session: URLSession?
6
+ private var config: ConnectionConfig?
7
+ private var reconnectAttempts = 0
8
+ private var connected = false
9
+ private var syncTimer: Timer?
10
+
11
+ var onStateChange: ((ConnectionState) -> Void)?
12
+ var onMessage: ((String) -> Void)?
13
+ var dbWriter: NativeDBWriter?
14
+
15
+ var isConnected: Bool { connected }
16
+
17
+ func configure(_ config: ConnectionConfig) {
18
+ self.config = config
19
+ session = URLSession(configuration: .default,
20
+ delegate: self, delegateQueue: OperationQueue())
21
+ }
22
+
23
+ func connect() {
24
+ guard let config = config,
25
+ let url = URL(string: config.wsUrl) else { return }
26
+ var request = URLRequest(url: url)
27
+ request.setValue("Bearer \(config.authToken)",
28
+ forHTTPHeaderField: "Authorization")
29
+ webSocket = session?.webSocketTask(with: request)
30
+ webSocket?.resume()
31
+ startListening()
32
+ startSyncTimer()
33
+ connected = true
34
+ reconnectAttempts = 0
35
+ onStateChange?(.connected)
36
+ }
37
+
38
+ func disconnect() {
39
+ syncTimer?.invalidate()
40
+ webSocket?.cancel(with: .normalClosure, reason: nil)
41
+ connected = false
42
+ onStateChange?(.disconnected)
43
+ }
44
+
45
+ func send(_ message: String) {
46
+ webSocket?.send(.string(message)) { [weak self] error in
47
+ if error != nil { self?.handleDisconnect() }
48
+ }
49
+ }
50
+
51
+ func getState() -> ConnectionState {
52
+ return connected ? .connected : .disconnected
53
+ }
54
+
55
+ private func startListening() {
56
+ webSocket?.receive { [weak self] result in
57
+ switch result {
58
+ case .success(let msg):
59
+ if case .string(let text) = msg {
60
+ self?.onMessage?(text)
61
+ }
62
+ self?.startListening()
63
+ case .failure(_):
64
+ self?.handleDisconnect()
65
+ }
66
+ }
67
+ }
68
+
69
+ private func handleDisconnect() {
70
+ guard let config = config else { return }
71
+ connected = false
72
+ onStateChange?(.reconnecting)
73
+ guard reconnectAttempts < Int(config.maxReconnectAttempts) else {
74
+ onStateChange?(.disconnected)
75
+ return
76
+ }
77
+ reconnectAttempts += 1
78
+ let delay = config.reconnectIntervalMs / 1000.0
79
+ DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
80
+ [weak self] in self?.connect()
81
+ }
82
+ }
83
+
84
+ private func startSyncTimer() {
85
+ guard let config = config else { return }
86
+ DispatchQueue.main.async {
87
+ self.syncTimer = Timer.scheduledTimer(
88
+ withTimeInterval: config.syncIntervalMs / 1000.0,
89
+ repeats: true
90
+ ) { [weak self] _ in self?.flushQueue() }
91
+ }
92
+ }
93
+
94
+ func flushQueue() -> Bool {
95
+ guard let config = config, let dbWriter = dbWriter else { return false }
96
+ let queued = dbWriter.getUnsyncedBatch(limit: Int(config.batchSize))
97
+ guard !queued.isEmpty else { return true }
98
+
99
+ let ids = queued.map { $0.id }
100
+ let payload: [[String: Any]] = queued.map { item in
101
+ [
102
+ "latitude": item.data.latitude,
103
+ "longitude": item.data.longitude,
104
+ "altitude": item.data.altitude,
105
+ "speed": item.data.speed,
106
+ "bearing": item.data.bearing,
107
+ "accuracy": item.data.accuracy,
108
+ "timestamp": item.data.timestamp
109
+ ]
110
+ }
111
+
112
+ if connected {
113
+ if let json = try? JSONSerialization.data(withJSONObject: payload),
114
+ let str = String(data: json, encoding: .utf8) {
115
+ send(str)
116
+ dbWriter.markSynced(ids)
117
+ return true
118
+ }
119
+ } else {
120
+ sendBatchHTTP(payload, ids: ids)
121
+ }
122
+ return true
123
+ }
124
+
125
+ private func sendBatchHTTP(_ locations: [[String: Any]], ids: [String]) {
126
+ guard let config = config,
127
+ let url = URL(string: "\(config.restUrl)/locations/batch")
128
+ else { return }
129
+
130
+ var request = URLRequest(url: url)
131
+ request.httpMethod = "POST"
132
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
133
+ request.setValue("Bearer \(config.authToken)",
134
+ forHTTPHeaderField: "Authorization")
135
+ request.httpBody = try? JSONSerialization.data(withJSONObject: locations)
136
+
137
+ URLSession.shared.dataTask(with: request) { [weak self] _, response, error in
138
+ let http = response as? HTTPURLResponse
139
+ if error == nil && http?.statusCode == 200 {
140
+ self?.dbWriter?.markSynced(ids)
141
+ }
142
+ }.resume()
143
+ }
144
+ }
@@ -0,0 +1,146 @@
1
+ import CoreLocation
2
+
3
+ class LocationEngine: NSObject, CLLocationManagerDelegate {
4
+ private let locationManager = CLLocationManager()
5
+ private var tracking = false
6
+
7
+ var onLocation: ((LocationData) -> Void)?
8
+ var onMotionChange: ((Bool) -> Void)?
9
+ var dbWriter: NativeDBWriter?
10
+ var currentRideId: String?
11
+
12
+ override init() {
13
+ super.init()
14
+ locationManager.delegate = self
15
+ }
16
+
17
+ func configure(_ config: LocationConfig) {
18
+ switch config.desiredAccuracy {
19
+ case .high:
20
+ locationManager.desiredAccuracy = kCLLocationAccuracyBest
21
+ case .balanced:
22
+ locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
23
+ default:
24
+ locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
25
+ }
26
+
27
+ locationManager.distanceFilter = config.distanceFilter
28
+ locationManager.allowsBackgroundLocationUpdates = true
29
+ locationManager.showsBackgroundLocationIndicator = true
30
+ locationManager.pausesLocationUpdatesAutomatically = false
31
+ }
32
+
33
+ func start() {
34
+ guard CLLocationManager.authorizationStatus() == .authorizedAlways ||
35
+ CLLocationManager.authorizationStatus() == .authorizedWhenInUse else {
36
+ locationManager.requestAlwaysAuthorization()
37
+ return
38
+ }
39
+ locationManager.startUpdatingLocation()
40
+ tracking = true
41
+ }
42
+
43
+ func stop() {
44
+ locationManager.stopUpdatingLocation()
45
+ tracking = false
46
+ }
47
+
48
+ var isTracking: Bool { tracking }
49
+
50
+ func getCurrentLocation(completion: @escaping (LocationData?) -> Void) {
51
+ locationManager.requestLocation()
52
+ // Store completion for delegate callback
53
+ pendingLocationCompletion = completion
54
+ }
55
+
56
+ private var pendingLocationCompletion: ((LocationData?) -> Void)?
57
+
58
+ func locationManager(_ manager: CLLocationManager,
59
+ didUpdateLocations locations: [CLLocation]) {
60
+ guard let location = locations.last else { return }
61
+
62
+ let data = LocationData(
63
+ latitude: location.coordinate.latitude,
64
+ longitude: location.coordinate.longitude,
65
+ altitude: location.altitude,
66
+ speed: max(location.speed, 0),
67
+ bearing: max(location.course, 0),
68
+ accuracy: location.horizontalAccuracy,
69
+ timestamp: location.timestamp.timeIntervalSince1970 * 1000
70
+ )
71
+
72
+
73
+ print(data,"dataaas")
74
+
75
+ // Handle pending one-shot request
76
+ if let completion = pendingLocationCompletion {
77
+ completion(data)
78
+ pendingLocationCompletion = nil
79
+ }
80
+
81
+ // Save to SQLite immediately (offline-first)
82
+ dbWriter?.insert(data, rideId: currentRideId)
83
+
84
+ // Notify JS via Nitro callback
85
+ onLocation?(data)
86
+
87
+ // Motion detection
88
+ onMotionChange?(location.speed >= 0.5)
89
+ }
90
+
91
+ func locationManager(_ manager: CLLocationManager,
92
+ didFailWithError error: Error) {
93
+ if let completion = pendingLocationCompletion {
94
+ completion(nil)
95
+ pendingLocationCompletion = nil
96
+ }
97
+ }
98
+ }
99
+
100
+ //// Simple struct to pass location data between components
101
+ //struct LocationData {
102
+ // let latitude: Double
103
+ // let longitude: Double
104
+ // let altitude: Double
105
+ // let speed: Double
106
+ // let bearing: Double
107
+ // let accuracy: Double
108
+ // let timestamp: Double
109
+ //
110
+ // var id: String = ""
111
+ //
112
+ // func toJSON() -> [String: Any] {
113
+ // return [
114
+ // "latitude": latitude,
115
+ // "longitude": longitude,
116
+ // "altitude": altitude,
117
+ // "speed": speed,
118
+ // "bearing": bearing,
119
+ // "accuracy": accuracy,
120
+ // "timestamp": timestamp
121
+ // ]
122
+ // }
123
+ //}
124
+ //
125
+ //// Config structs
126
+ //struct LocationConfig {
127
+ // let desiredAccuracy: String
128
+ // let distanceFilter: Double
129
+ // let intervalMs: Double
130
+ // let fastestIntervalMs: Double
131
+ // let stopTimeout: Double
132
+ // let stopOnTerminate: Bool
133
+ // let startOnBoot: Bool
134
+ // let foregroundNotificationTitle: String
135
+ // let foregroundNotificationText: String
136
+ //}
137
+ //
138
+ //struct ConnectionConfig {
139
+ // let wsUrl: String
140
+ // let restUrl: String
141
+ // let authToken: String
142
+ // let reconnectIntervalMs: Double
143
+ // let maxReconnectAttempts: Double
144
+ // let batchSize: Double
145
+ // let syncIntervalMs: Double
146
+ //}
@@ -0,0 +1,121 @@
1
+ import SQLite3
2
+
3
+ class NativeDBWriter {
4
+ private var db: OpaquePointer?
5
+
6
+ init() {
7
+ let documentsDir = FileManager.default.urls(
8
+ for: .documentDirectory, in: .userDomainMask).first!
9
+ let dbPath = documentsDir
10
+ .appendingPathComponent("nitro_location.db").path
11
+ sqlite3_open(dbPath, &db)
12
+ sqlite3_exec(db, "PRAGMA journal_mode = WAL", nil, nil, nil)
13
+ createTables()
14
+ }
15
+
16
+ private func createTables() {
17
+ let sql = """
18
+ CREATE TABLE IF NOT EXISTS locations (
19
+ id TEXT PRIMARY KEY,
20
+ latitude REAL NOT NULL,
21
+ longitude REAL NOT NULL,
22
+ altitude REAL,
23
+ speed REAL,
24
+ bearing REAL,
25
+ accuracy REAL,
26
+ timestamp INTEGER NOT NULL,
27
+ ride_id TEXT,
28
+ synced INTEGER DEFAULT 0,
29
+ retry_count INTEGER DEFAULT 0,
30
+ created_at INTEGER DEFAULT (strftime('%s','now') * 1000)
31
+ );
32
+ CREATE INDEX IF NOT EXISTS idx_locations_synced ON locations(synced);
33
+ CREATE INDEX IF NOT EXISTS idx_locations_timestamp ON locations(timestamp);
34
+ CREATE INDEX IF NOT EXISTS idx_locations_ride_id ON locations(ride_id);
35
+ """
36
+ sqlite3_exec(db, sql, nil, nil, nil)
37
+ }
38
+
39
+ func insert(_ location: LocationData, rideId: String? = nil) {
40
+ let sql = """
41
+ INSERT INTO locations
42
+ (id, latitude, longitude, altitude, speed,
43
+ bearing, accuracy, timestamp, ride_id, synced)
44
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
45
+ """
46
+ var stmt: OpaquePointer?
47
+ if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK {
48
+ let id = UUID().uuidString
49
+ sqlite3_bind_text(stmt, 1, (id as NSString).utf8String, -1, nil)
50
+ sqlite3_bind_double(stmt, 2, location.latitude)
51
+ sqlite3_bind_double(stmt, 3, location.longitude)
52
+ sqlite3_bind_double(stmt, 4, location.altitude)
53
+ sqlite3_bind_double(stmt, 5, location.speed)
54
+ sqlite3_bind_double(stmt, 6, location.bearing)
55
+ sqlite3_bind_double(stmt, 7, location.accuracy)
56
+ sqlite3_bind_int64(stmt, 8, Int64(location.timestamp))
57
+ if let rideId = rideId {
58
+ sqlite3_bind_text(stmt, 9,
59
+ (rideId as NSString).utf8String, -1, nil)
60
+ } else {
61
+ sqlite3_bind_null(stmt, 9)
62
+ }
63
+ sqlite3_step(stmt)
64
+ }
65
+ sqlite3_finalize(stmt)
66
+ }
67
+
68
+ func getUnsyncedBatch(limit: Int) -> [(id: String, data: LocationData)] {
69
+ let sql = """
70
+ SELECT id, latitude, longitude, altitude, speed,
71
+ bearing, accuracy, timestamp
72
+ FROM locations WHERE synced = 0
73
+ ORDER BY timestamp ASC LIMIT ?
74
+ """
75
+ var results: [(id: String, data: LocationData)] = []
76
+ var stmt: OpaquePointer?
77
+ if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK {
78
+ sqlite3_bind_int(stmt, 1, Int32(limit))
79
+ while sqlite3_step(stmt) == SQLITE_ROW {
80
+ let id = String(cString: sqlite3_column_text(stmt, 0))
81
+ let data = LocationData(
82
+ latitude: sqlite3_column_double(stmt, 1),
83
+ longitude: sqlite3_column_double(stmt, 2),
84
+ altitude: sqlite3_column_double(stmt, 3),
85
+ speed: sqlite3_column_double(stmt, 4),
86
+ bearing: sqlite3_column_double(stmt, 5),
87
+ accuracy: sqlite3_column_double(stmt, 6),
88
+ timestamp: Double(sqlite3_column_int64(stmt, 7))
89
+ )
90
+ results.append((id: id, data: data))
91
+ }
92
+ }
93
+ sqlite3_finalize(stmt)
94
+ return results
95
+ }
96
+
97
+ func markSynced(_ ids: [String]) {
98
+ let placeholders = ids.map { _ in "?" }.joined(separator: ",")
99
+ let sql = "UPDATE locations SET synced = 1 WHERE id IN (\(placeholders))"
100
+ var stmt: OpaquePointer?
101
+ if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK {
102
+ for (i, id) in ids.enumerated() {
103
+ sqlite3_bind_text(stmt, Int32(i + 1),
104
+ (id as NSString).utf8String, -1, nil)
105
+ }
106
+ sqlite3_step(stmt)
107
+ }
108
+ sqlite3_finalize(stmt)
109
+ }
110
+
111
+ func clearOldSynced() {
112
+ sqlite3_exec(db, """
113
+ DELETE FROM locations WHERE synced = 1
114
+ AND timestamp < (strftime('%s','now') * 1000 - 86400000)
115
+ """, nil, nil, nil)
116
+ }
117
+
118
+ deinit {
119
+ sqlite3_close(db)
120
+ }
121
+ }