@wayq/beekon-rn 0.0.5 → 0.0.7

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 (64) hide show
  1. package/BeekonRn.podspec +4 -2
  2. package/CHANGELOG.md +127 -0
  3. package/README.md +303 -81
  4. package/android/build.gradle +3 -2
  5. package/android/src/main/java/in/wayq/beekonrn/BeekonRnModule.kt +164 -7
  6. package/ios/BeekonRn.mm +23 -0
  7. package/ios/BeekonRn.swift +199 -10
  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 +7784 -2697
  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 +111 -3
  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 +7784 -2697
  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 +111 -3
  19. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +7784 -2697
  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 +111 -3
  22. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/_CodeSignature/CodeResources +1 -1
  23. package/lib/module/NativeBeekonRn.js +20 -0
  24. package/lib/module/NativeBeekonRn.js.map +1 -1
  25. package/lib/module/beekon.js +90 -7
  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 +98 -4
  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/error.js +13 -3
  33. package/lib/module/types/error.js.map +1 -1
  34. package/lib/module/types/license.js +2 -0
  35. package/lib/module/types/license.js.map +1 -0
  36. package/lib/typescript/src/NativeBeekonRn.d.ts +84 -0
  37. package/lib/typescript/src/NativeBeekonRn.d.ts.map +1 -1
  38. package/lib/typescript/src/beekon.d.ts +45 -1
  39. package/lib/typescript/src/beekon.d.ts.map +1 -1
  40. package/lib/typescript/src/index.d.ts +3 -1
  41. package/lib/typescript/src/index.d.ts.map +1 -1
  42. package/lib/typescript/src/internal/mappers.d.ts +12 -1
  43. package/lib/typescript/src/internal/mappers.d.ts.map +1 -1
  44. package/lib/typescript/src/types/auth.d.ts +99 -0
  45. package/lib/typescript/src/types/auth.d.ts.map +1 -0
  46. package/lib/typescript/src/types/config.d.ts +29 -0
  47. package/lib/typescript/src/types/config.d.ts.map +1 -1
  48. package/lib/typescript/src/types/enums.d.ts +14 -0
  49. package/lib/typescript/src/types/enums.d.ts.map +1 -1
  50. package/lib/typescript/src/types/error.d.ts +14 -4
  51. package/lib/typescript/src/types/error.d.ts.map +1 -1
  52. package/lib/typescript/src/types/license.d.ts +50 -0
  53. package/lib/typescript/src/types/license.d.ts.map +1 -0
  54. package/package.json +10 -2
  55. package/scripts/fetch-beekonkit.sh +4 -4
  56. package/src/NativeBeekonRn.ts +93 -0
  57. package/src/beekon.ts +104 -6
  58. package/src/index.tsx +4 -0
  59. package/src/internal/mappers.ts +109 -1
  60. package/src/types/auth.ts +101 -0
  61. package/src/types/config.ts +29 -0
  62. package/src/types/enums.ts +16 -0
  63. package/src/types/error.ts +19 -4
  64. package/src/types/license.ts +47 -0
@@ -9,15 +9,22 @@ import com.facebook.react.bridge.WritableArray
9
9
  import com.facebook.react.bridge.WritableMap
10
10
  import `in`.wayq.beekon.AccuracyMode
11
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
12
17
  import `in`.wayq.beekon.Beekon
13
18
  import `in`.wayq.beekon.BeekonConfig
14
19
  import `in`.wayq.beekon.BeekonException
15
20
  import `in`.wayq.beekon.BeekonGeofence
16
21
  import `in`.wayq.beekon.BeekonState
17
22
  import `in`.wayq.beekon.GeofenceEvent
23
+ import `in`.wayq.beekon.LicenseStatus
18
24
  import `in`.wayq.beekon.Location
19
25
  import `in`.wayq.beekon.LocationQuality
20
26
  import `in`.wayq.beekon.LocationTrigger
27
+ import `in`.wayq.beekon.LocationUnavailableReason
21
28
  import `in`.wayq.beekon.MotionState
22
29
  import `in`.wayq.beekon.NotificationConfig
23
30
  import `in`.wayq.beekon.StationaryMode
