@wayq/beekon-rn 0.0.3 → 0.0.6

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 (84) hide show
  1. package/BeekonRn.podspec +5 -3
  2. package/CHANGELOG.md +103 -0
  3. package/README.md +326 -52
  4. package/android/build.gradle +9 -4
  5. package/android/src/main/java/in/wayq/beekonrn/BeekonRnModule.kt +411 -59
  6. package/ios/BeekonRn.mm +103 -24
  7. package/ios/BeekonRn.swift +465 -61
  8. package/ios/Frameworks/BeekonKit.xcframework/Info.plist +5 -5
  9. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/BeekonKit +0 -0
  10. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Info.plist +0 -0
  11. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.abi.json +11424 -1279
  12. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  13. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.swiftinterface +262 -36
  14. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/BeekonKit +0 -0
  15. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Info.plist +0 -0
  16. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.abi.json +11424 -1279
  17. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  18. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +262 -36
  19. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +11424 -1279
  20. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  21. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +262 -36
  22. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/_CodeSignature/CodeResources +2 -80
  23. package/lib/module/NativeBeekonRn.js +35 -7
  24. package/lib/module/NativeBeekonRn.js.map +1 -1
  25. package/lib/module/beekon.js +246 -45
  26. package/lib/module/beekon.js.map +1 -1
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/internal/mappers.js +166 -25
  29. package/lib/module/internal/mappers.js.map +1 -1
  30. package/lib/module/types/auth.js +4 -0
  31. package/lib/module/types/auth.js.map +1 -0
  32. package/lib/module/types/config.js +2 -0
  33. package/lib/module/types/enums.js +2 -0
  34. package/lib/module/types/enums.js.map +1 -0
  35. package/lib/module/types/error.js +20 -4
  36. package/lib/module/types/error.js.map +1 -1
  37. package/lib/module/types/geofence.js +2 -0
  38. package/lib/module/types/geofence.js.map +1 -0
  39. package/lib/module/types/location.js +2 -0
  40. package/lib/module/types/sync.js +2 -0
  41. package/lib/module/types/sync.js.map +1 -0
  42. package/lib/typescript/src/NativeBeekonRn.d.ts +150 -20
  43. package/lib/typescript/src/NativeBeekonRn.d.ts.map +1 -1
  44. package/lib/typescript/src/beekon.d.ts +110 -33
  45. package/lib/typescript/src/beekon.d.ts.map +1 -1
  46. package/lib/typescript/src/index.d.ts +6 -2
  47. package/lib/typescript/src/index.d.ts.map +1 -1
  48. package/lib/typescript/src/internal/mappers.d.ts +16 -6
  49. package/lib/typescript/src/internal/mappers.d.ts.map +1 -1
  50. package/lib/typescript/src/types/auth.d.ts +99 -0
  51. package/lib/typescript/src/types/auth.d.ts.map +1 -0
  52. package/lib/typescript/src/types/config.d.ts +66 -20
  53. package/lib/typescript/src/types/config.d.ts.map +1 -1
  54. package/lib/typescript/src/types/enums.d.ts +62 -0
  55. package/lib/typescript/src/types/enums.d.ts.map +1 -0
  56. package/lib/typescript/src/types/error.d.ts +21 -5
  57. package/lib/typescript/src/types/error.d.ts.map +1 -1
  58. package/lib/typescript/src/types/geofence.d.ts +36 -0
  59. package/lib/typescript/src/types/geofence.d.ts.map +1 -0
  60. package/lib/typescript/src/types/location.d.ts +22 -8
  61. package/lib/typescript/src/types/location.d.ts.map +1 -1
  62. package/lib/typescript/src/types/state.d.ts +13 -4
  63. package/lib/typescript/src/types/state.d.ts.map +1 -1
  64. package/lib/typescript/src/types/sync.d.ts +27 -0
  65. package/lib/typescript/src/types/sync.d.ts.map +1 -0
  66. package/package.json +8 -5
  67. package/scripts/fetch-beekonkit.sh +5 -5
  68. package/src/NativeBeekonRn.ts +165 -20
  69. package/src/beekon.ts +278 -48
  70. package/src/index.tsx +24 -2
  71. package/src/internal/mappers.ts +242 -27
  72. package/src/types/auth.ts +101 -0
  73. package/src/types/config.ts +68 -20
  74. package/src/types/enums.ts +80 -0
  75. package/src/types/error.ts +23 -5
  76. package/src/types/geofence.ts +37 -0
  77. package/src/types/location.ts +28 -8
  78. package/src/types/state.ts +13 -3
  79. package/src/types/sync.ts +23 -0
  80. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeDirectory +0 -0
  81. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeRequirements +0 -0
  82. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeResources +0 -296
  83. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeSignature +0 -0
  84. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/_CodeSignature/CodeResources +0 -146
