@wayq/beekon-rn 0.0.3 → 0.0.5

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 (77) hide show
  1. package/BeekonRn.podspec +1 -1
  2. package/README.md +91 -39
  3. package/android/build.gradle +9 -4
  4. package/android/src/main/java/in/wayq/beekonrn/BeekonRnModule.kt +306 -60
  5. package/ios/BeekonRn.mm +90 -24
  6. package/ios/BeekonRn.swift +360 -60
  7. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/BeekonKit +0 -0
  8. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Info.plist +0 -0
  9. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.abi.json +7521 -1312
  10. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  11. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.swiftinterface +191 -40
  12. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/BeekonKit +0 -0
  13. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Info.plist +0 -0
  14. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.abi.json +7521 -1312
  15. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  16. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +191 -40
  17. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +7521 -1312
  18. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  19. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +191 -40
  20. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/_CodeSignature/CodeResources +2 -80
  21. package/lib/module/NativeBeekonRn.js +22 -7
  22. package/lib/module/NativeBeekonRn.js.map +1 -1
  23. package/lib/module/beekon.js +198 -46
  24. package/lib/module/beekon.js.map +1 -1
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/internal/mappers.js +122 -28
  27. package/lib/module/internal/mappers.js.map +1 -1
  28. package/lib/module/types/config.js +2 -0
  29. package/lib/module/types/enums.js +2 -0
  30. package/lib/module/types/enums.js.map +1 -0
  31. package/lib/module/types/error.js +10 -4
  32. package/lib/module/types/error.js.map +1 -1
  33. package/lib/module/types/geofence.js +2 -0
  34. package/lib/module/types/geofence.js.map +1 -0
  35. package/lib/module/types/location.js +2 -0
  36. package/lib/module/types/sync.js +2 -0
  37. package/lib/module/types/sync.js.map +1 -0
  38. package/lib/typescript/src/NativeBeekonRn.d.ts +102 -20
  39. package/lib/typescript/src/NativeBeekonRn.d.ts.map +1 -1
  40. package/lib/typescript/src/beekon.d.ts +81 -33
  41. package/lib/typescript/src/beekon.d.ts.map +1 -1
  42. package/lib/typescript/src/index.d.ts +5 -2
  43. package/lib/typescript/src/index.d.ts.map +1 -1
  44. package/lib/typescript/src/internal/mappers.d.ts +12 -6
  45. package/lib/typescript/src/internal/mappers.d.ts.map +1 -1
  46. package/lib/typescript/src/types/config.d.ts +50 -20
  47. package/lib/typescript/src/types/config.d.ts.map +1 -1
  48. package/lib/typescript/src/types/enums.d.ts +48 -0
  49. package/lib/typescript/src/types/enums.d.ts.map +1 -0
  50. package/lib/typescript/src/types/error.d.ts +11 -5
  51. package/lib/typescript/src/types/error.d.ts.map +1 -1
  52. package/lib/typescript/src/types/geofence.d.ts +36 -0
  53. package/lib/typescript/src/types/geofence.d.ts.map +1 -0
  54. package/lib/typescript/src/types/location.d.ts +22 -8
  55. package/lib/typescript/src/types/location.d.ts.map +1 -1
  56. package/lib/typescript/src/types/state.d.ts +13 -4
  57. package/lib/typescript/src/types/state.d.ts.map +1 -1
  58. package/lib/typescript/src/types/sync.d.ts +27 -0
  59. package/lib/typescript/src/types/sync.d.ts.map +1 -0
  60. package/package.json +4 -5
  61. package/scripts/fetch-beekonkit.sh +5 -5
  62. package/src/NativeBeekonRn.ts +110 -20
  63. package/src/beekon.ts +219 -49
  64. package/src/index.tsx +21 -2
  65. package/src/internal/mappers.ts +187 -30
  66. package/src/types/config.ts +52 -20
  67. package/src/types/enums.ts +64 -0
  68. package/src/types/error.ts +11 -8
  69. package/src/types/geofence.ts +37 -0
  70. package/src/types/location.ts +28 -8
  71. package/src/types/state.ts +13 -3
  72. package/src/types/sync.ts +23 -0
  73. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeDirectory +0 -0
  74. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeRequirements +0 -0
  75. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeResources +0 -296
  76. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeSignature +0 -0
  77. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/_CodeSignature/CodeResources +0 -146