@@ -43,6 +50,11 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
43
50
  private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
44
51
 
45
52
  init {
53
+ // Identify this wrapper to the native verifier (license-format-v1 §11) before
54
+ // any configure() can run — a direct native call, deliberately not bridged to
55
+ // JS. The module is constructed before its first method dispatch, so this is
56
+ // always set first.
57
+ Beekon.setWrapperInfo("rn", WRAPPER_VERSION)
46
58
  // No Beekon.initialize() — the SDK auto-initializes via AndroidX Startup.
47
59
  scope.launch { Beekon.state.collect { emitOnState(stateToWire(it)) } }
48
60
  scope.launch { Beekon.locations.collect { emitOnLocation(locationToWire(it)) } }
@@ -50,6 +62,8 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
50
62
  Beekon.geofenceEvents.collect { emitOnGeofenceEvent(geofenceEventToWire(it)) }
51
63
  }
52
64
  scope.launch { Beekon.syncStatus.collect { emitOnSyncStatus(syncStatusToWire(it)) } }
65
+ scope.launch { Beekon.authChanges.collect { emitOnAuthTokens(tokenRefreshToWire(it)) } }
66
+ scope.launch { Beekon.licenseStatus.collect { emitOnLicenseStatus(licenseStatusToWire(it)) } }
53
67
  }
54
68
 
55
69
  // ---------------------------------------------------------------------------
@@ -87,12 +101,13 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
87
101
  }
88
102
  }
89
103
 
90
- // Android has no native resumeIfNeeded Beekon.start() rehydrates the
91
- // persisted intent (mirrors the Flutter plugin).
104
+ // Calls the guarded native resume (Beekon.resumeIfNeeded), which only
105
+ // re-adopts a previously-active, non-user-stopped session — a no-op when
106
+ // already Tracking or when the user explicitly stopped (mirrors iOS).
92
107
  override fun resumeIfNeeded(promise: Promise) {
93
108
  scope.launch {
94
109
  try {
95
- Beekon.start()
110
+ Beekon.resumeIfNeeded()
96
111
  promise.resolve(null)
97
112
  } catch (t: Throwable) {
98
113
  promise.reject(errorCode(t), t.message ?: "resumeIfNeeded failed", t)
@@ -100,6 +115,21 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
100
115
  }
101
116
  }
102
117
 
118
+ override fun getCurrentLocation(timeoutMs: Double, accuracy: String, promise: Promise) {
119
+ scope.launch {
120
+ try {
121
+ // Empty accuracy is the wire encoding of "use the configured mode".
122
+ val mode = if (accuracy.isEmpty()) null else toAccuracyMode(accuracy)
123
+ val loc = Beekon.getCurrentLocation(timeoutMs.toLong(), mode)
124
+ val arr: WritableArray = Arguments.createArray()
125
+ if (loc != null) arr.pushMap(locationToWire(loc))
126
+ promise.resolve(arr)
127
+ } catch (t: Throwable) {
128
+ promise.reject(errorCode(t), t.message ?: "getCurrentLocation failed", t)
129
+ }
130
+ }
131
+ }
132
+
103
133
  // ---------------------------------------------------------------------------
104
134
  // History
105
135
  // ---------------------------------------------------------------------------
@@ -193,6 +223,15 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
193
223
  }
194
224
  }
195
225
 
226
+ // ---------------------------------------------------------------------------
227
+ // License (observational only — never blocks; license-format-v1 §8)
228
+ // ---------------------------------------------------------------------------
229
+
230
+ override fun licenseStatus(promise: Promise) {
231
+ // StateFlow.value is the current, synchronously-readable status.
232
+ promise.resolve(licenseStatusToWire(Beekon.licenseStatus.value))
233
+ }
234
+
196
235
  override fun invalidate() {
197
236
  super.invalidate()
198
237
  scope.cancel()
@@ -226,27 +265,86 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
226
265
  detectActivity = map.getBoolean("detectActivity"),
227
266
  sync = sync,
228
267
  notification = notification ?: NotificationConfig(),
268
+ // Passed through verbatim; absent/null means unset — the SDK falls through
269
+ // to the manifest meta-data, then evaluation (license-format-v1 §9).
270
+ licenseKey = optString(map, "licenseKey"),
229
271
  )
