therms-device-tracker 1.0.0-rc.1

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 (67) hide show
  1. package/ARCHITECTURE.md +145 -0
  2. package/CHANGELOG.md +26 -0
  3. package/LICENSE +21 -0
  4. package/README.md +386 -0
  5. package/android/build.gradle +25 -0
  6. package/android/src/main/AndroidManifest.xml +23 -0
  7. package/android/src/main/java/expo/modules/thermsdevicetracker/ActivityRecognitionProvider.kt +109 -0
  8. package/android/src/main/java/expo/modules/thermsdevicetracker/GeofenceProvider.kt +184 -0
  9. package/android/src/main/java/expo/modules/thermsdevicetracker/GeofenceTransitionReceiver.kt +34 -0
  10. package/android/src/main/java/expo/modules/thermsdevicetracker/LocationProvider.kt +84 -0
  11. package/android/src/main/java/expo/modules/thermsdevicetracker/LocationStore.kt +150 -0
  12. package/android/src/main/java/expo/modules/thermsdevicetracker/ScheduleProvider.kt +55 -0
  13. package/android/src/main/java/expo/modules/thermsdevicetracker/SyncProvider.kt +292 -0
  14. package/android/src/main/java/expo/modules/thermsdevicetracker/ThermsDeviceTrackerModule.kt +726 -0
  15. package/android/src/main/java/expo/modules/thermsdevicetracker/ThermsDeviceTrackerModuleSharedObject.kt +23 -0
  16. package/android/src/main/java/expo/modules/thermsdevicetracker/ThermsLocationService.kt +129 -0
  17. package/app.plugin.js +1 -0
  18. package/build/DeviceSettings.d.ts +14 -0
  19. package/build/DeviceSettings.d.ts.map +1 -0
  20. package/build/DeviceSettings.js +24 -0
  21. package/build/DeviceSettings.js.map +1 -0
  22. package/build/Logger.d.ts +13 -0
  23. package/build/Logger.d.ts.map +1 -0
  24. package/build/Logger.js +27 -0
  25. package/build/Logger.js.map +1 -0
  26. package/build/NativeModule.d.ts +51 -0
  27. package/build/NativeModule.d.ts.map +1 -0
  28. package/build/NativeModule.js +159 -0
  29. package/build/NativeModule.js.map +1 -0
  30. package/build/ThermsDeviceTracker.types.d.ts +204 -0
  31. package/build/ThermsDeviceTracker.types.d.ts.map +1 -0
  32. package/build/ThermsDeviceTracker.types.js +34 -0
  33. package/build/ThermsDeviceTracker.types.js.map +1 -0
  34. package/build/ThermsDeviceTrackerModule.d.ts +43 -0
  35. package/build/ThermsDeviceTrackerModule.d.ts.map +1 -0
  36. package/build/ThermsDeviceTrackerModule.js +3 -0
  37. package/build/ThermsDeviceTrackerModule.js.map +1 -0
  38. package/build/ThermsDeviceTrackerModule.web.d.ts +47 -0
  39. package/build/ThermsDeviceTrackerModule.web.d.ts.map +1 -0
  40. package/build/ThermsDeviceTrackerModule.web.js +132 -0
  41. package/build/ThermsDeviceTrackerModule.web.js.map +1 -0
  42. package/build/ThermsDeviceTrackerModuleSharedObject.d.ts +46 -0
  43. package/build/ThermsDeviceTrackerModuleSharedObject.d.ts.map +1 -0
  44. package/build/ThermsDeviceTrackerModuleSharedObject.js +24 -0
  45. package/build/ThermsDeviceTrackerModuleSharedObject.js.map +1 -0
  46. package/build/index.d.ts +101 -0
  47. package/build/index.d.ts.map +1 -0
  48. package/build/index.js +221 -0
  49. package/build/index.js.map +1 -0
  50. package/build/plugin/index.d.ts +14 -0
  51. package/build/plugin/index.d.ts.map +1 -0
  52. package/build/plugin/index.js +83 -0
  53. package/build/plugin/index.js.map +1 -0
  54. package/build/tsconfig.tsbuildinfo +1 -0
  55. package/expo-module.config.json +9 -0
  56. package/ios/GeofenceManager.swift +221 -0
  57. package/ios/LocationProvider.swift +32 -0
  58. package/ios/LocationStore.swift +98 -0
  59. package/ios/MotionActivityProvider.swift +109 -0
  60. package/ios/ProviderMonitor.swift +33 -0
  61. package/ios/ScheduleManager.swift +33 -0
  62. package/ios/SyncManager.swift +186 -0
  63. package/ios/ThermsDeviceTracker.podspec +24 -0
  64. package/ios/ThermsDeviceTrackerModule.swift +632 -0
  65. package/ios/ThermsDeviceTrackerModuleSharedObject.swift +17 -0
  66. package/ios/ThermsGeofenceTests.swift +474 -0
  67. package/package.json +95 -0