@@ -1,33 +1,43 @@
1
1
  import Foundation
2
2
  import BeekonKit
3
3
 
4
- /// Swift impl of the Beekon RN TurboModule. Bridges the actor-based native
5
- /// SDK (`BeekonKit.Beekon.shared`) to the ObjC TurboModule conformance in
4
+ /// Swift impl of the Beekon RN TurboModule. Bridges the actor-based native SDK
5
+ /// (`BeekonKit.Beekon.shared`) to the ObjC TurboModule conformance in
6
6
  /// `BeekonRn.mm`. All public-facing methods take ObjC-compatible callbacks
7
- /// (resolver/rejecter) and dictionaries so they can be invoked from `.mm`.
7
+ /// (resolver/rejecter) and dictionaries/arrays so they can be invoked from `.mm`.
8
8
  ///
9
- /// Event delivery: `BeekonRn.mm` constructs this with two closures that call
10
- /// the codegen-generated `emitOnState:` / `emitOnLocation:` ObjC methods on
11
- /// the spec; the closures are invoked from per-stream `Task`s started during
12
- /// init.
13
- // `@unchecked Sendable` is correct here: `onStateCb`/`onLocationCb` are
14
- // immutable closures, and `stateTask`/`locationsTask` are mutated only from
15
- // init/stopCollectors which run sequentially. NSObject can't be auto-Sendable
16
- // and `@objc` rules out `actor`, so this is the path.
9
+ /// Event delivery: `BeekonRn.mm` constructs this with four closures that call
10
+ /// the codegen `emitOnState:` / `emitOnLocation:` / `emitOnGeofenceEvent:` /
11
+ /// `emitOnSyncStatus:` ObjC methods; the closures are invoked from per-stream
12
+ /// `Task`s started during init.
13
+ //
14
+ // `@unchecked Sendable` is correct here: the four emit closures are immutable
15
+ // `let`s, and the stream `Task`s are mutated only from init/invalidate which run
16
+ // sequentially. NSObject can't be auto-Sendable and `@objc` rules out `actor`,
17
+ // so this is the path. Non-Sendable wire inputs (NSDictionary/NSArray) are
18
+ // converted to Sendable Beekon types *before* crossing into any `Task`.
17
19
  @objc public final class BeekonRnImpl: NSObject, @unchecked Sendable {
18
20
 
19
21
  private let onStateCb: (NSDictionary) -> Void
20
22
  private let onLocationCb: (NSDictionary) -> Void
23
+ private let onGeofenceEventCb: (NSDictionary) -> Void
24
+ private let onSyncStatusCb: (NSDictionary) -> Void
21
25
 
22
26
  private var stateTask: Task<Void, Never>?
23
27
  private var locationsTask: Task<Void, Never>?
28
+ private var geofenceEventsTask: Task<Void, Never>?
29
+ private var syncStatusTask: Task<Void, Never>?
24
30
 
25
31
  @objc public init(
26
32
  onState: @escaping (NSDictionary) -> Void,
27
- onLocation: @escaping (NSDictionary) -> Void
33
+ onLocation: @escaping (NSDictionary) -> Void,
34
+ onGeofenceEvent: @escaping (NSDictionary) -> Void,
35
+ onSyncStatus: @escaping (NSDictionary) -> Void
28
36
  ) {
29
37
  self.onStateCb = onState
30
38
  self.onLocationCb = onLocation
39
+ self.onGeofenceEventCb = onGeofenceEvent
40
+ self.onSyncStatusCb = onSyncStatus
31
41
  super.init()
32
42
  self.stateTask = Task { [weak self] in
33
43
  guard let self = self else { return }
@@ -41,13 +51,38 @@ import BeekonKit
41
51
  self.onLocationCb(self.locationToWire(loc))
42
52
  }
43
53
  }
54
+ self.geofenceEventsTask = Task { [weak self] in
55
+ guard let self = self else { return }
56
+ for await event in await Beekon.shared.geofenceEvents {
57
+ self.onGeofenceEventCb(self.geofenceEventToWire(event))
58
+ }
59
+ }
60
+ self.syncStatusTask = Task { [weak self] in
61
+ guard let self = self else { return }
62
+ for await status in await Beekon.shared.syncStatus {
63
+ self.onSyncStatusCb(self.syncStatusToWire(status))
64
+ }
65
+ }
44
66
  }