230
272
  }
231
273
 
232
- private fun wireToSyncConfig(map: ReadableMap): SyncConfig =
233
- SyncConfig(
274
+ private fun wireToSyncConfig(map: ReadableMap): SyncConfig {
275
+ val auth =
276
+ if (map.hasKey("auth") && !map.isNull("auth")) {
277
+ map.getMap("auth")?.let { wireToAuthConfig(it) }
278
+ } else {
279
+ null
280
+ }
281
+ return SyncConfig(
234
282
  url = map.getString("url") ?: "",
235
283
  headers = entriesToMap(map.getArray("headers")),
236
284
  intervalSeconds = map.getDouble("intervalSeconds").toLong(),
237
285
  batchSize = map.getDouble("batchSize").toInt(),
286
+ auth = auth,
238
287
  )
288
+ }
289
+
290
+ private fun wireToAuthConfig(map: ReadableMap): AuthConfig {
291
+ val expiresAt =
292
+ if (map.hasKey("expiresAtMs") && !map.isNull("expiresAtMs")) {
293
+ // Wire carries epoch millis; the native recipe wants epoch seconds.
294
+ (map.getDouble("expiresAtMs") / 1000.0).toLong()
295
+ } else {
296
+ null
297
+ }
298
+ val seedEpoch =
299
+ if (map.hasKey("seedEpoch") && !map.isNull("seedEpoch")) {
300
+ map.getDouble("seedEpoch").toLong()
301
+ } else {
302
+ null
303
+ }
304
+ val responseMapping =
305
+ if (map.hasKey("responseMapping") && !map.isNull("responseMapping")) {
306
+ map.getMap("responseMapping")?.let { wireToResponseMapping(it) } ?: AuthResponseMapping()
307
+ } else {
308
+ AuthResponseMapping()
309
+ }
310
+ return AuthConfig(
311
+ accessToken = optString(map, "accessToken"),
312
+ refreshToken = optString(map, "refreshToken"),
313
+ expiresAt = expiresAt,
314
+ strategy = toAuthStrategy(map.getString("strategy")),
315
+ refreshUrl = optString(map, "refreshUrl"),
316
+ refreshPayload = entriesToMap(map.getArray("refreshPayload")),
317
+ refreshHeaders = entriesToMap(map.getArray("refreshHeaders")),
318
+ refreshBodyFormat = toRefreshBodyFormat(map.getString("refreshBodyFormat")),
319
+ responseMapping = responseMapping,
320
+ skewMarginSeconds = map.getDouble("skewMarginSeconds").toLong(),
321
+ seedEpoch = seedEpoch,
322
+ )
323
+ }
324
+
325
+ private fun wireToResponseMapping(map: ReadableMap): AuthResponseMapping =
326
+ AuthResponseMapping(
327
+ accessToken = optString(map, "accessToken"),
328
+ refreshToken = optString(map, "refreshToken"),
329
+ expiresIn = optString(map, "expiresIn"),
330
+ expiresAt = optString(map, "expiresAt"),
331
+ )
332
+
333
+ private fun optString(map: ReadableMap, key: String): String? =
334
+ if (map.hasKey(key) && !map.isNull(key)) map.getString(key) else null
239
335
 
240
336
  private fun wireToNotification(map: ReadableMap): NotificationConfig {
241
337
  val title =
242
338
  if (map.hasKey("title") && !map.isNull("title")) map.getString("title") else null
243
339
  val text =
244
340
  if (map.hasKey("text") && !map.isNull("text")) map.getString("text") else null
341
+ val smallIcon =
342
+ if (map.hasKey("smallIcon") && !map.isNull("smallIcon")) map.getString("smallIcon") else null
245
343
  // Use the data-class default for `title` when the wire value is absent.
246
344
  return if (title != null) {
247
- NotificationConfig(title = title, text = text)
345
+ NotificationConfig(title = title, text = text, smallIcon = smallIcon)
248
346
  } else {
249
- NotificationConfig(text = text)
347
+ NotificationConfig(text = text, smallIcon = smallIcon)
250
348
  }
251
349
  }
252
350
 
@@ -347,6 +445,45 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
347
445
  return m
348
446
  }
