@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
@@ -1,33 +1,47 @@
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
25
+ private let onAuthTokensCb: (NSDictionary) -> Void
21
26
 
22
27
  private var stateTask: Task<Void, Never>?
23
28
  private var locationsTask: Task<Void, Never>?
29
+ private var geofenceEventsTask: Task<Void, Never>?
30
+ private var syncStatusTask: Task<Void, Never>?
31
+ private var authTokensTask: Task<Void, Never>?
24
32
 
25
33
  @objc public init(
26
34
  onState: @escaping (NSDictionary) -> Void,
27
- onLocation: @escaping (NSDictionary) -> Void
35
+ onLocation: @escaping (NSDictionary) -> Void,
36
+ onGeofenceEvent: @escaping (NSDictionary) -> Void,
37
+ onSyncStatus: @escaping (NSDictionary) -> Void,
38
+ onAuthTokens: @escaping (NSDictionary) -> Void
28
39
  ) {
29
40
  self.onStateCb = onState
30
41
  self.onLocationCb = onLocation
42
+ self.onGeofenceEventCb = onGeofenceEvent
43
+ self.onSyncStatusCb = onSyncStatus
44
+ self.onAuthTokensCb = onAuthTokens
31
45
  super.init()
32
46
  self.stateTask = Task { [weak self] in
33
47
  guard let self = self else { return }
@@ -41,13 +55,46 @@ import BeekonKit
41
55
  self.onLocationCb(self.locationToWire(loc))
42
56
  }
43
57
  }
58
+ self.geofenceEventsTask = Task { [weak self] in
59
+ guard let self = self else { return }
60
+ for await event in await Beekon.shared.geofenceEvents {
61
+ self.onGeofenceEventCb(self.geofenceEventToWire(event))
62
+ }
63
+ }
64
+ self.syncStatusTask = Task { [weak self] in
65
+ guard let self = self else { return }
66
+ for await status in await Beekon.shared.syncStatus {
67
+ self.onSyncStatusCb(self.syncStatusToWire(status))
68
+ }
69
+ }
70
+ self.authTokensTask = Task { [weak self] in
71
+ guard let self = self else { return }
72
+ for await tokens in await Beekon.shared.authChanges {
73
+ self.onAuthTokensCb(self.authTokensToWire(tokens))
74
+ }
75
+ }
44
76
  }
45
77
 
46
78
  @objc public func invalidate() {
47
79
  stateTask?.cancel()
48
80
  locationsTask?.cancel()
81
+ geofenceEventsTask?.cancel()
82
+ syncStatusTask?.cancel()
83
+ authTokensTask?.cancel()
49
84
  stateTask = nil
50
85
  locationsTask = nil
86
+ geofenceEventsTask = nil
87
+ syncStatusTask = nil
88
+ authTokensTask = nil
89
+ }
90
+
91
+ /// Register Beekon's background-refresh task and install cold-launch hooks.
92
+ /// Must be called once, synchronously, during app launch (before
93
+ /// `didFinishLaunchingWithOptions` returns) — invoke from the host AppDelegate.
94
+ @objc public static func registerBackgroundTasks() {
95
+ Beekon.registerBackgroundTasks()
96
+ // Touch the shared actor so its cold-launch resume hooks install.
97
+ _ = Beekon.shared
51
98
  }
52
99
 
53
100
  // MARK: - Lifecycle