@@ -3,54 +3,72 @@ package `in`.wayq.beekonrn
3
3
  import com.facebook.react.bridge.Arguments
4
4
  import com.facebook.react.bridge.Promise
5
5
  import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.bridge.ReadableArray
6
7
  import com.facebook.react.bridge.ReadableMap
7
8
  import com.facebook.react.bridge.WritableArray
8
9
  import com.facebook.react.bridge.WritableMap
10
+ import `in`.wayq.beekon.AccuracyMode
11
+ import `in`.wayq.beekon.ActivityType
12
+ import `in`.wayq.beekon.AuthBodyFormat
13
+ import `in`.wayq.beekon.AuthConfig
14
+ import `in`.wayq.beekon.AuthResponseMapping
15
+ import `in`.wayq.beekon.AuthStrategy
16
+ import `in`.wayq.beekon.AuthTokens
9
17
  import `in`.wayq.beekon.Beekon
10
18
  import `in`.wayq.beekon.BeekonConfig
11
- import `in`.wayq.beekon.BeekonError
19
+ import `in`.wayq.beekon.BeekonException
20
+ import `in`.wayq.beekon.BeekonGeofence
12
21
  import `in`.wayq.beekon.BeekonState
22
+ import `in`.wayq.beekon.GeofenceEvent
13
23
  import `in`.wayq.beekon.Location
24
+ import `in`.wayq.beekon.LocationQuality
25
+ import `in`.wayq.beekon.LocationTrigger
26
+ import `in`.wayq.beekon.LocationUnavailableReason
27
+ import `in`.wayq.beekon.MotionState
28
+ import `in`.wayq.beekon.NotificationConfig
29
+ import `in`.wayq.beekon.StationaryMode
14
30
  import `in`.wayq.beekon.StopReason
31
+ import `in`.wayq.beekon.SyncConfig
32
+ import `in`.wayq.beekon.SyncFailure
33
+ import `in`.wayq.beekon.SyncStatus
34
+ import `in`.wayq.beekon.Transition
15
35
  import java.time.Instant
16
36
  import kotlinx.coroutines.CoroutineScope
17
37
  import kotlinx.coroutines.Dispatchers
18
- import kotlinx.coroutines.Job
19
38
  import kotlinx.coroutines.SupervisorJob
20
39
  import kotlinx.coroutines.cancel
21
40
  import kotlinx.coroutines.launch
22
41
 