349
447
 
448
+ private fun tokenRefreshToWire(t: AuthTokens): WritableMap {
449
+ val m = Arguments.createMap()
450
+ m.putString("accessToken", t.accessToken)
451
+ val refreshToken = t.refreshToken
452
+ if (refreshToken != null) m.putString("refreshToken", refreshToken) else m.putNull("refreshToken")
453
+ val expiresAt = t.expiresAt
454
+ // Native epoch seconds → wire epoch millis.
455
+ if (expiresAt != null) m.putDouble("expiresAtMs", (expiresAt * 1000L).toDouble()) else m.putNull("expiresAtMs")
456
+ m.putDouble("epoch", t.epoch.toDouble())
457
+ return m
458
+ }
459
+
460
+ // Reads the SDK's wire-stable names (no validation here). `tier`/`entitlements`
461
+ // populate only for Licensed; `reason` only for Invalid.
462
+ private fun licenseStatusToWire(s: LicenseStatus): WritableMap {
463
+ val m = Arguments.createMap()
464
+ m.putString("status", s.wireName)
465
+ when (s) {
466
+ is LicenseStatus.Licensed -> {
467
+ m.putString("tier", s.tier)
468
+ val arr: WritableArray = Arguments.createArray()
469
+ for (e in s.entitlements) arr.pushString(e)
470
+ m.putArray("entitlements", arr)
471
+ m.putNull("reason")
472
+ }
473
+ is LicenseStatus.Invalid -> {
474
+ m.putNull("tier")
475
+ m.putArray("entitlements", Arguments.createArray())
476
+ m.putString("reason", s.reason.wireName)
477
+ }
478
+ else -> {
479
+ m.putNull("tier")
480
+ m.putArray("entitlements", Arguments.createArray())
481
+ m.putNull("reason")
482
+ }
483
+ }
484
+ return m
485
+ }
486
+
350
487
  private fun putNullableDouble(map: WritableMap, key: String, value: Double?) {
351
488
  if (value == null) map.putNull(key) else map.putDouble(key, value)
352
489
  }
@@ -367,6 +504,16 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
367
504
  else -> StationaryMode.Pause
368
505
  }
369
506
 
507
+ private fun toAuthStrategy(s: String?): AuthStrategy = when (s) {
508
+ "raw" -> AuthStrategy.Raw
509
+ else -> AuthStrategy.Bearer
510
+ }
511
+
512
+ private fun toRefreshBodyFormat(s: String?): AuthBodyFormat = when (s) {
513
+ "json" -> AuthBodyFormat.Json
514
+ else -> AuthBodyFormat.Form
515
+ }
516
+
370
517
  private fun stopReasonToWire(r: StopReason): String = when (r) {
371
518
  StopReason.User -> "user"
372
519
  StopReason.PermissionDenied -> "permissionDenied"
@@ -419,10 +566,20 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
419
566
  private fun errorCode(t: Throwable): String = when (t) {
420
567
  is BeekonException.StorageException -> "STORAGE_FAILURE"
421
568
  is BeekonException.InvalidGeofence -> "INVALID_GEOFENCE"
569
+ is BeekonException.LocationUnavailable -> when (t.reason) {
570
+ LocationUnavailableReason.PermissionDenied -> "PERMISSION_DENIED"
571
+ LocationUnavailableReason.LocationServicesDisabled -> "LOCATION_SERVICES_DISABLED"
572
+ LocationUnavailableReason.Unavailable -> "LOCATION_UNAVAILABLE"
573
+ }
422
574
  else -> "INTERNAL_ERROR"
423
575
  }
424
576
 
425
577
  companion object {
426
578
  const val NAME = NativeBeekonRnSpec.NAME
579
+
580
+ // Reported to the native verifier via setWrapperInfo (diagnostics only — the
581
+ // verifier consumes only the product). Keep in sync with package.json
582
+ // "version" on release.
583
+ private const val WRAPPER_VERSION = "0.0.7"
427
584
  }
428
585
  }