@@ -55,17 +102,11 @@ import BeekonKit
55
102
  @objc public func configure(
56
103
  _ config: NSDictionary,
57
104
  resolver resolve: @escaping @Sendable (Any?) -> Void,
58
- rejecter reject: @escaping @Sendable (String?, String?, Error?) -> Void
105
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
59
106
  ) {
60
107
  // Convert the non-Sendable NSDictionary to a Sendable BeekonConfig before
61
108
  // 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
- }
109
+ let cfg = wireToConfig(config)
69
110
  Task {
70
111
  await Beekon.shared.configure(cfg)
71
112
  resolve(nil)
@@ -74,88 +115,325 @@ import BeekonKit
74
115
 
75
116
  @objc public func startWithResolver(
76
117
  _ resolve: @escaping @Sendable (Any?) -> Void,
118
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
119
+ ) {
120
+ // start() never throws — outcome surfaces on the `state` stream.
121
+ Task {
122
+ await Beekon.shared.start()
123
+ resolve(nil)
124
+ }
125
+ }
126
+
127
+ @objc public func stopWithResolver(
128
+ _ resolve: @escaping @Sendable (Any?) -> Void,
129
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
130
+ ) {
131
+ Task {
132
+ await Beekon.shared.stop()
133
+ resolve(nil)
134
+ }
135
+ }
136
+
137
+ @objc public func resumeIfNeededWithResolver(
138
+ _ resolve: @escaping @Sendable (Any?) -> Void,
139
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
140
+ ) {
141
+ Task {
142
+ await Beekon.shared.resumeIfNeeded()
143
+ resolve(nil)
144
+ }
145
+ }
146
+
147
+ @objc public func getCurrentLocationTimeoutMs(
148
+ _ timeoutMs: Double,
149
+ accuracy: String,
150
+ resolver resolve: @escaping @Sendable (Any?) -> Void,
77
151
  rejecter reject: @escaping @Sendable (String?, String?, Error?) -> Void
78
152
  ) {
153
+ // Empty accuracy is the wire encoding of "use the configured mode".
154
+ let mode: AccuracyMode? = accuracy.isEmpty ? nil : accuracyModeFromWire(accuracy)
79
155
  Task { [weak self] in
80
156
  guard let self = self else { return }
81
157
  do {
82
- try await Beekon.shared.start()
83
- resolve(nil)
158
+ let location = try await Beekon.shared.getCurrentLocation(
159
+ timeout: timeoutMs / 1000.0,
160
+ accuracy: mode
161
+ )
162
+ // 0- or 1-element array — empty means timeout / no fix.
163
+ resolve(location.map { [self.locationToWire($0)] } ?? [])
84
164
  } catch {
85
165
  reject(self.errorCode(error), error.localizedDescription, error)
86
166
  }
87
167
  }
88
168
  }
89
169
 
90
- @objc public func stopWithResolver(
170
+ // MARK: - History
171
+
172
+ @objc public func getLocationsFromMs(
173
+ _ fromMs: Double,
174
+ toMs: Double,
175
+ resolver resolve: @escaping @Sendable (Any?) -> Void,
176
+ rejecter reject: @escaping @Sendable (String?, String?, Error?) -> Void
177
+ ) {
178
+ Task { [weak self] in
179
+ guard let self = self else { return }
180
+ do {
181
+ let from = Date(timeIntervalSince1970: fromMs / 1000.0)
182
+ let to = Date(timeIntervalSince1970: toMs / 1000.0)
183
+ let locations = try await Beekon.shared.getLocations(from: from, to: to)
184
+ resolve(locations.map { self.locationToWire($0) })
185
+ } catch {
186
+ reject(self.errorCode(error), error.localizedDescription, error)
187
+ }
188
+ }
189
+ }
190
+
191
+ @objc public func deleteLocationsBeforeMs(
192
+ _ beforeMs: Double,
193
+ resolver resolve: @escaping @Sendable (Any?) -> Void,
194
+ rejecter reject: @escaping @Sendable (String?, String?, Error?) -> Void
195
+ ) {
196
+ Task { [weak self] in
197
+ guard let self = self else { return }
198
+ do {
199
+ // Negative is the wire sentinel for "delete all" (no cutoff).
200
+ let before: Date? =
201
+ beforeMs < 0 ? nil : Date(timeIntervalSince1970: beforeMs / 1000.0)
202
+ let count = try await Beekon.shared.deleteLocations(before: before)
203
+ resolve(count)
204
+ } catch {
205
+ reject(self.errorCode(error), error.localizedDescription, error)
206
+ }
207
+ }
208
+ }
209
+
210
+ @objc public func pendingUploadCountWithResolver(
91
211
  _ resolve: @escaping @Sendable (Any?) -> Void,
92
212
  rejecter reject: @escaping @Sendable (String?, String?, Error?) -> Void
213
+ ) {
214
+ Task { [weak self] in
215
+ guard let self = self else { return }
216
+ do {
217
+ let count = try await Beekon.shared.pendingUploadCount()
218
+ resolve(count)
219
+ } catch {
220
+ reject(self.errorCode(error), error.localizedDescription, error)
221
+ }
222
+ }
223
+ }
224
+
225
+ // MARK: - Sync
226
+
227
+ @objc public func syncWithResolver(
228
+ _ resolve: @escaping @Sendable (Any?) -> Void,
229
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
93
230
  ) {
94
231
  Task {
95
- await Beekon.shared.stop()
232
+ await Beekon.shared.sync()
96
233
  resolve(nil)
97
234
  }
98
235
  }
99
236
 
100
- @objc public func historyFromMs(
101
- _ fromMs: Double,
102
- toMs: Double,
237
+ @objc public func setExtras(
238
+ _ entries: NSArray,
239
+ resolver resolve: @escaping @Sendable (Any?) -> Void,
240
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
241
+ ) {
242
+ let extras = entriesToDict(entries)
243
+ Task {
244
+ await Beekon.shared.setExtras(extras)
245
+ resolve(nil)
246
+ }
247
+ }
248
+
249
+ // MARK: - Geofences
250
+
251
+ @objc public func addGeofences(
252
+ _ geofences: NSArray,
103
253
  resolver resolve: @escaping @Sendable (Any?) -> Void,
104
254
  rejecter reject: @escaping @Sendable (String?, String?, Error?) -> Void
105
255
  ) {
256
+ let list = wireToGeofences(geofences)
106
257
  Task { [weak self] in
107
258
  guard let self = self else { return }
108
259
  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)
260
+ try await Beekon.shared.addGeofences(list)
261
+ resolve(nil)
117
262
  } catch {
118
263
  reject(self.errorCode(error), error.localizedDescription, error)
119
264
  }
120
265
  }
121
266
  }
122
267
 
123
- // MARK: - Mappers
268
+ @objc public func removeGeofences(
269
+ _ ids: NSArray,
270
+ resolver resolve: @escaping @Sendable (Any?) -> Void,
271
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
272
+ ) {
273
+ let list = ids.compactMap { $0 as? String }
274
+ Task {
275
+ await Beekon.shared.removeGeofences(ids: list)
276
+ resolve(nil)
277
+ }
278
+ }
124
279
 
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"]
136
- )
280
+ @objc public func listGeofencesWithResolver(
281
+ _ resolve: @escaping @Sendable (Any?) -> Void,
282
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
283
+ ) {
284
+ Task { [weak self] in
285
+ guard let self = self else { return }
286
+ let geofences = await Beekon.shared.listGeofences()
287
+ resolve(geofences.map { self.geofenceToWire($0) })
137
288
  }
289
+ }
290
+
291
+ // MARK: - Mappers: wire (NSDictionary/NSArray) → Beekon
292
+
293
+ private func wireToConfig(_ d: NSDictionary) -> BeekonConfig {
294
+ let minTime =
295
+ (d["minTimeBetweenLocationsSeconds"] as? NSNumber)?.doubleValue ?? 30
296
+ let minDist =
297
+ (d["minDistanceBetweenLocationsMeters"] as? NSNumber)?.doubleValue ?? 100
298
+ let stationaryRadius =
299
+ (d["stationaryRadiusMeters"] as? NSNumber)?.doubleValue ?? 5
300
+ let detectActivity = (d["detectActivity"] as? NSNumber)?.boolValue ?? false
301
+
302
+ var sync: SyncConfig?
303
+ if let s = d["sync"] as? NSDictionary,
304
+ let urlStr = s["url"] as? String {
305
+ if let url = URL(string: urlStr) {
306
+ let auth = (s["auth"] as? NSDictionary).map { wireToAuthConfig($0) }
307
+ sync = SyncConfig(
308
+ url: url,
309
+ headers: entriesToDict((s["headers"] as? NSArray) ?? []),
310
+ intervalSeconds: (s["intervalSeconds"] as? NSNumber)?.doubleValue ?? 300,
311
+ batchSize: (s["batchSize"] as? NSNumber)?.intValue ?? 100,
312
+ auth: auth
313
+ )
314
+ } else {
315
+ // Don't silently drop the whole sync config on an unparseable URL —
316
+ // surface it so a malformed endpoint is diagnosable instead of a no-op.
317
+ NSLog("[BeekonRn] sync config ignored: url is not a valid URL: %@", urlStr)
318
+ }
319
+ }
320
+
321
+ // `notification` is Android-only — iOS ignores it.
138
322
  return BeekonConfig(
139
- intervalSeconds: intervalSeconds,
140
- distanceMeters: distanceMeters
323
+ minTimeBetweenLocationsSeconds: minTime,
324
+ minDistanceBetweenLocationsMeters: minDist,
325
+ accuracyMode: accuracyModeFromWire(d["accuracyMode"] as? String),
326
+ whenStationary: stationaryModeFromWire(d["whenStationary"] as? String),
327
+ stationaryRadiusMeters: stationaryRadius,
328
+ detectActivity: detectActivity,
329
+ sync: sync
141
330
  )
142
- // androidNotification ignored on iOS.
143
331
  }