45
67
 
46
68
  @objc public func invalidate() {
47
69
  stateTask?.cancel()
48
70
  locationsTask?.cancel()
71
+ geofenceEventsTask?.cancel()
72
+ syncStatusTask?.cancel()
49
73
  stateTask = nil
50
74
  locationsTask = nil
75
+ geofenceEventsTask = nil
76
+ syncStatusTask = nil
77
+ }
78
+
79
+ /// Register Beekon's background-refresh task and install cold-launch hooks.
80
+ /// Must be called once, synchronously, during app launch (before
81
+ /// `didFinishLaunchingWithOptions` returns) — invoke from the host AppDelegate.
82
+ @objc public static func registerBackgroundTasks() {
83
+ Beekon.registerBackgroundTasks()
84
+ // Touch the shared actor so its cold-launch resume hooks install.
85
+ _ = Beekon.shared
51
86
  }
52
87
 
53
88
  // MARK: - Lifecycle
@@ -55,17 +90,11 @@ import BeekonKit
55
90
  @objc public func configure(
56
91
  _ config: NSDictionary,
57
92
  resolver resolve: @escaping @Sendable (Any?) -> Void,
58
- rejecter reject: @escaping @Sendable (String?, String?, Error?) -> Void
93
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
59
94
  ) {
60
95
  // Convert the non-Sendable NSDictionary to a Sendable BeekonConfig before
61
96
  // crossing into the Task closure.
62
- let cfg: BeekonConfig
63
- do {
64
- cfg = try wireToConfig(config)
65
- } catch {
66
- reject(errorCode(error), error.localizedDescription, error)
67
- return
68
- }
97
+ let cfg = wireToConfig(config)
69
98
  Task {
70
99
  await Beekon.shared.configure(cfg)
71
100
  resolve(nil)
@@ -74,88 +103,266 @@ import BeekonKit
74
103
 
75
104
  @objc public func startWithResolver(
76
105
  _ resolve: @escaping @Sendable (Any?) -> Void,
106
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
107
+ ) {
108
+ // start() never throws — outcome surfaces on the `state` stream.
109
+ Task {
110
+ await Beekon.shared.start()
111
+ resolve(nil)
112
+ }
113
+ }
114
+
115
+ @objc public func stopWithResolver(
116
+ _ resolve: @escaping @Sendable (Any?) -> Void,
117
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
118
+ ) {
119
+ Task {
120
+ await Beekon.shared.stop()
121
+ resolve(nil)
122
+ }
123
+ }
124
+
125
+ @objc public func resumeIfNeededWithResolver(
126
+ _ resolve: @escaping @Sendable (Any?) -> Void,
127
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
128
+ ) {
129
+ Task {
130
+ await Beekon.shared.resumeIfNeeded()
131
+ resolve(nil)
132
+ }
133
+ }
134
+
135
+ // MARK: - History
136
+
137
+ @objc public func getLocationsFromMs(
138
+ _ fromMs: Double,
139
+ toMs: Double,
140
+ resolver resolve: @escaping @Sendable (Any?) -> Void,
77
141
  rejecter reject: @escaping @Sendable (String?, String?, Error?) -> Void
78
142
  ) {
79
143
  Task { [weak self] in
80
144
  guard let self = self else { return }
81
145
  do {
82
- try await Beekon.shared.start()
83
- resolve(nil)
146
+ let from = Date(timeIntervalSince1970: fromMs / 1000.0)
147
+ let to = Date(timeIntervalSince1970: toMs / 1000.0)
148
+ let locations = try await Beekon.shared.getLocations(from: from, to: to)
149
+ resolve(locations.map { self.locationToWire($0) })
84
150
  } catch {
85
151
  reject(self.errorCode(error), error.localizedDescription, error)
86
152
  }
87
153
  }
88
154
  }
89
155
 
90
- @objc public func stopWithResolver(
156
+ @objc public func deleteLocationsBeforeMs(
157
+ _ beforeMs: Double,
158
+ resolver resolve: @escaping @Sendable (Any?) -> Void,
159
+ rejecter reject: @escaping @Sendable (String?, String?, Error?) -> Void
160
+ ) {
161
+ Task { [weak self] in
162
+ guard let self = self else { return }
163
+ do {
164
+ // Negative is the wire sentinel for "delete all" (no cutoff).
165
+ let before: Date? =
166
+ beforeMs < 0 ? nil : Date(timeIntervalSince1970: beforeMs / 1000.0)
167
+ let count = try await Beekon.shared.deleteLocations(before: before)
168
+ resolve(count)
169
+ } catch {
170
+ reject(self.errorCode(error), error.localizedDescription, error)
171
+ }
172
+ }
173
+ }
174
+
175
+ @objc public func pendingUploadCountWithResolver(
91
176
  _ resolve: @escaping @Sendable (Any?) -> Void,
92
177
  rejecter reject: @escaping @Sendable (String?, String?, Error?) -> Void
178
+ ) {
179
+ Task { [weak self] in
180
+ guard let self = self else { return }
181
+ do {
182
+ let count = try await Beekon.shared.pendingUploadCount()
183
+ resolve(count)
184
+ } catch {
185
+ reject(self.errorCode(error), error.localizedDescription, error)
186
+ }
187
+ }
188
+ }
189
+
190
+ // MARK: - Sync
191
+
192
+ @objc public func syncWithResolver(
193
+ _ resolve: @escaping @Sendable (Any?) -> Void,
194
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
93
195
  ) {
94
196
  Task {
95
- await Beekon.shared.stop()
197
+ await Beekon.shared.sync()
96
198
  resolve(nil)
97
199
  }
98
200
  }
99
201
 
100
- @objc public func historyFromMs(
101
- _ fromMs: Double,
102
- toMs: Double,
202
+ @objc public func setExtras(
203
+ _ entries: NSArray,
204
+ resolver resolve: @escaping @Sendable (Any?) -> Void,
205
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
206
+ ) {
207
+ let extras = entriesToDict(entries)
208
+ Task {
209
+ await Beekon.shared.setExtras(extras)
210
+ resolve(nil)
211
+ }
212
+ }
213
+
214
+ // MARK: - Geofences
215
+
216
+ @objc public func addGeofences(
217
+ _ geofences: NSArray,
103
218
  resolver resolve: @escaping @Sendable (Any?) -> Void,
104
219
  rejecter reject: @escaping @Sendable (String?, String?, Error?) -> Void
105
220
  ) {
221
+ let list = wireToGeofences(geofences)
106
222
  Task { [weak self] in
107
223
  guard let self = self else { return }
108
224
  do {
109
- let from = Date(timeIntervalSince1970: fromMs / 1000.0)
110
- let to = Date(timeIntervalSince1970: toMs / 1000.0)
111
- // Function-form overload of `locations` — the property is the live
112
- // stream, the function is the historical fetch. Swift resolves on
113
- // signature.
114
- let locations = try await Beekon.shared.locations(from: from, to: to)
115
- let arr = locations.map { self.locationToWire($0) }
116
- resolve(arr)
225
+ try await Beekon.shared.addGeofences(list)
226
+ resolve(nil)
117
227
  } catch {
118
228
  reject(self.errorCode(error), error.localizedDescription, error)
119
229
  }
120
230
  }
121
231
  }
122
232
 
123
- // MARK: - Mappers
233
+ @objc public func removeGeofences(
234
+ _ ids: NSArray,
235
+ resolver resolve: @escaping @Sendable (Any?) -> Void,
236
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
237
+ ) {
238
+ let list = ids.compactMap { $0 as? String }
239
+ Task {
240
+ await Beekon.shared.removeGeofences(ids: list)
241
+ resolve(nil)
242
+ }
243
+ }
244
+
245
+ @objc public func listGeofencesWithResolver(
246
+ _ resolve: @escaping @Sendable (Any?) -> Void,
247
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
248
+ ) {
249
+ Task { [weak self] in
250
+ guard let self = self else { return }
251
+ let geofences = await Beekon.shared.listGeofences()
252
+ resolve(geofences.map { self.geofenceToWire($0) })
253
+ }
254
+ }
255
+
256
+ // MARK: - Mappers: wire (NSDictionary/NSArray) → Beekon
124
257
 
125
- private func wireToConfig(_ d: NSDictionary) throws -> BeekonConfig {
126
- // Wire defaults are applied by the TS facade — both fields are required at
127
- // the wire level, so missing keys are a programmer error.
128
- guard
129
- let intervalSeconds = (d["intervalSeconds"] as? NSNumber)?.doubleValue,
130
- let distanceMeters = (d["distanceMeters"] as? NSNumber)?.doubleValue
131
- else {
132
- throw NSError(
133
- domain: "BeekonRn",
134
- code: -1,
135
- userInfo: [NSLocalizedDescriptionKey: "intervalSeconds/distanceMeters missing"]
258
+ private func wireToConfig(_ d: NSDictionary) -> BeekonConfig {
259
+ let minTime =
260
+ (d["minTimeBetweenLocationsSeconds"] as? NSNumber)?.doubleValue ?? 30
261
+ let minDist =
262
+ (d["minDistanceBetweenLocationsMeters"] as? NSNumber)?.doubleValue ?? 100
263
+ let stationaryRadius =
264
+ (d["stationaryRadiusMeters"] as? NSNumber)?.doubleValue ?? 5
265
+ let detectActivity = (d["detectActivity"] as? NSNumber)?.boolValue ?? false
266
+
267
+ var sync: SyncConfig?
268
+ if let s = d["sync"] as? NSDictionary,
269
+ let urlStr = s["url"] as? String,
270
+ let url = URL(string: urlStr) {
271
+ sync = SyncConfig(
272
+ url: url,
273
+ headers: entriesToDict((s["headers"] as? NSArray) ?? []),
274
+ intervalSeconds: (s["intervalSeconds"] as? NSNumber)?.doubleValue ?? 300,
275
+ batchSize: (s["batchSize"] as? NSNumber)?.intValue ?? 100
136
276
  )
137
277
  }
278
+
279
+ // `notification` is Android-only — iOS ignores it.
138
280
  return BeekonConfig(
139
- intervalSeconds: intervalSeconds,
140
- distanceMeters: distanceMeters
281
+ minTimeBetweenLocationsSeconds: minTime,
282
+ minDistanceBetweenLocationsMeters: minDist,
283
+ accuracyMode: accuracyModeFromWire(d["accuracyMode"] as? String),
284
+ whenStationary: stationaryModeFromWire(d["whenStationary"] as? String),
285
+ stationaryRadiusMeters: stationaryRadius,
286
+ detectActivity: detectActivity,
287
+ sync: sync
141
288
  )
142
- // androidNotification ignored on iOS.
143
289
  }
144
290
 
291
+ private func wireToGeofences(_ arr: NSArray) -> [BeekonGeofence] {
292
+ var out: [BeekonGeofence] = []
293
+ for case let m as NSDictionary in arr {
294
+ guard
295
+ let id = m["id"] as? String,
296
+ let lat = (m["lat"] as? NSNumber)?.doubleValue,
297
+ let lng = (m["lng"] as? NSNumber)?.doubleValue,
298
+ let radius = (m["radiusMeters"] as? NSNumber)?.doubleValue
299
+ else { continue }
300
+ out.append(
301
+ BeekonGeofence(
302
+ id: id,
303
+ latitude: lat,
304
+ longitude: lng,
305
+ radiusMeters: radius,
306
+ notifyOnEntry: (m["notifyOnEntry"] as? NSNumber)?.boolValue ?? true,
307
+ notifyOnExit: (m["notifyOnExit"] as? NSNumber)?.boolValue ?? true
308
+ )
309
+ )
310
+ }
311
+ return out
312
+ }
313
+
314
+ private func entriesToDict(_ arr: NSArray) -> [String: String] {
315
+ var out: [String: String] = [:]
316
+ for case let e as NSDictionary in arr {
317
+ if let k = e["key"] as? String, let v = e["value"] as? String {
318
+ out[k] = v
319
+ }
320
+ }
321
+ return out
322
+ }
323
+
324
+ // MARK: - Mappers: Beekon → wire (NSDictionary)
325
+
145
326
  private func locationToWire(_ loc: Location) -> NSDictionary {
146
- // NSDictionary literals can't carry `nil` values, so optionals collapse
147
- // to `NSNull` — the JS Codegen layer translates that back to `null`.
327
+ // NSDictionary literals can't carry `nil`, so optionals collapse to
328
+ // `NSNull` — the JS Codegen layer translates that back to `null`.
148
329
  let d = NSMutableDictionary()
149
- d["lat"] = loc.lat
150
- d["lng"] = loc.lng
330
+ d["id"] = loc.id
331
+ d["lat"] = loc.latitude
332
+ d["lng"] = loc.longitude
151
333
  d["timestampMs"] = loc.timestamp.timeIntervalSince1970 * 1000.0
152
334
  d["accuracy"] = loc.accuracy.map { $0 as Any } ?? NSNull()
153
335
  d["speed"] = loc.speed.map { $0 as Any } ?? NSNull()
154
336
  d["bearing"] = loc.bearing.map { $0 as Any } ?? NSNull()
155
337
  d["altitude"] = loc.altitude.map { $0 as Any } ?? NSNull()
338
+ d["quality"] = qualityToWire(loc.quality)
339
+ d["trigger"] = triggerToWire(loc.trigger)
340
+ d["motion"] = motionToWire(loc.motion)
341
+ d["activity"] = loc.activity.map { activityToWire($0) as Any } ?? NSNull()
342
+ d["isMock"] = loc.isMock
156
343
  return d
157
344
  }
158
345
 
346
+ private func geofenceToWire(_ g: BeekonGeofence) -> NSDictionary {
347
+ return [
348
+ "id": g.id,
349
+ "lat": g.latitude,
350
+ "lng": g.longitude,
351
+ "radiusMeters": g.radiusMeters,
352
+ "notifyOnEntry": g.notifyOnEntry,
353
+ "notifyOnExit": g.notifyOnExit,
354
+ ]
355
+ }
356
+
357
+ private func geofenceEventToWire(_ e: GeofenceEvent) -> NSDictionary {
358
+ return [
359
+ "id": e.id,
360
+ "geofenceId": e.geofenceId,
361
+ "type": transitionToWire(e.type),
362
+ "timestampMs": e.timestamp.timeIntervalSince1970 * 1000.0,
363
+ ]
364
+ }
365
+
159
366
  private func stateToWire(_ s: BeekonState) -> NSDictionary {
160
367
  switch s {
161
368
  case .idle:
@@ -164,6 +371,40 @@ import BeekonKit
164
371
  return ["type": "tracking"]
165
372
  case .stopped(let reason):
166
373
  return ["type": "stopped", "stopReason": stopReasonToWire(reason)]
374
+ // BeekonKit enums are non-frozen (library-evolution binary); handle unknowns.
375
+ @unknown default:
376
+ return ["type": "idle"]
377
+ }
378
+ }
379
+
380
+ private func syncStatusToWire(_ s: SyncStatus) -> NSDictionary {
381
+ switch s {
382
+ case .idle:
383
+ return ["type": "idle"]
384
+ case .pending:
385
+ return ["type": "pending"]
386
+ case .failed(let reason):
387
+ return ["type": "failed", "failure": syncFailureToWire(reason)]
388
+ @unknown default:
389
+ return ["type": "idle"]
390
+ }
391
+ }
392
+
393
+ // MARK: - Enum mappers
394
+
395
+ private func accuracyModeFromWire(_ s: String?) -> AccuracyMode {
396
+ switch s {
397
+ case "high": return .high
398
+ case "low": return .low
399
+ default: return .balanced
400
+ }
401
+ }
402
+
403
+ private func stationaryModeFromWire(_ s: String?) -> StationaryMode {
404
+ switch s {
405
+ case "keepTracking": return .keepTracking
406
+ case "pauseWithCheckIns": return .pauseWithCheckIns
407
+ default: return .pause
167
408
  }
168
409
  }
169
410
 
@@ -172,18 +413,77 @@ import BeekonKit
172
413
  case .user: return "user"
173
414
  case .permissionDenied: return "permissionDenied"
174
415
  case .locationServicesDisabled: return "locationServicesDisabled"
416
+ case .locationUnavailable: return "locationUnavailable"
417
+ case .system: return "system"
418
+ @unknown default: return "system"
419
+ }
420
+ }
421
+
422
+ private func syncFailureToWire(_ f: SyncFailure) -> String {
423
+ switch f {
424
+ case .auth: return "auth"
425
+ case .rejected: return "rejected"
426
+ @unknown default: return "rejected"
427
+ }
428
+ }
429
+
430
+ private func qualityToWire(_ q: LocationQuality) -> String {
431
+ switch q {
432
+ case .ok: return "ok"
433
+ case .lowAccuracy: return "lowAccuracy"
434
+ case .implausibleSpeed: return "implausibleSpeed"
435
+ @unknown default: return "ok"
436
+ }
437
+ }
438
+
439
+ private func triggerToWire(_ t: LocationTrigger) -> String {
440
+ switch t {
441
+ case .interval: return "interval"
442
+ case .motion: return "motion"
443
+ case .checkIn: return "checkIn"
444
+ case .geofence: return "geofence"
445
+ case .manual: return "manual"
446
+ @unknown default: return "interval"
447
+ }
448
+ }
449
+
450
+ private func motionToWire(_ m: MotionState) -> String {
451
+ switch m {
452
+ case .moving: return "moving"
453
+ case .stationary: return "stationary"
454
+ case .unknown: return "unknown"
455
+ @unknown default: return "unknown"
456
+ }
457
+ }
458
+
459
+ private func activityToWire(_ a: ActivityType) -> String {
460
+ switch a {
461
+ case .stationary: return "stationary"
462
+ case .walking: return "walking"
463
+ case .running: return "running"
464
+ case .cycling: return "cycling"
465
+ case .automotive: return "automotive"
466
+ case .unknown: return "unknown"
467
+ @unknown default: return "unknown"
468
+ }
469
+ }
470
+
471
+ private func transitionToWire(_ t: Transition) -> String {
472
+ switch t {
473
+ case .enter: return "enter"
474
+ case .exit: return "exit"
475
+ @unknown default: return "enter"
175
476
  }
176
477
  }
177
478
 
178
- /// Maps native `BeekonError` cases to stable string codes shared with
179
- /// Android. JS-side error handling can switch on these without per-platform
180
- /// branches.
479
+ /// Maps native `BeekonError` cases to stable string codes shared with Android,
480
+ /// so JS-side error handling can switch on them without per-platform branches.
181
481
  private func errorCode(_ e: Error) -> String {
182
482
  if let be = e as? BeekonError {
183
483
  switch be {
184
- case .permissionDenied: return "PERMISSION_DENIED"
185
- case .locationServicesDisabled: return "LOCATION_SERVICES_DISABLED"
186
- case .storageFailure: return "STORAGE_FAILURE"
484
+ case .storage: return "STORAGE_FAILURE"
485
+ case .invalidGeofence: return "INVALID_GEOFENCE"
486
+ @unknown default: return "INTERNAL_ERROR"
187
487
  }
188
488
  }
189
489
  return "INTERNAL_ERROR"