package/ios/BeekonRn.mm CHANGED
@@ -29,6 +29,12 @@
29
29
  }
30
30
  onSyncStatus:^(NSDictionary *_Nonnull st) {
31
31
  [weakSelf emitOnSyncStatus:st];
32
+ }
33
+ onAuthTokens:^(NSDictionary *_Nonnull t) {
34
+ [weakSelf emitOnAuthTokens:t];
35
+ }
36
+ onLicenseStatus:^(NSDictionary *_Nonnull ls) {
37
+ [weakSelf emitOnLicenseStatus:ls];
32
38
  }];
33
39
  }
34
40
  return self;
@@ -72,6 +78,16 @@
72
78
  [_impl resumeIfNeededWithResolver:resolve rejecter:reject];
73
79
  }
74
80
 
81
+ - (void)getCurrentLocation:(double)timeoutMs
82
+ accuracy:(NSString *)accuracy
83
+ resolve:(RCTPromiseResolveBlock)resolve
84
+ reject:(RCTPromiseRejectBlock)reject {
85
+ [_impl getCurrentLocationTimeoutMs:timeoutMs
86
+ accuracy:accuracy
87
+ resolver:resolve
88
+ rejecter:reject];
89
+ }
90
+
75
91
  // MARK: - History
76
92
 
77
93
  - (void)getLocations:(double)fromMs
@@ -124,6 +140,13 @@
124
140
  [_impl listGeofencesWithResolver:resolve rejecter:reject];
125
141
  }
126
142
 
143
+ // MARK: - License
144
+
145
+ - (void)licenseStatus:(RCTPromiseResolveBlock)resolve
146
+ reject:(RCTPromiseRejectBlock)reject {
147
+ [_impl licenseStatusWithResolver:resolve rejecter:reject];
148
+ }
149
+
127
150
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
128
151
  (const facebook::react::ObjCTurboModule::InitParams &)params {
129
152
  return std::make_shared<facebook::react::NativeBeekonRnSpecJSI>(params);
@@ -22,22 +22,39 @@ import BeekonKit
22
22
  private let onLocationCb: (NSDictionary) -> Void
23
23
  private let onGeofenceEventCb: (NSDictionary) -> Void
24
24
  private let onSyncStatusCb: (NSDictionary) -> Void
25
+ private let onAuthTokensCb: (NSDictionary) -> Void
26
+ private let onLicenseStatusCb: (NSDictionary) -> Void
25
27
 
26
28
  private var stateTask: Task<Void, Never>?
27
29
  private var locationsTask: Task<Void, Never>?
28
30
  private var geofenceEventsTask: Task<Void, Never>?
29
31
  private var syncStatusTask: Task<Void, Never>?
32
+ private var authTokensTask: Task<Void, Never>?
33
+ private var licenseStatusTask: Task<Void, Never>?
34
+
35
+ // Reported to the native verifier via setWrapperInfo (diagnostics only — the
36
+ // verifier consumes only the product). Keep in sync with package.json
37
+ // "version" on release.
38
+ private static let wrapperVersion = "0.0.7"
30
39
 
31
40
  @objc public init(
32
41
  onState: @escaping (NSDictionary) -> Void,
33
42
  onLocation: @escaping (NSDictionary) -> Void,
34
43
  onGeofenceEvent: @escaping (NSDictionary) -> Void,
35
- onSyncStatus: @escaping (NSDictionary) -> Void
44
+ onSyncStatus: @escaping (NSDictionary) -> Void,
45
+ onAuthTokens: @escaping (NSDictionary) -> Void,
46
+ onLicenseStatus: @escaping (NSDictionary) -> Void
36
47
  ) {
48
+ // Identify this wrapper to the native verifier (license-format-v1 §11) before
49
+ // any configure() can run — a direct native call (nonisolated static),
50
+ // deliberately not bridged to JS.
51
+ Beekon.setWrapperInfo(product: "rn", version: BeekonRnImpl.wrapperVersion)
37
52
  self.onStateCb = onState
38
53
  self.onLocationCb = onLocation
39
54
  self.onGeofenceEventCb = onGeofenceEvent
40
55
  self.onSyncStatusCb = onSyncStatus
56
+ self.onAuthTokensCb = onAuthTokens
57
+ self.onLicenseStatusCb = onLicenseStatus
41
58
  super.init()
42
59
  self.stateTask = Task { [weak self] in
43
60
  guard let self = self else { return }
@@ -63,6 +80,18 @@ import BeekonKit
63
80
  self.onSyncStatusCb(self.syncStatusToWire(status))
64
81
  }
65
82
  }
83
+ self.authTokensTask = Task { [weak self] in
84
+ guard let self = self else { return }
85
+ for await tokens in await Beekon.shared.authChanges {
86
+ self.onAuthTokensCb(self.authTokensToWire(tokens))
87
+ }
88
+ }
89
+ self.licenseStatusTask = Task { [weak self] in
90
+ guard let self = self else { return }
91
+ for await status in await Beekon.shared.licenseStatusUpdates {
92
+ self.onLicenseStatusCb(self.licenseStatusToWire(status))
93
+ }
94
+ }
66
95
  }
