@kafitra/react-native-live-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 (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +396 -0
  3. package/android/build.gradle +71 -0
  4. package/android/gradle.properties +7 -0
  5. package/android/src/main/AndroidManifest.xml +40 -0
  6. package/android/src/main/java/com/livetracking/LiveTrackingModuleImpl.kt +728 -0
  7. package/android/src/main/java/com/livetracking/LiveTrackingPackage.kt +16 -0
  8. package/android/src/main/java/com/livetracking/location/LocationEngine.kt +93 -0
  9. package/android/src/main/java/com/livetracking/network/NetworkListener.kt +127 -0
  10. package/android/src/main/java/com/livetracking/optimizer/ActivityRecognitionHandler.kt +248 -0
  11. package/android/src/main/java/com/livetracking/optimizer/MotionSleepManager.kt +130 -0
  12. package/android/src/main/java/com/livetracking/permissions/PermissionHandler.kt +145 -0
  13. package/android/src/main/java/com/livetracking/queue/QueueEngine.kt +167 -0
  14. package/android/src/main/java/com/livetracking/queue/QueuedLocation.kt +16 -0
  15. package/android/src/main/java/com/livetracking/queue/TrackingDatabase.kt +239 -0
  16. package/android/src/main/java/com/livetracking/receiver/BootReceiver.kt +53 -0
  17. package/android/src/main/java/com/livetracking/service/TrackingForegroundService.kt +145 -0
  18. package/android/src/main/java/com/livetracking/sync/FirebaseSyncEngine.kt +277 -0
  19. package/android/src/main/java/com/livetracking/sync/LocationDataPoint.kt +31 -0
  20. package/android/src/main/java/com/livetracking/sync/SyncEngineController.kt +220 -0
  21. package/android/src/main/java/com/livetracking/sync/SyncTargetConfig.kt +20 -0
  22. package/android/src/main/java/com/livetracking/sync/TargetHandler.kt +601 -0
  23. package/android/src/newarch/java/com/livetracking/LiveTrackingModule.kt +64 -0
  24. package/android/src/oldarch/java/com/livetracking/LiveTrackingModule.kt +70 -0
  25. package/android/src/test/java/com/livetracking/BackoffCalculationTest.kt +216 -0
  26. package/android/src/test/java/com/livetracking/BatchAccumulatorTest.kt +391 -0
  27. package/android/src/test/java/com/livetracking/BootReceiverTest.kt +247 -0
  28. package/android/src/test/java/com/livetracking/FirebaseSyncEngineTest.kt +337 -0
  29. package/android/src/test/java/com/livetracking/LocationEngineTest.kt +202 -0
  30. package/android/src/test/java/com/livetracking/MotionSleepManagerTest.kt +420 -0
  31. package/android/src/test/java/com/livetracking/OfflineQueueTest.kt +462 -0
  32. package/android/src/test/java/com/livetracking/PermissionHandlerTest.kt +200 -0
  33. package/android/src/test/java/com/livetracking/QueueEngineTest.kt +335 -0
  34. package/android/src/test/java/com/livetracking/SyncEngineControllerTest.kt +855 -0
  35. package/ios/ActivityRecognitionHandler.swift +196 -0
  36. package/ios/BackgroundModeHelper.swift +132 -0
  37. package/ios/FirebaseSyncEngine.swift +276 -0
  38. package/ios/LiveTracking-Bridging-Header.h +2 -0
  39. package/ios/LiveTracking.m +37 -0
  40. package/ios/LiveTracking.swift +773 -0
  41. package/ios/LocationDataPoint.swift +56 -0
  42. package/ios/LocationEngine.swift +160 -0
  43. package/ios/MotionSleepManager.swift +151 -0
  44. package/ios/NetworkListener.swift +105 -0
  45. package/ios/OfflineQueueManager.swift +503 -0
  46. package/ios/PermissionHandler.swift +148 -0
  47. package/ios/QueueEngine.swift +249 -0
  48. package/ios/SyncEngineController.swift +396 -0
  49. package/ios/SyncTargetConfig.swift +36 -0
  50. package/ios/TargetHandler.swift +715 -0
  51. package/ios/Tests/ActivityRecognitionHandlerTests.swift +259 -0
  52. package/ios/Tests/FirebaseSyncEngineTests.swift +303 -0
  53. package/ios/Tests/LocationEngineTests.swift +244 -0
  54. package/ios/Tests/MotionSleepManagerTests.swift +355 -0
  55. package/ios/Tests/NetworkListenerTests.swift +188 -0
  56. package/ios/Tests/OfflineQueueFlushTests.swift +375 -0
  57. package/ios/Tests/PermissionHandlerTests.swift +238 -0
  58. package/ios/Tests/QueueEngineTests.swift +346 -0
  59. package/ios/TrackingCleanup.swift +93 -0
  60. package/ios/TrackingNotificationManager.swift +187 -0
  61. package/lib/commonjs/EventEmitter.js +113 -0
  62. package/lib/commonjs/EventEmitter.js.map +1 -0
  63. package/lib/commonjs/LiveTracking.js +134 -0
  64. package/lib/commonjs/LiveTracking.js.map +1 -0
  65. package/lib/commonjs/NativeLiveTracking.js +21 -0
  66. package/lib/commonjs/NativeLiveTracking.js.map +1 -0
  67. package/lib/commonjs/filters/distanceTimeFilter.js +63 -0
  68. package/lib/commonjs/filters/distanceTimeFilter.js.map +1 -0
  69. package/lib/commonjs/index.js +103 -0
  70. package/lib/commonjs/index.js.map +1 -0
  71. package/lib/commonjs/serialization/locationSerializer.js +51 -0
  72. package/lib/commonjs/serialization/locationSerializer.js.map +1 -0
  73. package/lib/commonjs/types.js +77 -0
  74. package/lib/commonjs/types.js.map +1 -0
  75. package/lib/commonjs/utils/distance.js +63 -0
  76. package/lib/commonjs/utils/distance.js.map +1 -0
  77. package/lib/commonjs/utils/retry.js +80 -0
  78. package/lib/commonjs/utils/retry.js.map +1 -0
  79. package/lib/commonjs/validation.js +463 -0
  80. package/lib/commonjs/validation.js.map +1 -0
  81. package/lib/module/EventEmitter.js +105 -0
  82. package/lib/module/EventEmitter.js.map +1 -0
  83. package/lib/module/LiveTracking.js +127 -0
  84. package/lib/module/LiveTracking.js.map +1 -0
  85. package/lib/module/NativeLiveTracking.js +16 -0
  86. package/lib/module/NativeLiveTracking.js.map +1 -0
  87. package/lib/module/filters/distanceTimeFilter.js +58 -0
  88. package/lib/module/filters/distanceTimeFilter.js.map +1 -0
  89. package/lib/module/index.js +32 -0
  90. package/lib/module/index.js.map +1 -0
  91. package/lib/module/serialization/locationSerializer.js +45 -0
  92. package/lib/module/serialization/locationSerializer.js.map +1 -0
  93. package/lib/module/types.js +94 -0
  94. package/lib/module/types.js.map +1 -0
  95. package/lib/module/utils/distance.js +56 -0
  96. package/lib/module/utils/distance.js.map +1 -0
  97. package/lib/module/utils/retry.js +72 -0
  98. package/lib/module/utils/retry.js.map +1 -0
  99. package/lib/module/validation.js +456 -0
  100. package/lib/module/validation.js.map +1 -0
  101. package/lib/typescript/EventEmitter.d.ts +65 -0
  102. package/lib/typescript/EventEmitter.d.ts.map +1 -0
  103. package/lib/typescript/LiveTracking.d.ts +23 -0
  104. package/lib/typescript/LiveTracking.d.ts.map +1 -0
  105. package/lib/typescript/NativeLiveTracking.d.ts +25 -0
  106. package/lib/typescript/NativeLiveTracking.d.ts.map +1 -0
  107. package/lib/typescript/filters/distanceTimeFilter.d.ts +44 -0
  108. package/lib/typescript/filters/distanceTimeFilter.d.ts.map +1 -0
  109. package/lib/typescript/index.d.ts +21 -0
  110. package/lib/typescript/index.d.ts.map +1 -0
  111. package/lib/typescript/serialization/locationSerializer.d.ts +39 -0
  112. package/lib/typescript/serialization/locationSerializer.d.ts.map +1 -0
  113. package/lib/typescript/types.d.ts +217 -0
  114. package/lib/typescript/types.d.ts.map +1 -0
  115. package/lib/typescript/utils/distance.d.ts +38 -0
  116. package/lib/typescript/utils/distance.d.ts.map +1 -0
  117. package/lib/typescript/utils/retry.d.ts +60 -0
  118. package/lib/typescript/utils/retry.d.ts.map +1 -0
  119. package/lib/typescript/validation.d.ts +26 -0
  120. package/lib/typescript/validation.d.ts.map +1 -0
  121. package/package.json +126 -0
  122. package/react-native-live-tracking.podspec +47 -0
  123. package/src/EventEmitter.ts +118 -0
  124. package/src/LiveTracking.ts +159 -0
  125. package/src/NativeLiveTracking.ts +29 -0
  126. package/src/filters/distanceTimeFilter.ts +75 -0
  127. package/src/index.ts +51 -0
  128. package/src/serialization/locationSerializer.ts +57 -0
  129. package/src/types.ts +252 -0
  130. package/src/utils/distance.ts +68 -0
  131. package/src/utils/retry.ts +75 -0
  132. package/src/validation.ts +552 -0
@@ -0,0 +1,196 @@
1
+ import Foundation
2
+ import CoreMotion
3
+
4
+ /**
5
+ * Activity types recognized by the handler.
6
+ */
7
+ enum ActivityType {
8
+ case stationary
9
+ case walking
10
+ case automotive
11
+ case unknown
12
+ }
13
+
14
+ /**
15
+ * Protocol for receiving activity state changes from ActivityRecognitionHandler.
16
+ */
17
+ protocol ActivityStateDelegate: AnyObject {
18
+ /**
19
+ * Called when the detected activity type changes.
20
+ *
21
+ * - Parameter activity: The new detected activity type
22
+ */
23
+ func onActivityChanged(activity: ActivityType)
24
+
25
+ /**
26
+ * Called when the stationary duration exceeds the configured threshold.
27
+ *
28
+ * - Parameter durationMs: The duration in milliseconds that the device has been stationary
29
+ */
30
+ func onStationaryDurationExceeded(durationMs: Int64)
31
+ }
32
+
33
+ /**
34
+ * iOS Activity Recognition Handler that wraps CMMotionActivityManager.
35
+ * Detects user activity (stationary, walking, automotive) and tracks stationary duration
36
+ * to support Motion Sleep Mode for battery optimization.
37
+ *
38
+ * Requirements: 8.1, 8.3
39
+ */
40
+ class ActivityRecognitionHandler {
41
+
42
+ // MARK: - Properties
43
+
44
+ weak var delegate: ActivityStateDelegate?
45
+
46
+ /// Threshold in milliseconds for stationary duration notification (default: 3 minutes)
47
+ var stationaryThresholdMs: Int64 = 180_000
48
+
49
+ private let motionActivityManager: CMMotionActivityManager
50
+ private let operationQueue: OperationQueue
51
+
52
+ private var currentActivity: ActivityType = .unknown
53
+ private var stationaryStartTime: Date?
54
+ private var isStationary: Bool = false
55
+ private var isRunning: Bool = false
56
+ private var stationaryDurationExceededNotified: Bool = false
57
+
58
+ // MARK: - Initialization
59
+
60
+ init() {
61
+ motionActivityManager = CMMotionActivityManager()
62
+ operationQueue = OperationQueue()
63
+ operationQueue.name = "com.livetracking.activityRecognition"
64
+ operationQueue.maxConcurrentOperationCount = 1
65
+ }
66
+
67
+ // MARK: - Public Methods
68
+
69
+ /**
70
+ * Get the current detected activity type.
71
+ *
72
+ * - Returns: The current activity type
73
+ */
74
+ func getCurrentActivity() -> ActivityType {
75
+ return currentActivity
76
+ }
77
+
78
+ /**
79
+ * Get the current stationary duration in milliseconds.
80
+ * Returns 0 if the device is not currently stationary.
81
+ *
82
+ * - Returns: Duration in milliseconds that the device has been stationary, or 0
83
+ */
84
+ func getStationaryDurationMs() -> Int64 {
85
+ guard isStationary, let startTime = stationaryStartTime else {
86
+ return 0
87
+ }
88
+ return Int64(Date().timeIntervalSince(startTime) * 1000)
89
+ }
90
+
91
+ /**
92
+ * Check if the device is currently in stationary state.
93
+ *
94
+ * - Returns: true if the device is detected as stationary
95
+ */
96
+ func isDeviceStationary() -> Bool {
97
+ return isStationary
98
+ }
99
+
100
+ /**
101
+ * Start activity recognition updates.
102
+ * Uses CMMotionActivityManager to receive periodic activity detection updates.
103
+ *
104
+ * Note: Requires Motion & Fitness permission (NSMotionUsageDescription in Info.plist).
105
+ */
106
+ func startActivityRecognition() {
107
+ guard CMMotionActivityManager.isActivityAvailable() else {
108
+ print("[ActivityRecognitionHandler] Activity recognition is not available on this device")
109
+ return
110
+ }
111
+
112
+ if isRunning { return }
113
+
114
+ motionActivityManager.startActivityUpdates(to: operationQueue) { [weak self] activity in
115
+ guard let self = self, let activity = activity else { return }
116
+ self.handleActivityUpdate(activity)
117
+ }
118
+
119
+ isRunning = true
120
+ }
121
+
122
+ /**
123
+ * Stop activity recognition updates.
124
+ * Stops CMMotionActivityManager updates and resets internal state.
125
+ */
126
+ func stopActivityRecognition() {
127
+ if !isRunning { return }
128
+
129
+ motionActivityManager.stopActivityUpdates()
130
+
131
+ // Reset state
132
+ isRunning = false
133
+ isStationary = false
134
+ stationaryStartTime = nil
135
+ stationaryDurationExceededNotified = false
136
+ currentActivity = .unknown
137
+ }
138
+
139
+ // MARK: - Private Methods
140
+
141
+ private func handleActivityUpdate(_ activity: CMMotionActivity) {
142
+ let newActivity = mapToActivityType(activity)
143
+ let previousActivity = currentActivity
144
+ currentActivity = newActivity
145
+
146
+ if newActivity != previousActivity {
147
+ delegate?.onActivityChanged(activity: newActivity)
148
+ }
149
+
150
+ switch newActivity {
151
+ case .stationary:
152
+ handleStationaryState()
153
+ case .walking, .automotive:
154
+ handleMovingState()
155
+ case .unknown:
156
+ break
157
+ }
158
+ }
159
+
160
+ private func handleStationaryState() {
161
+ if !isStationary {
162
+ // Transition to stationary - start tracking duration
163
+ isStationary = true
164
+ stationaryStartTime = Date()
165
+ stationaryDurationExceededNotified = false
166
+ } else {
167
+ // Already stationary - check if threshold exceeded
168
+ let durationMs = getStationaryDurationMs()
169
+ if durationMs >= stationaryThresholdMs && !stationaryDurationExceededNotified {
170
+ stationaryDurationExceededNotified = true
171
+ delegate?.onStationaryDurationExceeded(durationMs: durationMs)
172
+ }
173
+ }
174
+ }
175
+
176
+ private func handleMovingState() {
177
+ if isStationary {
178
+ // Transition from stationary to moving
179
+ isStationary = false
180
+ stationaryStartTime = nil
181
+ stationaryDurationExceededNotified = false
182
+ }
183
+ }
184
+
185
+ private func mapToActivityType(_ activity: CMMotionActivity) -> ActivityType {
186
+ if activity.stationary {
187
+ return .stationary
188
+ } else if activity.walking || activity.running {
189
+ return .walking
190
+ } else if activity.automotive {
191
+ return .automotive
192
+ } else {
193
+ return .unknown
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,132 @@
1
+ import Foundation
2
+ import CoreLocation
3
+
4
+ /**
5
+ * BackgroundModeHelper
6
+ *
7
+ * Utility class that provides significant location change monitoring as a fallback
8
+ * mechanism when the app is terminated by the system. Significant location changes
9
+ * will relaunch the app, allowing tracking to resume.
10
+ *
11
+ * ## Important: Consuming App Configuration Required
12
+ *
13
+ * This library does NOT modify the consuming app's Info.plist. The consuming app
14
+ * MUST add the following entries to their Info.plist:
15
+ *
16
+ * ### 1. UIBackgroundModes
17
+ * Add `location` to the `UIBackgroundModes` array:
18
+ * ```xml
19
+ * <key>UIBackgroundModes</key>
20
+ * <array>
21
+ * <string>location</string>
22
+ * </array>
23
+ * ```
24
+ *
25
+ * ### 2. Location Usage Descriptions
26
+ * Add both usage description keys:
27
+ * ```xml
28
+ * <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
29
+ * <string>This app needs location access to track your position in the background.</string>
30
+ * <key>NSLocationWhenInUseUsageDescription</key>
31
+ * <string>This app needs location access to track your position.</string>
32
+ * ```
33
+ *
34
+ * ### How Significant Location Change Monitoring Works
35
+ *
36
+ * When the app is terminated by the system (e.g., due to memory pressure), iOS can
37
+ * still deliver significant location change events. These events will relaunch the app
38
+ * in the background with `UIApplication.LaunchOptionsKey.location` set in the launch
39
+ * options dictionary.
40
+ *
41
+ * The consuming app should check for this key in `application(_:didFinishLaunchingWithOptions:)`
42
+ * and restart location tracking if present:
43
+ *
44
+ * ```swift
45
+ * func application(_ application: UIApplication,
46
+ * didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
47
+ * if launchOptions?[.location] != nil {
48
+ * // App was relaunched due to a significant location change.
49
+ * // Restart tracking here.
50
+ * }
51
+ * return true
52
+ * }
53
+ * ```
54
+ *
55
+ * Significant location changes are triggered by cell tower transitions and typically
56
+ * fire every 500 meters or more. This is less precise than continuous GPS but ensures
57
+ * the app can resume full-accuracy tracking after termination.
58
+ */
59
+ @objc
60
+ class BackgroundModeHelper: NSObject, SignificantLocationMonitoring {
61
+
62
+ // MARK: - Singleton
63
+
64
+ @objc static let shared = BackgroundModeHelper()
65
+
66
+ private override init() {
67
+ super.init()
68
+ }
69
+
70
+ // MARK: - Significant Location Monitoring
71
+
72
+ /**
73
+ * Starts significant location change monitoring as a fallback mechanism.
74
+ *
75
+ * This should be called when tracking begins so that if the app is terminated
76
+ * by the system, iOS will relaunch the app upon detecting a significant location
77
+ * change (typically cell tower transitions, ~500m or more).
78
+ *
79
+ * - Parameter locationManager: The CLLocationManager instance to use for monitoring.
80
+ *
81
+ * - Note: The consuming app must have `UIBackgroundModes` with `location` in Info.plist
82
+ * and the user must have granted "Always" location permission for this to work
83
+ * after app termination.
84
+ */
85
+ @objc
86
+ func startSignificantLocationMonitoring(locationManager: CLLocationManager) {
87
+ guard CLLocationManager.significantLocationChangeMonitoringAvailable() else {
88
+ return
89
+ }
90
+ locationManager.startMonitoringSignificantLocationChanges()
91
+ }
92
+
93
+ /**
94
+ * Stops significant location change monitoring.
95
+ *
96
+ * Call this when tracking is stopped to prevent unnecessary app relaunches
97
+ * after termination.
98
+ *
99
+ * - Parameter locationManager: The CLLocationManager instance to stop monitoring on.
100
+ */
101
+ @objc
102
+ func stopSignificantLocationMonitoring(locationManager: CLLocationManager) {
103
+ locationManager.stopMonitoringSignificantLocationChanges()
104
+ }
105
+
106
+ /**
107
+ * Checks whether significant location change monitoring is available on this device.
108
+ *
109
+ * Significant location monitoring may not be available on all devices (e.g., devices
110
+ * without cellular hardware). Always check availability before starting monitoring.
111
+ *
112
+ * - Returns: `true` if significant location change monitoring is available, `false` otherwise.
113
+ */
114
+ @objc
115
+ func isSignificantLocationMonitoringAvailable() -> Bool {
116
+ return CLLocationManager.significantLocationChangeMonitoringAvailable()
117
+ }
118
+
119
+ /**
120
+ * Checks if the app was launched due to a significant location change event.
121
+ *
122
+ * Call this from `application(_:didFinishLaunchingWithOptions:)` to determine
123
+ * if the app should restart tracking.
124
+ *
125
+ * - Parameter launchOptions: The launch options dictionary from AppDelegate.
126
+ * - Returns: `true` if the app was relaunched due to a location event.
127
+ */
128
+ @objc
129
+ func wasLaunchedDueToLocationEvent(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
130
+ return launchOptions?[.location] != nil
131
+ }
132
+ }
@@ -0,0 +1,276 @@
1
+ import Foundation
2
+ import FirebaseDatabase
3
+ import FirebaseFirestore
4
+
5
+ /**
6
+ * Protocol for Firebase sync operation callbacks.
7
+ */
8
+ protocol SyncCallback: AnyObject {
9
+ func onSuccess()
10
+ func onError(errorCode: String, message: String)
11
+ }
12
+
13
+ /**
14
+ * FirebaseSyncEngine handles writing location data to Firebase.
15
+ *
16
+ * Supports both Firebase Realtime Database (RTDB) and Cloud Firestore.
17
+ * Implements retry with exponential backoff for resilient writes.
18
+ *
19
+ * - updateCurrentLocation: overwrites the current location at currentLocationPath
20
+ * - pushHistoryBatch: appends a batch of locations to historyPath
21
+ *
22
+ * Requirements: 4.1, 4.3, 5.2, 10.4
23
+ */
24
+ class FirebaseSyncEngine {
25
+
26
+ // MARK: - Constants
27
+
28
+ private static let baseDelayMs: Int = 1000
29
+ private static let multiplier: Double = 2.0
30
+ private static let jitterMs: Int = 200
31
+ private static let maxRetriesCurrent: Int = 3
32
+ private static let maxRetriesHistory: Int = 5
33
+
34
+ // MARK: - Properties
35
+
36
+ private let service: String
37
+ private let currentLocationPath: String?
38
+ private let historyPath: String?
39
+ private let syncQueue = DispatchQueue(label: "com.livetracking.firebase.sync", qos: .utility)
40
+
41
+ // MARK: - Initialization
42
+
43
+ /**
44
+ * Initialize the FirebaseSyncEngine.
45
+ *
46
+ * - Parameter service: Either "RTDB" or "Firestore"
47
+ * - Parameter currentLocationPath: Firebase path for current location (overwrite)
48
+ * - Parameter historyPath: Firebase path for history locations (append)
49
+ */
50
+ init(service: String, currentLocationPath: String?, historyPath: String?) {
51
+ self.service = service
52
+ self.currentLocationPath = currentLocationPath
53
+ self.historyPath = historyPath
54
+ }
55
+
56
+ // MARK: - Public Methods
57
+
58
+ /**
59
+ * Update the current location at the configured currentLocationPath.
60
+ * Uses set/update (overwrite) semantics.
61
+ * Retries up to 3 times with exponential backoff on failure.
62
+ *
63
+ * - Parameter latitude: Location latitude
64
+ * - Parameter longitude: Location longitude
65
+ * - Parameter timestamp: Unix timestamp in milliseconds
66
+ * - Parameter accuracy: Location accuracy in meters
67
+ * - Parameter speed: Speed in m/s, nullable
68
+ * - Parameter callback: SyncCallback for success/error notification
69
+ */
70
+ func updateCurrentLocation(
71
+ latitude: Double,
72
+ longitude: Double,
73
+ timestamp: Int64,
74
+ accuracy: Double,
75
+ speed: Double?,
76
+ callback: SyncCallback
77
+ ) {
78
+ guard let path = currentLocationPath else {
79
+ callback.onError(errorCode: "NO_PATH", message: "currentLocationPath is not configured")
80
+ return
81
+ }
82
+
83
+ var data: [String: Any] = [
84
+ "latitude": latitude,
85
+ "longitude": longitude,
86
+ "timestamp": timestamp,
87
+ "accuracy": accuracy,
88
+ "updatedAt": timestamp
89
+ ]
90
+ if let speed = speed {
91
+ data["speed"] = speed
92
+ }
93
+
94
+ syncQueue.async { [weak self] in
95
+ guard let self = self else { return }
96
+ self.executeWithRetry(
97
+ maxRetries: FirebaseSyncEngine.maxRetriesCurrent,
98
+ operation: { completion in
99
+ self.performCurrentLocationWrite(path: path, data: data, completion: completion)
100
+ },
101
+ callback: callback
102
+ )
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Push a batch of locations to the configured historyPath.
108
+ * Uses push/append semantics (each location gets a unique key).
109
+ * Retries up to 5 times with exponential backoff on failure.
110
+ *
111
+ * - Parameter locations: Array of location dictionaries to send
112
+ * - Parameter callback: SyncCallback for success/error notification
113
+ */
114
+ func pushHistoryBatch(locations: [[String: Any]], callback: SyncCallback) {
115
+ guard let path = historyPath else {
116
+ callback.onError(errorCode: "NO_PATH", message: "historyPath is not configured")
117
+ return
118
+ }
119
+
120
+ if locations.isEmpty {
121
+ callback.onSuccess()
122
+ return
123
+ }
124
+
125
+ syncQueue.async { [weak self] in
126
+ guard let self = self else { return }
127
+ self.executeWithRetry(
128
+ maxRetries: FirebaseSyncEngine.maxRetriesHistory,
129
+ operation: { completion in
130
+ self.performHistoryBatchWrite(path: path, locations: locations, completion: completion)
131
+ },
132
+ callback: callback
133
+ )
134
+ }
135
+ }
136
+
137
+ // MARK: - Private Write Methods
138
+
139
+ private func performCurrentLocationWrite(
140
+ path: String,
141
+ data: [String: Any],
142
+ completion: @escaping (Bool, String?, String?) -> Void
143
+ ) {
144
+ switch service {
145
+ case "RTDB":
146
+ let reference = Database.database().reference(withPath: path)
147
+ reference.setValue(data) { error, _ in
148
+ if let error = error {
149
+ completion(false, "FIREBASE_WRITE_FAILED", error.localizedDescription)
150
+ } else {
151
+ completion(true, nil, nil)
152
+ }
153
+ }
154
+ case "Firestore":
155
+ let document = Firestore.firestore().document(path)
156
+ document.setData(data) { error in
157
+ if let error = error {
158
+ completion(false, "FIREBASE_WRITE_FAILED", error.localizedDescription)
159
+ } else {
160
+ completion(true, nil, nil)
161
+ }
162
+ }
163
+ default:
164
+ completion(false, "INVALID_SERVICE", "Unknown service: \(service). Use 'RTDB' or 'Firestore'.")
165
+ }
166
+ }
167
+
168
+ private func performHistoryBatchWrite(
169
+ path: String,
170
+ locations: [[String: Any]],
171
+ completion: @escaping (Bool, String?, String?) -> Void
172
+ ) {
173
+ switch service {
174
+ case "RTDB":
175
+ let reference = Database.database().reference(withPath: path)
176
+ var updates: [String: Any] = [:]
177
+
178
+ for location in locations {
179
+ let key = reference.childByAutoId().key ?? UUID().uuidString
180
+ updates[key] = location
181
+ }
182
+
183
+ if updates.isEmpty {
184
+ completion(true, nil, nil)
185
+ return
186
+ }
187
+
188
+ reference.updateChildValues(updates) { error, _ in
189
+ if let error = error {
190
+ completion(false, "FIREBASE_WRITE_FAILED", error.localizedDescription)
191
+ } else {
192
+ completion(true, nil, nil)
193
+ }
194
+ }
195
+
196
+ case "Firestore":
197
+ let firestore = Firestore.firestore()
198
+ let collection = firestore.collection(path)
199
+ let batch = firestore.batch()
200
+
201
+ for location in locations {
202
+ let docRef = collection.document()
203
+ batch.setData(location, forDocument: docRef)
204
+ }
205
+
206
+ batch.commit { error in
207
+ if let error = error {
208
+ completion(false, "FIREBASE_WRITE_FAILED", error.localizedDescription)
209
+ } else {
210
+ completion(true, nil, nil)
211
+ }
212
+ }
213
+
214
+ default:
215
+ completion(false, "INVALID_SERVICE", "Unknown service: \(service). Use 'RTDB' or 'Firestore'.")
216
+ }
217
+ }
218
+
219
+ // MARK: - Retry Logic
220
+
221
+ /**
222
+ * Execute an operation with exponential backoff retry.
223
+ *
224
+ * Retry delay formula: baseDelay × 2^(attempt-1) ± jitter
225
+ * - Base delay: 1000ms
226
+ * - Multiplier: 2x
227
+ * - Jitter: ±200ms (random)
228
+ *
229
+ * - Parameter maxRetries: Maximum number of retry attempts
230
+ * - Parameter operation: The Firebase write operation to execute, calls completion(success, errorCode, message)
231
+ * - Parameter callback: Final callback after all retries exhausted or success
232
+ */
233
+ private func executeWithRetry(
234
+ maxRetries: Int,
235
+ operation: @escaping (@escaping (Bool, String?, String?) -> Void) -> Void,
236
+ callback: SyncCallback
237
+ ) {
238
+ var attempt = 0
239
+
240
+ func tryOperation() {
241
+ attempt += 1
242
+ operation { success, errorCode, message in
243
+ if success {
244
+ callback.onSuccess()
245
+ } else {
246
+ if attempt >= maxRetries {
247
+ callback.onError(
248
+ errorCode: errorCode ?? "FIREBASE_WRITE_FAILED",
249
+ message: "Failed after \(attempt) attempts: \(message ?? "Unknown error")"
250
+ )
251
+ } else {
252
+ let delay = self.calculateBackoffDelay(attempt: attempt)
253
+ Thread.sleep(forTimeInterval: Double(delay) / 1000.0)
254
+ tryOperation()
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ tryOperation()
261
+ }
262
+
263
+ /**
264
+ * Calculate exponential backoff delay with jitter.
265
+ *
266
+ * Formula: baseDelay × 2^(attempt-1) ± random jitter
267
+ *
268
+ * - Parameter attempt: Current attempt number (1-indexed)
269
+ * - Returns: Delay in milliseconds
270
+ */
271
+ func calculateBackoffDelay(attempt: Int) -> Int {
272
+ let exponentialDelay = Double(FirebaseSyncEngine.baseDelayMs) * pow(FirebaseSyncEngine.multiplier, Double(attempt - 1))
273
+ let jitter = Int.random(in: -FirebaseSyncEngine.jitterMs...FirebaseSyncEngine.jitterMs)
274
+ return max(0, Int(exponentialDelay) + jitter)
275
+ }
276
+ }
@@ -0,0 +1,2 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
@@ -0,0 +1,37 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
3
+
4
+ /**
5
+ * Objective-C bridge file for the LiveTracking Swift module.
6
+ *
7
+ * Uses RCT_EXTERN_MODULE to expose Swift methods to React Native.
8
+ * Also conforms to RCTTurboModule so the module is discoverable via
9
+ * TurboModuleRegistry in New Architecture / Bridgeless mode (RN 0.76+).
10
+ */
11
+ @interface RCT_EXTERN_MODULE(LiveTracking, RCTEventEmitter)
12
+
13
+ RCT_EXTERN_METHOD(configure:(NSString *)config
14
+ resolve:(RCTPromiseResolveBlock)resolve
15
+ reject:(RCTPromiseRejectBlock)reject)
16
+
17
+ RCT_EXTERN_METHOD(start:(RCTPromiseResolveBlock)resolve
18
+ reject:(RCTPromiseRejectBlock)reject)
19
+
20
+ RCT_EXTERN_METHOD(stop:(RCTPromiseResolveBlock)resolve
21
+ reject:(RCTPromiseRejectBlock)reject)
22
+
23
+ RCT_EXTERN_METHOD(getStatus:(RCTPromiseResolveBlock)resolve
24
+ reject:(RCTPromiseRejectBlock)reject)
25
+
26
+ RCT_EXTERN_METHOD(getQueuedLocations:(RCTPromiseResolveBlock)resolve
27
+ reject:(RCTPromiseRejectBlock)reject)
28
+
29
+ RCT_EXTERN_METHOD(getQueuedLocationsByTarget:(RCTPromiseResolveBlock)resolve
30
+ reject:(RCTPromiseRejectBlock)reject)
31
+
32
+ + (BOOL)requiresMainQueueSetup
33
+ {
34
+ return NO;
35
+ }
36
+
37
+ @end