@@ -0,0 +1,186 @@
1
+ import Foundation
2
+
3
+ /// Simple protocol for Sync data source seam (testability, mirrors GeofenceRegionMonitoring).
4
+ /// Allows injecting mock stores in SyncManager tests without concrete LocationStore dep.
5
+ /// Both batch and immediate use live getAll() at perform time.
6
+ protocol SyncDataSource: AnyObject {
7
+ func getAll() -> [[String: Any]]
8
+ }
9
+
10
+ /// Focused manager for opt-in native background HTTP data sync.
11
+ /// Mirrors GeofenceManager + ScheduleManager seams for consistency (callbacks, injection, testability).
12
+ ///
13
+ /// Responsibilities:
14
+ /// - Accept sync config (url, method, batch/immediate, interval, etc.) from JS via module.
15
+ /// - Pull data from injected LocationStore (primarily locations; geofence data not included in sync payloads).
16
+ /// - Perform native HTTP POST/PUT using URLSession (background config + temp file body for suspended/killed app durability).
17
+ /// - Support immediate (on data) or periodic batch.
18
+ /// - Emit via onSync callback (success/fail, count, etc.). No direct sendEvent coupling.
19
+ ///
20
+ /// Key invariants (do not regress):
21
+ /// - Live `getAll()` from the injected SyncDataSource at perform time for both batch and immediate.
22
+ /// - Consumer responsibility: on success we only emit; caller decides to destroyLocations().
23
+ /// - onSync callback only; module owns sendEvent.
24
+ ///
25
+ /// This keeps ThermsDeviceTrackerModule thin. Callback pattern: owner (module) sets `onSync` closure;
26
+ /// manager never calls sendEvent directly. See GeofenceManager/ScheduleManager for same seam.
27
+ ///
28
+ /// Config (from ThermsConfig.sync):
29
+ /// - enabled, url (required), method, headers, batch, interval, maxBatchSize.
30
+ ///
31
+ /// Data: reuses persisted records from LocationStore (see insert/getAll). Module controls when
32
+ /// to call syncNow() (e.g. after location insert if immediate, or on schedule heartbeat).
33
+ ///
34
+ /// ---
35
+ /// iOS PERIODIC BATCH LIMITATIONS + REQUIREMENTS (CRITICAL - READ):
36
+ /// - **Current impl**: periodic batch uses a repeating `Timer` (scheduled in `start`). This ONLY fires
37
+ /// while the app process is alive (foreground, or limited bg execution after user activity).
38
+ /// It does NOT wake a suspended or terminated app.
39
+ /// - **For true background periodic sync on iOS** (while suspended/killed):
40
+ /// 1. Ensure "Background fetch" capability / UIBackgroundModes includes "fetch" (config plugin adds this
41
+ /// automatically when isIosBackgroundLocationEnabled, along with 'location').
42
+ /// 2. Pass `iosBackgroundTaskIdentifier` (e.g. "com.yourcompany.therms.sync") to the config plugin in app.json.
43
+ /// The plugin (see plugin/src/index.ts + ThermsDeviceTrackerPluginProps) appends it to BGTaskSchedulerPermittedIdentifiers.
44
+ /// 3. In your AppDelegate (or SceneDelegate equivalent), BEFORE app finishes launching:
45
+ /// `BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.yourcompany.therms.sync", using: nil) { task in
46
+ /// // Your handler: call into your extended module or use expo-task-manager + JS ThermsDeviceTracker.startSync().
47
+ /// // Then: task.setTaskCompleted(success: true)
48
+ /// // Optionally schedule next: let req = BGAppRefreshTaskRequest(identifier: ...); try? BGTaskScheduler.shared.submit(req)
49
+ /// }`
50
+ /// The identifier **must match exactly** the iosBackgroundTaskIdentifier supplied to the plugin.
51
+ /// 4. Submit a refresh task request from active code or prior handler.
52
+ /// - The `performSync` itself *does* use a background-configured URLSession (uploadTask with temp file body), which CAN continue
53
+ /// uploads even if the app is suspended/killed (OS will manage the session). But *scheduling* the batch
54
+ /// call requires BGTaskScheduler (or the demo Timer).
55
+ /// - See Apple "BGTaskScheduler", "Background Modes", "URLSessionConfiguration.background".
56
+ /// - In practice for Expo apps, many use expo-task-manager or a host AppDelegate wrapper for registration.
57
+ /// Without the above, batch falls back to Timer-only behavior.
58
+ /// - Registration seam notes: See the full AppDelegate Swift example + expo-task-manager alternative in README.md
59
+ /// ("iOS Background Sync..." section, which includes use of the identifier and cross-refs example/App.tsx).
60
+ /// The example/App.tsx has explicit comments + small helper demonstrating registration using iosBackgroundTaskIdentifier.
61
+ /// The module does not expose a public `shared` singleton or direct `syncManager` by default (thin coordinator design).
62
+ /// For pure native BGTask handlers you would extend the native module yourself to add static access + a trigger.
63
+ /// Recommended for Expo/JS apps: use expo-task-manager + ThermsDeviceTracker.startSync() (wakes JS, calls through public API).
64
+ /// This triggers the manager (see example/App.tsx).
65
+ /// ---
66
+ final class SyncManager {
67
+ // Callback supplied by owner (module) for events like onSync.
68
+ // Pattern: consistent onSync callback (no direct sendEvent). Mirrors other *Manager seams.
69
+ var onSync: (([String: Any]) -> Void)?
70
+
71
+ // Injected for data access + test seams (like store in GeofenceManager).
72
+ private weak var store: SyncDataSource?
73
+
74
+ private var config: [String: Any]?
75
+ private var isEnabled = false
76
+ private var timer: Timer? // fallback/demo; real bg uses BGTaskScheduler
77
+
78
+ init(store: SyncDataSource? = nil) {
79
+ self.store = store
80
+ }
81
+
82
+ /// Start/configure sync. Called from module on ready/start when sync.enabled.
83
+ /// Mirrors scheduleManager.start(config).
84
+ func start(config: [String: Any]?) {
85
+ stop()
86
+ self.config = config
87
+ isEnabled = (config?["enabled"] as? Bool) ?? false
88
+ guard isEnabled, let url = config?["url"] as? String, !url.isEmpty else {
89
+ onSync?(["state": "disabled", "reason": "no url or disabled"])
90
+ return
91
+ }
92
+ onSync?(["state": "started", "url": url])
93
+
94
+ let batch = (config?["batch"] as? Bool) ?? true
95
+ if batch {
96
+ let interval = (config?["interval"] as? TimeInterval) ?? 60.0
97
+ // Demo/fallback periodic via Timer (process must be alive). See top-of-file CRITICAL section (updated)
98
+ // for full BGTaskScheduler + iosBackgroundTaskIdentifier (via plugin) + AppDelegate registration requirements
99
+ // to get OS bg periodic execution. (Without host registration, this Timer path is the only periodic mechanism.)
100
+ // Note: unlike Android WorkManager (15min min for periodic), Timer accepts small intervals here.
101
+ timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
102
+ self?.performSync()
103
+ }
104
+ } else {
105
+ // Immediate mode: caller (module) should invoke syncNow() after data events.
106
+ }
107
+ }
108
+
109
+ func stop() {
110
+ timer?.invalidate()
111
+ timer = nil
112
+ isEnabled = false
113
+ config = nil
114
+ onSync?(["state": "stopped"])
115
+ }
116
+
117
+ /// Trigger a sync (called by module on location if immediate, or from schedule).
118
+ /// Pulls from store (locations primarily).
119
+ func syncNow() {
120
+ guard isEnabled else { return }
121
+ performSync()
122
+ }
123
+
124
+ private func performSync() {
125
+ guard let urlStr = config?["url"] as? String,
126
+ let url = URL(string: urlStr),
127
+ let store = store else { return }
128
+
129
+ let items = store.getAll()
130
+ guard !items.isEmpty else {
131
+ onSync?(["state": "noop", "count": 0])
132
+ return
133
+ }
134
+
135
+ let maxBatchSize = (config?["maxBatchSize"] as? Int) ?? 0
136
+ let payload: [[String: Any]] = maxBatchSize > 0 ? Array(items.prefix(maxBatchSize)) : items
137
+ // maxBatchSize <= 0 (or absent) means "send all available" (matches Android SyncWorker behavior).
138
+
139
+ let method = (config?["method"] as? String)?.uppercased() ?? "POST"
140
+ var req = URLRequest(url: url)
141
+ req.httpMethod = method
142
+ if let headers = config?["headers"] as? [String: String] {
143
+ for (k, v) in headers { req.setValue(v, forHTTPHeaderField: k) }
144
+ }
145
+ req.setValue("application/json", forHTTPHeaderField: "Content-Type")
146
+
147
+ // Serialize payload. For background URLSession uploads (esp. when app may suspend/kill),
148
+ // write to a temp file and use uploadTask(with:fromFile:) instead of in-memory body.
149
+ // This follows Apple guidance for background transfer service durability.
150
+ let fileURL: URL
151
+ do {
152
+ let bodyData = try JSONSerialization.data(withJSONObject: payload)
153
+ let tmpDir = FileManager.default.temporaryDirectory
154
+ fileURL = tmpDir.appendingPathComponent("therms-sync-\(UUID().uuidString).json")
155
+ try bodyData.write(to: fileURL)
156
+ } catch {
157
+ onSync?(["state": "error", "message": "serialize failed"])
158
+ return
159
+ }
160
+
161
+ // Use background-capable session for bg execution.
162
+ let bgConfig = URLSessionConfiguration.background(withIdentifier: "therms.sync.\(UUID().uuidString)")
163
+ let session = URLSession(configuration: bgConfig)
164
+
165
+ let task = session.uploadTask(with: req, fromFile: fileURL) { [weak self] data, response, error in
166
+ let httpResp = response as? HTTPURLResponse
167
+ let success = (error == nil) && (httpResp?.statusCode ?? 500) < 300
168
+ self?.onSync?([
169
+ "state": success ? "success" : "error",
170
+ "success": success,
171
+ "status": httpResp?.statusCode ?? -1,
172
+ "count": payload.count,
173
+ "message": error?.localizedDescription
174
+ ])
175
+
176
+ // Best-effort cleanup of temp body file (harmless if already removed).
177
+ try? FileManager.default.removeItem(at: fileURL)
178
+
179
+ if success {
180
+ // Best-effort: after successful send of batch prefix, caller/module can destroy if desired.
181
+ // For now emit; consumer decides (or future auto after ack).
182
+ }
183
+ }
184
+ task.resume()
185
+ }
186
+ }
@@ -0,0 +1,24 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'ThermsDeviceTracker'
3
+ s.version = '1.0.0'
4
+ s.summary = 'Device physical activity and geolocation tracking for THERMS'
5
+ s.description = 'Expo native module providing unified access to location updates, physical activity recognition (walking/running/etc), and pedometer data with foreground and background support.'
6
+ s.author = 'Cory Robinson <cory@therms.io>'
7
+ s.homepage = 'https://www.therms.io'
8
+ s.platforms = {
9
+ :ios => '16.4',
10
+ :tvos => '16.4'
11
+ }
12
+ s.source = { git: '' }
13
+ s.static_framework = true
14
+
15
+ s.dependency 'ExpoModulesCore'
16
+
17
+ # Swift/Objective-C compatibility
18
+ s.pod_target_xcconfig = {
19
+ 'DEFINES_MODULE' => 'YES',
20
+ }
21
+
22
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
23
+ s.exclude_files = "**/*Tests.{swift,h,m,mm}"
24
+ end