67
96
 
68
97
  @objc public func invalidate() {
@@ -70,10 +99,14 @@ import BeekonKit
70
99
  locationsTask?.cancel()
71
100
  geofenceEventsTask?.cancel()
72
101
  syncStatusTask?.cancel()
102
+ authTokensTask?.cancel()
103
+ licenseStatusTask?.cancel()
73
104
  stateTask = nil
74
105
  locationsTask = nil
75
106
  geofenceEventsTask = nil
76
107
  syncStatusTask = nil
108
+ authTokensTask = nil
109
+ licenseStatusTask = nil
77
110
  }
78
111
 
79
112
  /// Register Beekon's background-refresh task and install cold-launch hooks.
@@ -132,6 +165,29 @@ import BeekonKit
132
165
  }
133
166
  }
134
167
 
168
+ @objc public func getCurrentLocationTimeoutMs(
169
+ _ timeoutMs: Double,
170
+ accuracy: String,
171
+ resolver resolve: @escaping @Sendable (Any?) -> Void,
172
+ rejecter reject: @escaping @Sendable (String?, String?, Error?) -> Void
173
+ ) {
174
+ // Empty accuracy is the wire encoding of "use the configured mode".
175
+ let mode: AccuracyMode? = accuracy.isEmpty ? nil : accuracyModeFromWire(accuracy)
176
+ Task { [weak self] in
177
+ guard let self = self else { return }
178
+ do {
179
+ let location = try await Beekon.shared.getCurrentLocation(
180
+ timeout: timeoutMs / 1000.0,
181
+ accuracy: mode
182
+ )
183
+ // 0- or 1-element array — empty means timeout / no fix.
184
+ resolve(location.map { [self.locationToWire($0)] } ?? [])
185
+ } catch {
186
+ reject(self.errorCode(error), error.localizedDescription, error)
187
+ }
188
+ }
189
+ }
190
+
135
191
  // MARK: - History
136
192
 
137
193
  @objc public func getLocationsFromMs(
@@ -253,6 +309,19 @@ import BeekonKit
253
309
  }
254
310
  }
255
311
 
312
+ // MARK: - License (observational only — never blocks; license-format-v1 §8)
313
+
314
+ @objc public func licenseStatusWithResolver(
315
+ _ resolve: @escaping @Sendable (Any?) -> Void,
316
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
317
+ ) {
318
+ Task { [weak self] in
319
+ guard let self = self else { return }
320
+ // `licenseStatus` is an actor-isolated property — read it with `await`.
321
+ resolve(self.licenseStatusToWire(await Beekon.shared.licenseStatus))
322
+ }
323
+ }
324
+
256
325
  // MARK: - Mappers: wire (NSDictionary/NSArray) → Beekon
257
326
 