144
332
 
333
+ private func wireToGeofences(_ arr: NSArray) -> [BeekonGeofence] {
334
+ var out: [BeekonGeofence] = []
335
+ for case let m as NSDictionary in arr {
336
+ guard
337
+ let id = m["id"] as? String,
338
+ let lat = (m["lat"] as? NSNumber)?.doubleValue,
339
+ let lng = (m["lng"] as? NSNumber)?.doubleValue,
340
+ let radius = (m["radiusMeters"] as? NSNumber)?.doubleValue
341
+ else { continue }
342
+ out.append(
343
+ BeekonGeofence(
344
+ id: id,
345
+ latitude: lat,
346
+ longitude: lng,
347
+ radiusMeters: radius,
348
+ notifyOnEntry: (m["notifyOnEntry"] as? NSNumber)?.boolValue ?? true,
349
+ notifyOnExit: (m["notifyOnExit"] as? NSNumber)?.boolValue ?? true
350
+ )
351
+ )
352
+ }
353
+ return out
354
+ }
355
+
356
+ private func entriesToDict(_ arr: NSArray) -> [String: String] {
357
+ var out: [String: String] = [:]
358
+ for case let e as NSDictionary in arr {
359
+ if let k = e["key"] as? String, let v = e["value"] as? String {
360
+ out[k] = v
361
+ }
362
+ }
363
+ return out
364
+ }
365
+
366
+ private func wireToAuthConfig(_ d: NSDictionary) -> AuthConfig {
367
+ // Wire carries epoch millis; the native recipe wants epoch seconds.
368
+ let expiresAt = (d["expiresAtMs"] as? NSNumber).map { $0.doubleValue / 1000.0 }
369
+ let responseMapping = (d["responseMapping"] as? NSDictionary)
370
+ .map { wireToResponseMapping($0) } ?? AuthResponseMapping()
371
+ return AuthConfig(
372
+ accessToken: d["accessToken"] as? String,
373
+ refreshToken: d["refreshToken"] as? String,
374
+ expiresAt: expiresAt,
375
+ strategy: authStrategyFromWire(d["strategy"] as? String),
376
+ refreshUrl: (d["refreshUrl"] as? String).flatMap { URL(string: $0) },
377
+ refreshPayload: entriesToDict((d["refreshPayload"] as? NSArray) ?? []),
378
+ refreshHeaders: entriesToDict((d["refreshHeaders"] as? NSArray) ?? []),
379
+ refreshBodyFormat: authBodyFormatFromWire(d["refreshBodyFormat"] as? String),
380
+ responseMapping: responseMapping,
381
+ skewMarginSeconds: (d["skewMarginSeconds"] as? NSNumber)?.doubleValue ?? 60,
382
+ seedEpoch: (d["seedEpoch"] as? NSNumber)?.intValue
383
+ )
384
+ }
385
+
386
+ private func wireToResponseMapping(_ d: NSDictionary) -> AuthResponseMapping {
387
+ return AuthResponseMapping(
388
+ accessToken: d["accessToken"] as? String,
389
+ refreshToken: d["refreshToken"] as? String,
390
+ expiresIn: d["expiresIn"] as? String,
391
+ expiresAt: d["expiresAt"] as? String
392
+ )
393
+ }
394
+
395
+ // MARK: - Mappers: Beekon → wire (NSDictionary)
396
+
145
397
  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`.
398
+ // NSDictionary literals can't carry `nil`, so optionals collapse to
399
+ // `NSNull` — the JS Codegen layer translates that back to `null`.
148
400
  let d = NSMutableDictionary()
149
- d["lat"] = loc.lat
150
- d["lng"] = loc.lng
401
+ d["id"] = loc.id
402
+ d["lat"] = loc.latitude
403
+ d["lng"] = loc.longitude
151
404
  d["timestampMs"] = loc.timestamp.timeIntervalSince1970 * 1000.0
152
405
  d["accuracy"] = loc.accuracy.map { $0 as Any } ?? NSNull()
153
406
  d["speed"] = loc.speed.map { $0 as Any } ?? NSNull()
154
407
  d["bearing"] = loc.bearing.map { $0 as Any } ?? NSNull()
155
408
  d["altitude"] = loc.altitude.map { $0 as Any } ?? NSNull()
409
+ d["quality"] = qualityToWire(loc.quality)
410
+ d["trigger"] = triggerToWire(loc.trigger)
411
+ d["motion"] = motionToWire(loc.motion)
412
+ d["activity"] = loc.activity.map { activityToWire($0) as Any } ?? NSNull()
413
+ d["isMock"] = loc.isMock
156
414
  return d
157
415
  }
158
416
 
417
+ private func geofenceToWire(_ g: BeekonGeofence) -> NSDictionary {
418
+ return [
419
+ "id": g.id,
420
+ "lat": g.latitude,
421
+ "lng": g.longitude,
422
+ "radiusMeters": g.radiusMeters,
423
+ "notifyOnEntry": g.notifyOnEntry,
424
+ "notifyOnExit": g.notifyOnExit,
425
+ ]
426
+ }
427
+
428
+ private func geofenceEventToWire(_ e: GeofenceEvent) -> NSDictionary {
429
+ return [
430
+ "id": e.id,
431
+ "geofenceId": e.geofenceId,
432
+ "type": transitionToWire(e.type),
433
+ "timestampMs": e.timestamp.timeIntervalSince1970 * 1000.0,
434
+ ]
435
+ }
436
+
159
437
  private func stateToWire(_ s: BeekonState) -> NSDictionary {
160
438
  switch s {
161
439
  case .idle:
@@ -164,6 +442,66 @@ import BeekonKit
164
442
  return ["type": "tracking"]
165
443
  case .stopped(let reason):
166
444
  return ["type": "stopped", "stopReason": stopReasonToWire(reason)]
445
+ // BeekonKit enums are non-frozen (library-evolution binary); handle unknowns.
446
+ @unknown default:
447
+ return ["type": "idle"]
448
+ }
449
+ }
450
+
451
+ private func syncStatusToWire(_ s: SyncStatus) -> NSDictionary {
452
+ switch s {
453
+ case .idle:
454
+ return ["type": "idle"]
455
+ case .pending:
456
+ return ["type": "pending"]
457
+ case .failed(let reason):
458
+ return ["type": "failed", "failure": syncFailureToWire(reason)]
459
+ @unknown default:
460
+ return ["type": "idle"]
461
+ }
462
+ }
463
+
464
+ private func authTokensToWire(_ t: AuthTokens) -> NSDictionary {
465
+ // NSDictionary literals can't carry `nil`; optionals collapse to `NSNull`,
466
+ // which the Codegen layer translates back to `null` on the JS side.
467
+ let d = NSMutableDictionary()
468
+ d["accessToken"] = t.accessToken
469
+ d["refreshToken"] = t.refreshToken.map { $0 as Any } ?? NSNull()
470
+ // Native epoch seconds → wire epoch millis.
471
+ d["expiresAtMs"] = t.expiresAt.map { ($0 * 1000.0) as Any } ?? NSNull()
472
+ d["epoch"] = t.epoch
473
+ return d
474
+ }
475
+
476
+ // MARK: - Enum mappers
477
+
478
+ private func accuracyModeFromWire(_ s: String?) -> AccuracyMode {
479
+ switch s {
480
+ case "high": return .high
481
+ case "low": return .low
482
+ default: return .balanced
483
+ }
484
+ }
485
+
486
+ private func stationaryModeFromWire(_ s: String?) -> StationaryMode {
487
+ switch s {
488
+ case "keepTracking": return .keepTracking
489
+ case "pauseWithCheckIns": return .pauseWithCheckIns
490
+ default: return .pause
491
+ }
492
+ }
493
+
494
+ private func authStrategyFromWire(_ s: String?) -> AuthStrategy {
495
+ switch s {
496
+ case "raw": return .raw
497
+ default: return .bearer
498
+ }
499
+ }
500
+
501
+ private func authBodyFormatFromWire(_ s: String?) -> AuthBodyFormat {
502
+ switch s {
503
+ case "json": return .json
504
+ default: return .form
167
505
  }
168
506
  }
169
507
 
@@ -172,18 +510,84 @@ import BeekonKit
172
510
  case .user: return "user"
173
511
  case .permissionDenied: return "permissionDenied"
174
512
  case .locationServicesDisabled: return "locationServicesDisabled"
513
+ case .locationUnavailable: return "locationUnavailable"
514
+ case .system: return "system"
515
+ @unknown default: return "system"
516
+ }
517
+ }
518
+
519
+ private func syncFailureToWire(_ f: SyncFailure) -> String {
520
+ switch f {
521
+ case .auth: return "auth"
522
+ case .rejected: return "rejected"
523
+ @unknown default: return "rejected"
524
+ }
525
+ }
526
+
527
+ private func qualityToWire(_ q: LocationQuality) -> String {
528
+ switch q {
529
+ case .ok: return "ok"
530
+ case .lowAccuracy: return "lowAccuracy"
531
+ case .implausibleSpeed: return "implausibleSpeed"
532
+ @unknown default: return "ok"
533
+ }
534
+ }
535
+
536
+ private func triggerToWire(_ t: LocationTrigger) -> String {
537
+ switch t {
538
+ case .interval: return "interval"
539
+ case .motion: return "motion"
540
+ case .checkIn: return "checkIn"
541
+ case .geofence: return "geofence"
542
+ case .manual: return "manual"
543
+ @unknown default: return "interval"
544
+ }
545
+ }
546
+
547
+ private func motionToWire(_ m: MotionState) -> String {
548
+ switch m {
549
+ case .moving: return "moving"
550
+ case .stationary: return "stationary"
551
+ case .unknown: return "unknown"
552
+ @unknown default: return "unknown"
553
+ }
554
+ }
555
+
556
+ private func activityToWire(_ a: ActivityType) -> String {
557
+ switch a {
558
+ case .stationary: return "stationary"
559
+ case .walking: return "walking"
560
+ case .running: return "running"
561
+ case .cycling: return "cycling"
562
+ case .automotive: return "automotive"
563
+ case .unknown: return "unknown"
564
+ @unknown default: return "unknown"
565
+ }
566
+ }
567
+
568
+ private func transitionToWire(_ t: Transition) -> String {
569
+ switch t {
570
+ case .enter: return "enter"
571
+ case .exit: return "exit"
572
+ @unknown default: return "enter"
175
573
  }
176
574
  }
177
575
 
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.
576
+ /// Maps native `BeekonError` cases to stable string codes shared with Android,
577
+ /// so JS-side error handling can switch on them without per-platform branches.
181
578
  private func errorCode(_ e: Error) -> String {
182
579
  if let be = e as? BeekonError {
183
580
  switch be {
184
- case .permissionDenied: return "PERMISSION_DENIED"
185
- case .locationServicesDisabled: return "LOCATION_SERVICES_DISABLED"
186
- case .storageFailure: return "STORAGE_FAILURE"
581
+ case .storage: return "STORAGE_FAILURE"
582
+ case .invalidGeofence: return "INVALID_GEOFENCE"
583
+ case .locationUnavailable(let reason):
584
+ switch reason {
585
+ case .permissionDenied: return "PERMISSION_DENIED"
586
+ case .locationServicesDisabled: return "LOCATION_SERVICES_DISABLED"
587
+ case .unavailable: return "LOCATION_UNAVAILABLE"
588
+ @unknown default: return "LOCATION_UNAVAILABLE"
589
+ }
590
+ @unknown default: return "INTERNAL_ERROR"
187
591
  }
188
592
  }
189
593
  return "INTERNAL_ERROR"
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>BeekonKit.framework/BeekonKit</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64_x86_64-simulator</string>
11
+ <string>ios-arm64</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>BeekonKit.framework</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
- <string>x86_64</string>
18
17
  </array>
19
18
  <key>SupportedPlatform</key>
20
19
  <string>ios</string>
21
- <key>SupportedPlatformVariant</key>
22
- <string>simulator</string>
23
20
  </dict>
24
21
  <dict>
25
22
  <key>BinaryPath</key>
26
23
  <string>BeekonKit.framework/BeekonKit</string>
27
24
  <key>LibraryIdentifier</key>
28
- <string>ios-arm64</string>
25
+ <string>ios-arm64_x86_64-simulator</string>
29
26
  <key>LibraryPath</key>
30
27
  <string>BeekonKit.framework</string>
31
28
  <key>SupportedArchitectures</key>
32
29
  <array>
33
30
  <string>arm64</string>
31
+ <string>x86_64</string>
34
32
  </array>
35
33
  <key>SupportedPlatform</key>
36
34
  <string>ios</string>
35
+ <key>SupportedPlatformVariant</key>
36
+ <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>