23
- class BeekonRnModule(private val reactContext: ReactApplicationContext) :
42
+ class BeekonRnModule(reactContext: ReactApplicationContext) :
24
43
  NativeBeekonRnSpec(reactContext) {
25
44
 
26
45
  // Default dispatcher (not Main.immediate) — Codegen's emitOnX is thread-safe
27
- // and marshals to JS internally, so collecting on Main buys nothing and
28
- // risks jank if any mapper does work. SupervisorJob so a single failure
29
- // doesn't tear down siblings.
46
+ // and marshals to JS internally, so collecting on Main buys nothing and risks
47
+ // jank if a mapper does work. SupervisorJob so one failing collector doesn't
48
+ // tear down its siblings.
30
49
  private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
31
- private var stateJob: Job? = null
32
- private var locationsJob: Job? = null
33
50
 
34
51
  init {
35
- // Beekon.initialize is idempotent and does not start tracking. Safe to call
36
- // from module construction the application context is already alive.
37
- Beekon.initialize(reactContext.applicationContext)
38
- stateJob = scope.launch {
39
- Beekon.state.collect { s -> emitOnState(stateToWire(s)) }
40
- }
41
- locationsJob = scope.launch {
42
- Beekon.locations.collect { loc -> emitOnLocation(locationToWire(loc)) }
52
+ // No Beekon.initialize() the SDK auto-initializes via AndroidX Startup.
53
+ scope.launch { Beekon.state.collect { emitOnState(stateToWire(it)) } }
54
+ scope.launch { Beekon.locations.collect { emitOnLocation(locationToWire(it)) } }
55
+ scope.launch {
56
+ Beekon.geofenceEvents.collect { emitOnGeofenceEvent(geofenceEventToWire(it)) }
43
57
  }
58
+ scope.launch { Beekon.syncStatus.collect { emitOnSyncStatus(syncStatusToWire(it)) } }
59
+ scope.launch { Beekon.authChanges.collect { emitOnAuthTokens(tokenRefreshToWire(it)) } }
44
60
  }
45
61
 
62
+ // ---------------------------------------------------------------------------
63
+ // Lifecycle (non-suspend native calls resolve directly)
64
+ // ---------------------------------------------------------------------------
65
+
46
66
  override fun configure(config: ReadableMap, promise: Promise) {
47
- scope.launch {
48
- try {
49
- Beekon.configure(wireToConfig(config))
50
- promise.resolve(null)
51
- } catch (t: Throwable) {
52
- promise.reject(errorCode(t), t.message ?: "configure failed", t)
53
- }
67
+ try {
68
+ Beekon.configure(wireToConfig(config))
69
+ promise.resolve(null)
70
+ } catch (t: Throwable) {
71
+ promise.reject(errorCode(t), t.message ?: "configure failed", t)
54
72
  }
55
73
  }
56
74
 
@@ -76,17 +94,124 @@ class BeekonRnModule(private val reactContext: ReactApplicationContext) :
76
94
  }
77
95
  }
78
96
 
79
- override fun history(fromMs: Double, toMs: Double, promise: Promise) {
97
+ // Calls the guarded native resume (Beekon.resumeIfNeeded), which only
98
+ // re-adopts a previously-active, non-user-stopped session — a no-op when
99
+ // already Tracking or when the user explicitly stopped (mirrors iOS).
100
+ override fun resumeIfNeeded(promise: Promise) {
101
+ scope.launch {
102
+ try {
103
+ Beekon.resumeIfNeeded()
104
+ promise.resolve(null)
105
+ } catch (t: Throwable) {
106
+ promise.reject(errorCode(t), t.message ?: "resumeIfNeeded failed", t)
107
+ }
108
+ }
109
+ }
110
+
111
+ override fun getCurrentLocation(timeoutMs: Double, accuracy: String, promise: Promise) {
112
+ scope.launch {
113
+ try {
114
+ // Empty accuracy is the wire encoding of "use the configured mode".
115
+ val mode = if (accuracy.isEmpty()) null else toAccuracyMode(accuracy)
116
+ val loc = Beekon.getCurrentLocation(timeoutMs.toLong(), mode)
117
+ val arr: WritableArray = Arguments.createArray()
118
+ if (loc != null) arr.pushMap(locationToWire(loc))
119
+ promise.resolve(arr)
120
+ } catch (t: Throwable) {
121
+ promise.reject(errorCode(t), t.message ?: "getCurrentLocation failed", t)
122
+ }
123
+ }
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // History
128
+ // ---------------------------------------------------------------------------
129
+
130
+ override fun getLocations(fromMs: Double, toMs: Double, promise: Promise) {
80
131
  scope.launch {
81
132
  try {
82
133
  val from = Instant.ofEpochMilli(fromMs.toLong())
83
134
  val to = Instant.ofEpochMilli(toMs.toLong())
84
- val locations = Beekon.history(from, to)
85
135
  val arr: WritableArray = Arguments.createArray()
86
- for (loc in locations) arr.pushMap(locationToWire(loc))
136
+ for (loc in Beekon.getLocations(from, to)) arr.pushMap(locationToWire(loc))
87
137
  promise.resolve(arr)
88
138
  } catch (t: Throwable) {
89
- promise.reject(errorCode(t), t.message ?: "history failed", t)
139
+ promise.reject(errorCode(t), t.message ?: "getLocations failed", t)
140
+ }
141
+ }
142
+ }
143
+
144
+ override fun deleteLocations(beforeMs: Double, promise: Promise) {
145
+ scope.launch {
146
+ try {
147
+ // Negative is the wire sentinel for "delete all" (no cutoff).
148
+ val before = if (beforeMs < 0) null else Instant.ofEpochMilli(beforeMs.toLong())
149
+ promise.resolve(Beekon.deleteLocations(before))
150
+ } catch (t: Throwable) {
151
+ promise.reject(errorCode(t), t.message ?: "deleteLocations failed", t)
152
+ }
153
+ }
154
+ }
155
+
156
+ override fun pendingUploadCount(promise: Promise) {
157
+ scope.launch {
158
+ try {
159
+ promise.resolve(Beekon.pendingUploadCount())
160
+ } catch (t: Throwable) {
161
+ promise.reject(errorCode(t), t.message ?: "pendingUploadCount failed", t)
162
+ }
163
+ }
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Sync
168
+ // ---------------------------------------------------------------------------
169
+
170
+ override fun sync(promise: Promise) {
171
+ Beekon.sync()
172
+ promise.resolve(null)
173
+ }
174
+
175
+ override fun setExtras(entries: ReadableArray, promise: Promise) {
176
+ Beekon.setExtras(entriesToMap(entries))
177
+ promise.resolve(null)
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Geofences
182
+ // ---------------------------------------------------------------------------
183
+
184
+ override fun addGeofences(geofences: ReadableArray, promise: Promise) {
185
+ scope.launch {
186
+ try {
187
+ Beekon.addGeofences(wireToGeofences(geofences))
188
+ promise.resolve(null)
189
+ } catch (t: Throwable) {
190
+ promise.reject(errorCode(t), t.message ?: "addGeofences failed", t)
191
+ }
192
+ }
193
+ }
194
+
195
+ override fun removeGeofences(ids: ReadableArray, promise: Promise) {
196
+ scope.launch {
197
+ try {
198
+ val list = (0 until ids.size()).mapNotNull { ids.getString(it) }
199
+ Beekon.removeGeofences(list)
200
+ promise.resolve(null)
201
+ } catch (t: Throwable) {
202
+ promise.reject(errorCode(t), t.message ?: "removeGeofences failed", t)
203
+ }
204
+ }
205
+ }
206
+
207
+ override fun listGeofences(promise: Promise) {
208
+ scope.launch {
209
+ try {
210
+ val arr: WritableArray = Arguments.createArray()
211
+ for (g in Beekon.listGeofences()) arr.pushMap(geofenceToWire(g))
212
+ promise.resolve(arr)
213
+ } catch (t: Throwable) {
214
+ promise.reject(errorCode(t), t.message ?: "listGeofences failed", t)
90
215
  }
91
216
  }
92
217
  }
@@ -97,54 +222,182 @@ class BeekonRnModule(private val reactContext: ReactApplicationContext) :
97
222
  }
98
223
 
99
224
  // ---------------------------------------------------------------------------
100
- // Mappers (Wire Kotlin types)
225
+ // Mappers: wire (ReadableMap/Array) Kotlin
101
226
  // ---------------------------------------------------------------------------
102
227
 
103
228
  private fun wireToConfig(map: ReadableMap): BeekonConfig {
104
- val intervalSeconds = map.getDouble("intervalSeconds").toLong()
105
- val distanceMeters = map.getDouble("distanceMeters")
106
- val notification = map.getMap("androidNotification")
107
- ?.let { wireToNotification(it) }
108
- ?: BeekonConfig.Notification()
229
+ val sync =
230
+ if (map.hasKey("sync") && !map.isNull("sync")) {
231
+ map.getMap("sync")?.let { wireToSyncConfig(it) }
232
+ } else {
233
+ null
234
+ }
235
+ val notification =
236
+ if (map.hasKey("notification") && !map.isNull("notification")) {
237
+ map.getMap("notification")?.let { wireToNotification(it) }
238
+ } else {
239
+ null
240
+ }
109
241
  return BeekonConfig(
110
- intervalSeconds = intervalSeconds,
111
- distanceMeters = distanceMeters,
112
- notification = notification,
242
+ minTimeBetweenLocationsSeconds =
243
+ map.getDouble("minTimeBetweenLocationsSeconds").toLong(),
244
+ minDistanceBetweenLocationsMeters =
245
+ map.getDouble("minDistanceBetweenLocationsMeters"),
246
+ accuracyMode = toAccuracyMode(map.getString("accuracyMode")),
247
+ whenStationary = toStationaryMode(map.getString("whenStationary")),
248
+ stationaryRadiusMeters = map.getDouble("stationaryRadiusMeters"),
249
+ detectActivity = map.getBoolean("detectActivity"),
250
+ sync = sync,
251
+ notification = notification ?: NotificationConfig(),
252
+ )
253
+ }
254
+
255
+ private fun wireToSyncConfig(map: ReadableMap): SyncConfig {
256
+ val auth =
257
+ if (map.hasKey("auth") && !map.isNull("auth")) {
258
+ map.getMap("auth")?.let { wireToAuthConfig(it) }
259
+ } else {
260
+ null
261
+ }
262
+ return SyncConfig(
263
+ url = map.getString("url") ?: "",
264
+ headers = entriesToMap(map.getArray("headers")),
265
+ intervalSeconds = map.getDouble("intervalSeconds").toLong(),
266
+ batchSize = map.getDouble("batchSize").toInt(),
267
+ auth = auth,
113
268
  )
114
269
  }
115
270
 
116
- private fun wireToNotification(map: ReadableMap): BeekonConfig.Notification {
117
- val title = if (map.hasKey("title") && !map.isNull("title")) map.getString("title") else null
118
- val text = if (map.hasKey("text") && !map.isNull("text")) map.getString("text") else null
119
- val resName =
120
- if (map.hasKey("smallIconResName") && !map.isNull("smallIconResName"))
121
- map.getString("smallIconResName")
122
- else null
123
- val smallIcon: Int? = resName?.let {
124
- val pkg = reactContext.packageName
125
- val id = reactContext.resources.getIdentifier(it, "drawable", pkg)
126
- if (id == 0) {
127
- throw IllegalArgumentException("drawable resource '$it' not found in package '$pkg'")
271
+ private fun wireToAuthConfig(map: ReadableMap): AuthConfig {
272
+ val expiresAt =
273
+ if (map.hasKey("expiresAtMs") && !map.isNull("expiresAtMs")) {
274
+ // Wire carries epoch millis; the native recipe wants epoch seconds.
275
+ (map.getDouble("expiresAtMs") / 1000.0).toLong()
276
+ } else {
277
+ null
128
278
  }
129
- id
279
+ val seedEpoch =
280
+ if (map.hasKey("seedEpoch") && !map.isNull("seedEpoch")) {
281
+ map.getDouble("seedEpoch").toLong()
282
+ } else {
283
+ null
284
+ }
285
+ val responseMapping =
286
+ if (map.hasKey("responseMapping") && !map.isNull("responseMapping")) {
287
+ map.getMap("responseMapping")?.let { wireToResponseMapping(it) } ?: AuthResponseMapping()
288
+ } else {
289
+ AuthResponseMapping()
290
+ }
291
+ return AuthConfig(
292
+ accessToken = optString(map, "accessToken"),
293
+ refreshToken = optString(map, "refreshToken"),
294
+ expiresAt = expiresAt,
295
+ strategy = toAuthStrategy(map.getString("strategy")),
296
+ refreshUrl = optString(map, "refreshUrl"),
297
+ refreshPayload = entriesToMap(map.getArray("refreshPayload")),
298
+ refreshHeaders = entriesToMap(map.getArray("refreshHeaders")),
299
+ refreshBodyFormat = toRefreshBodyFormat(map.getString("refreshBodyFormat")),
300
+ responseMapping = responseMapping,
301
+ skewMarginSeconds = map.getDouble("skewMarginSeconds").toLong(),
302
+ seedEpoch = seedEpoch,
303
+ )
304
+ }
305
+
306
+ private fun wireToResponseMapping(map: ReadableMap): AuthResponseMapping =
307
+ AuthResponseMapping(
308
+ accessToken = optString(map, "accessToken"),
309
+ refreshToken = optString(map, "refreshToken"),
310
+ expiresIn = optString(map, "expiresIn"),
311
+ expiresAt = optString(map, "expiresAt"),
312
+ )
313
+
314
+ private fun optString(map: ReadableMap, key: String): String? =
315
+ if (map.hasKey(key) && !map.isNull(key)) map.getString(key) else null
316
+
317
+ private fun wireToNotification(map: ReadableMap): NotificationConfig {
318
+ val title =
319
+ if (map.hasKey("title") && !map.isNull("title")) map.getString("title") else null
320
+ val text =
321
+ if (map.hasKey("text") && !map.isNull("text")) map.getString("text") else null
322
+ val smallIcon =
323
+ if (map.hasKey("smallIcon") && !map.isNull("smallIcon")) map.getString("smallIcon") else null
324
+ // Use the data-class default for `title` when the wire value is absent.
325
+ return if (title != null) {
326
+ NotificationConfig(title = title, text = text, smallIcon = smallIcon)
327
+ } else {
328
+ NotificationConfig(text = text, smallIcon = smallIcon)
130
329
  }
131
- return BeekonConfig.Notification(title = title, text = text, smallIcon = smallIcon)
132
330
  }
133
331
 
332
+ private fun wireToGeofences(arr: ReadableArray): List<BeekonGeofence> {
333
+ val out = ArrayList<BeekonGeofence>(arr.size())
334
+ for (i in 0 until arr.size()) {
335
+ val m = arr.getMap(i) ?: continue
336
+ out += BeekonGeofence(
337
+ id = m.getString("id") ?: "",
338
+ latitude = m.getDouble("lat"),
339
+ longitude = m.getDouble("lng"),
340
+ radiusMeters = m.getDouble("radiusMeters"),
341
+ notifyOnEntry = m.getBoolean("notifyOnEntry"),
342
+ notifyOnExit = m.getBoolean("notifyOnExit"),
343
+ )
344
+ }
345
+ return out
346
+ }
347
+
348
+ private fun entriesToMap(arr: ReadableArray?): Map<String, String> {
349
+ if (arr == null) return emptyMap()
350
+ val out = LinkedHashMap<String, String>(arr.size())
351
+ for (i in 0 until arr.size()) {
352
+ val e = arr.getMap(i) ?: continue
353
+ val k = e.getString("key") ?: continue
354
+ val v = e.getString("value") ?: continue
355
+ out[k] = v
356
+ }
357
+ return out
358
+ }
359
+
360
+ // ---------------------------------------------------------------------------
361
+ // Mappers: Kotlin → wire (WritableMap)
362
+ // ---------------------------------------------------------------------------
363
+
134
364
  private fun locationToWire(loc: Location): WritableMap {
135
365
  val m = Arguments.createMap()
136
- m.putDouble("lat", loc.lat)
137
- m.putDouble("lng", loc.lng)
366
+ m.putString("id", loc.id)
367
+ m.putDouble("lat", loc.latitude)
368
+ m.putDouble("lng", loc.longitude)
138
369
  m.putDouble("timestampMs", loc.timestamp.toEpochMilli().toDouble())
139
370
  putNullableDouble(m, "accuracy", loc.accuracy)
140
371
  putNullableDouble(m, "speed", loc.speed)
141
372
  putNullableDouble(m, "bearing", loc.bearing)
142
373
  putNullableDouble(m, "altitude", loc.altitude)
374
+ m.putString("quality", qualityToWire(loc.quality))
375
+ m.putString("trigger", triggerToWire(loc.trigger))
376
+ m.putString("motion", motionToWire(loc.motion))
377
+ val activity = loc.activity
378
+ if (activity != null) m.putString("activity", activityToWire(activity)) else m.putNull("activity")
379
+ m.putBoolean("isMock", loc.isMock)
143
380
  return m
144
381
  }
145
382
 
146
- private fun putNullableDouble(map: WritableMap, key: String, value: Double?) {
147
- if (value == null) map.putNull(key) else map.putDouble(key, value)
383
+ private fun geofenceToWire(g: BeekonGeofence): WritableMap {
384
+ val m = Arguments.createMap()
385
+ m.putString("id", g.id)
386
+ m.putDouble("lat", g.latitude)
387
+ m.putDouble("lng", g.longitude)
388
+ m.putDouble("radiusMeters", g.radiusMeters)
389
+ m.putBoolean("notifyOnEntry", g.notifyOnEntry)
390
+ m.putBoolean("notifyOnExit", g.notifyOnExit)
391
+ return m
392
+ }
393
+
394
+ private fun geofenceEventToWire(e: GeofenceEvent): WritableMap {
395
+ val m = Arguments.createMap()
396
+ m.putString("id", e.id)
397
+ m.putString("geofenceId", e.geofenceId)
398
+ m.putString("type", transitionToWire(e.type))
399
+ m.putDouble("timestampMs", e.timestamp.toEpochMilli().toDouble())
400
+ return m
148
401
  }
149
402
 
150
403
  private fun stateToWire(s: BeekonState): WritableMap {
@@ -160,19 +413,118 @@ class BeekonRnModule(private val reactContext: ReactApplicationContext) :
160
413
  return m
161
414
  }
162
415
 
416
+ private fun syncStatusToWire(s: SyncStatus): WritableMap {
417
+ val m = Arguments.createMap()
418
+ when (s) {
419
+ SyncStatus.Idle -> m.putString("type", "idle")
420
+ SyncStatus.Pending -> m.putString("type", "pending")
421
+ is SyncStatus.Failed -> {
422
+ m.putString("type", "failed")
423
+ m.putString("failure", syncFailureToWire(s.reason))
424
+ }
425
+ }
426
+ return m
427
+ }
428
+
429
+ private fun tokenRefreshToWire(t: AuthTokens): WritableMap {
430
+ val m = Arguments.createMap()
431
+ m.putString("accessToken", t.accessToken)
432
+ val refreshToken = t.refreshToken
433
+ if (refreshToken != null) m.putString("refreshToken", refreshToken) else m.putNull("refreshToken")
434
+ val expiresAt = t.expiresAt
435
+ // Native epoch seconds → wire epoch millis.
436
+ if (expiresAt != null) m.putDouble("expiresAtMs", (expiresAt * 1000L).toDouble()) else m.putNull("expiresAtMs")
437
+ m.putDouble("epoch", t.epoch.toDouble())
438
+ return m
439
+ }
440
+
441
+ private fun putNullableDouble(map: WritableMap, key: String, value: Double?) {
442
+ if (value == null) map.putNull(key) else map.putDouble(key, value)
443
+ }
444
+
445
+ // ---------------------------------------------------------------------------
446
+ // Enum mappers
447
+ // ---------------------------------------------------------------------------
448
+
449
+ private fun toAccuracyMode(s: String?): AccuracyMode = when (s) {
450
+ "high" -> AccuracyMode.High
451
+ "low" -> AccuracyMode.Low
452
+ else -> AccuracyMode.Balanced
453
+ }
454
+
455
+ private fun toStationaryMode(s: String?): StationaryMode = when (s) {
456
+ "keepTracking" -> StationaryMode.KeepTracking
457
+ "pauseWithCheckIns" -> StationaryMode.PauseWithCheckIns
458
+ else -> StationaryMode.Pause
459
+ }
460
+
461
+ private fun toAuthStrategy(s: String?): AuthStrategy = when (s) {
462
+ "raw" -> AuthStrategy.Raw
463
+ else -> AuthStrategy.Bearer
464
+ }
465
+
466
+ private fun toRefreshBodyFormat(s: String?): AuthBodyFormat = when (s) {
467
+ "json" -> AuthBodyFormat.Json
468
+ else -> AuthBodyFormat.Form
469
+ }
470
+
163
471
  private fun stopReasonToWire(r: StopReason): String = when (r) {
164
472
  StopReason.User -> "user"
165
473
  StopReason.PermissionDenied -> "permissionDenied"
166
474
  StopReason.LocationServicesDisabled -> "locationServicesDisabled"
475
+ StopReason.LocationUnavailable -> "locationUnavailable"
167
476
  StopReason.System -> "system"
168
477
  }
169
478
 
170
- // Maps native error sealed types to stable JS-side codes. Same code strings
171
- // on iOS so JS can switch on them platform-agnostically.
479
+ private fun syncFailureToWire(f: SyncFailure): String = when (f) {
480
+ SyncFailure.Auth -> "auth"
481
+ SyncFailure.Rejected -> "rejected"
482
+ }
483
+
484
+ private fun qualityToWire(q: LocationQuality): String = when (q) {
485
+ LocationQuality.Ok -> "ok"
486
+ LocationQuality.LowAccuracy -> "lowAccuracy"
487
+ LocationQuality.ImplausibleSpeed -> "implausibleSpeed"
488
+ }
489
+
490
+ private fun triggerToWire(t: LocationTrigger): String = when (t) {
491
+ LocationTrigger.Interval -> "interval"
492
+ LocationTrigger.Motion -> "motion"
493
+ LocationTrigger.CheckIn -> "checkIn"
494
+ LocationTrigger.Geofence -> "geofence"
495
+ LocationTrigger.Manual -> "manual"
496
+ }
497
+
498
+ private fun motionToWire(m: MotionState): String = when (m) {
499
+ MotionState.Moving -> "moving"
500
+ MotionState.Stationary -> "stationary"
501
+ MotionState.Unknown -> "unknown"
502
+ }
503
+
504
+ private fun activityToWire(a: ActivityType): String = when (a) {
505
+ ActivityType.Stationary -> "stationary"
506
+ ActivityType.Walking -> "walking"
507
+ ActivityType.Running -> "running"
508
+ ActivityType.Cycling -> "cycling"
509
+ ActivityType.Automotive -> "automotive"
510
+ ActivityType.Unknown -> "unknown"
511
+ }
512
+
513
+ private fun transitionToWire(t: Transition): String = when (t) {
514
+ Transition.Enter -> "enter"
515
+ Transition.Exit -> "exit"
516
+ }
517
+
518
+ // Maps native exceptions to stable JS-side codes, shared with iOS so JS can
519
+ // switch on them platform-agnostically.
172
520
  private fun errorCode(t: Throwable): String = when (t) {
173
- is BeekonError.PermissionDenied -> "PERMISSION_DENIED"
174
- is BeekonError.LocationServicesDisabled -> "LOCATION_SERVICES_DISABLED"
175
- is BeekonError.StorageFailure -> "STORAGE_FAILURE"
521
+ is BeekonException.StorageException -> "STORAGE_FAILURE"
522
+ is BeekonException.InvalidGeofence -> "INVALID_GEOFENCE"
523
+ is BeekonException.LocationUnavailable -> when (t.reason) {
524
+ LocationUnavailableReason.PermissionDenied -> "PERMISSION_DENIED"
525
+ LocationUnavailableReason.LocationServicesDisabled -> "LOCATION_SERVICES_DISABLED"
526
+ LocationUnavailableReason.Unavailable -> "LOCATION_UNAVAILABLE"
527
+ }
176
528
  else -> "INTERNAL_ERROR"
177
529
  }
178
530