258
327
  private func wireToConfig(_ d: NSDictionary) -> BeekonConfig {
@@ -266,14 +335,21 @@ import BeekonKit
266
335
 
267
336
  var sync: SyncConfig?
268
337
  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
276
- )
338
+ let urlStr = s["url"] as? String {
339
+ if let url = URL(string: urlStr) {
340
+ let auth = (s["auth"] as? NSDictionary).map { wireToAuthConfig($0) }
341
+ sync = SyncConfig(
342
+ url: url,
343
+ headers: entriesToDict((s["headers"] as? NSArray) ?? []),
344
+ intervalSeconds: (s["intervalSeconds"] as? NSNumber)?.doubleValue ?? 300,
345
+ batchSize: (s["batchSize"] as? NSNumber)?.intValue ?? 100,
346
+ auth: auth
347
+ )
348
+ } else {
349
+ // Don't silently drop the whole sync config on an unparseable URL —
350
+ // surface it so a malformed endpoint is diagnosable instead of a no-op.
351
+ NSLog("[BeekonRn] sync config ignored: url is not a valid URL: %@", urlStr)
352
+ }
277
353
  }
278
354
 
279
355
  // `notification` is Android-only — iOS ignores it.
@@ -284,7 +360,10 @@ import BeekonKit
284
360
  whenStationary: stationaryModeFromWire(d["whenStationary"] as? String),
285
361
  stationaryRadiusMeters: stationaryRadius,
286
362
  detectActivity: detectActivity,
287
- sync: sync
363
+ sync: sync,
364
+ // Passed through verbatim; nil/blank means unset — the SDK falls through to
365
+ // the Info.plist value, then evaluation (license-format-v1 §9).
366
+ licenseKey: d["licenseKey"] as? String
288
367
  )
289
368
  }
290
369
 
@@ -321,6 +400,35 @@ import BeekonKit
321
400
  return out
322
401
  }
323
402
 
403
+ private func wireToAuthConfig(_ d: NSDictionary) -> AuthConfig {
404
+ // Wire carries epoch millis; the native recipe wants epoch seconds.
405
+ let expiresAt = (d["expiresAtMs"] as? NSNumber).map { $0.doubleValue / 1000.0 }
406
+ let responseMapping = (d["responseMapping"] as? NSDictionary)
407
+ .map { wireToResponseMapping($0) } ?? AuthResponseMapping()
408
+ return AuthConfig(
409
+ accessToken: d["accessToken"] as? String,
410
+ refreshToken: d["refreshToken"] as? String,
411
+ expiresAt: expiresAt,
412
+ strategy: authStrategyFromWire(d["strategy"] as? String),
413
+ refreshUrl: (d["refreshUrl"] as? String).flatMap { URL(string: $0) },
414
+ refreshPayload: entriesToDict((d["refreshPayload"] as? NSArray) ?? []),
415
+ refreshHeaders: entriesToDict((d["refreshHeaders"] as? NSArray) ?? []),
416
+ refreshBodyFormat: authBodyFormatFromWire(d["refreshBodyFormat"] as? String),
417
+ responseMapping: responseMapping,
418
+ skewMarginSeconds: (d["skewMarginSeconds"] as? NSNumber)?.doubleValue ?? 60,
419
+ seedEpoch: (d["seedEpoch"] as? NSNumber)?.intValue
420
+ )
421
+ }
422
+
423
+ private func wireToResponseMapping(_ d: NSDictionary) -> AuthResponseMapping {
424
+ return AuthResponseMapping(
425
+ accessToken: d["accessToken"] as? String,
426
+ refreshToken: d["refreshToken"] as? String,
427
+ expiresIn: d["expiresIn"] as? String,
428
+ expiresAt: d["expiresAt"] as? String
429
+ )
430
+ }
431
+
324
432
  // MARK: - Mappers: Beekon → wire (NSDictionary)
325
433
 
326
434
  private func locationToWire(_ loc: Location) -> NSDictionary {
@@ -390,6 +498,66 @@ import BeekonKit
390
498
  }
391
499
  }
392
500
 
501
+ private func authTokensToWire(_ t: AuthTokens) -> NSDictionary {
502
+ // NSDictionary literals can't carry `nil`; optionals collapse to `NSNull`,
503
+ // which the Codegen layer translates back to `null` on the JS side.
504
+ let d = NSMutableDictionary()
505
+ d["accessToken"] = t.accessToken
506
+ d["refreshToken"] = t.refreshToken.map { $0 as Any } ?? NSNull()
507
+ // Native epoch seconds → wire epoch millis.
508
+ d["expiresAtMs"] = t.expiresAt.map { ($0 * 1000.0) as Any } ?? NSNull()
509
+ d["epoch"] = t.epoch
510
+ return d
511
+ }
512
+
513
+ private func licenseStatusToWire(_ s: LicenseStatus) -> NSDictionary {
514
+ // NSDictionary literals can't carry `nil`; optionals collapse to `NSNull`,
515
+ // which the Codegen layer translates back to `null` on the JS side. iOS has
516
+ // no `.wireName`, so the wire-stable strings are written literally here (as
517
+ // with stateToWire / syncStatusToWire). No validation — the SDK already
518
+ // decided the status.
519
+ let d = NSMutableDictionary()
520
+ switch s {
521
+ case let .licensed(tier, entitlements):
522
+ d["status"] = "licensed"
523
+ d["tier"] = tier
524
+ d["entitlements"] = entitlements
525
+ d["reason"] = NSNull()
526
+ case let .invalid(reason):
527
+ d["status"] = "invalid"
528
+ d["tier"] = NSNull()
529
+ d["entitlements"] = [String]()
530
+ d["reason"] = reason.rawValue
531
+ case .evaluation:
532
+ d["status"] = "evaluation"
533
+ d["tier"] = NSNull()
534
+ d["entitlements"] = [String]()
535
+ d["reason"] = NSNull()
536
+ case .expired:
537
+ d["status"] = "expired"
538
+ d["tier"] = NSNull()
539
+ d["entitlements"] = [String]()
540
+ d["reason"] = NSNull()
541
+ case .updateEntitlementLapsed:
542
+ d["status"] = "updateEntitlementLapsed"
543
+ d["tier"] = NSNull()
544
+ d["entitlements"] = [String]()
545
+ d["reason"] = NSNull()
546
+ case .notDetermined:
547
+ d["status"] = "notDetermined"
548
+ d["tier"] = NSNull()
549
+ d["entitlements"] = [String]()
550
+ d["reason"] = NSNull()
551
+ // BeekonKit enums are non-frozen (library-evolution binary); handle unknowns.
552
+ @unknown default:
553
+ d["status"] = "notDetermined"
554
+ d["tier"] = NSNull()
555
+ d["entitlements"] = [String]()
556
+ d["reason"] = NSNull()
557
+ }
558
+ return d
559
+ }
560
+
393
561
  // MARK: - Enum mappers
394
562
 
395
563
  private func accuracyModeFromWire(_ s: String?) -> AccuracyMode {
@@ -408,6 +576,20 @@ import BeekonKit
408
576
  }
409
577
  }
410
578
 
579
+ private func authStrategyFromWire(_ s: String?) -> AuthStrategy {
580
+ switch s {
581
+ case "raw": return .raw
582
+ default: return .bearer
583
+ }
584
+ }
585
+
586
+ private func authBodyFormatFromWire(_ s: String?) -> AuthBodyFormat {
587
+ switch s {
588
+ case "json": return .json
589
+ default: return .form
590
+ }
591
+ }
592
+
411
593
  private func stopReasonToWire(_ r: StopReason) -> String {
412
594
  switch r {
413
595
  case .user: return "user"
@@ -483,6 +665,13 @@ import BeekonKit
483
665
  switch be {
484
666
  case .storage: return "STORAGE_FAILURE"
485
667
  case .invalidGeofence: return "INVALID_GEOFENCE"
668
+ case .locationUnavailable(let reason):
669
+ switch reason {
670
+ case .permissionDenied: return "PERMISSION_DENIED"
671
+ case .locationServicesDisabled: return "LOCATION_SERVICES_DISABLED"
672
+ case .unavailable: return "LOCATION_UNAVAILABLE"
673
+ @unknown default: return "LOCATION_UNAVAILABLE"
674
+ }
486
675
  @unknown default: return "INTERNAL_ERROR"
487
676
  }
488
677
  }
@@ -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>