@wayq/beekon-rn 0.1.0 → 0.1.2

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 (44) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +28 -2
  3. package/android/build.gradle +1 -1
  4. package/android/src/main/AndroidManifest.xml +10 -0
  5. package/android/src/main/java/in/wayq/beekonrn/BeekonRnModule.kt +105 -1
  6. package/ios/BeekonRn.swift +60 -10
  7. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/BeekonKit +0 -0
  8. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.abi.json +5472 -2309
  9. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  10. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.swiftinterface +87 -3
  11. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/BeekonKit +0 -0
  12. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.abi.json +5472 -2309
  13. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  14. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +87 -3
  15. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +5472 -2309
  16. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  17. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +87 -3
  18. package/lib/module/NativeBeekonRn.js +8 -0
  19. package/lib/module/NativeBeekonRn.js.map +1 -1
  20. package/lib/module/beekon.js +14 -1
  21. package/lib/module/beekon.js.map +1 -1
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/module/internal/mappers.js +83 -2
  24. package/lib/module/internal/mappers.js.map +1 -1
  25. package/lib/typescript/src/NativeBeekonRn.d.ts +20 -0
  26. package/lib/typescript/src/NativeBeekonRn.d.ts.map +1 -1
  27. package/lib/typescript/src/beekon.d.ts +11 -1
  28. package/lib/typescript/src/beekon.d.ts.map +1 -1
  29. package/lib/typescript/src/index.d.ts +2 -2
  30. package/lib/typescript/src/index.d.ts.map +1 -1
  31. package/lib/typescript/src/internal/mappers.d.ts +3 -2
  32. package/lib/typescript/src/internal/mappers.d.ts.map +1 -1
  33. package/lib/typescript/src/types/geofence.d.ts +37 -0
  34. package/lib/typescript/src/types/geofence.d.ts.map +1 -1
  35. package/lib/typescript/src/types/permission.d.ts +36 -0
  36. package/lib/typescript/src/types/permission.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/scripts/fetch-beekonkit.sh +4 -4
  39. package/src/NativeBeekonRn.ts +24 -0
  40. package/src/beekon.ts +20 -1
  41. package/src/index.tsx +7 -0
  42. package/src/internal/mappers.ts +114 -0
  43. package/src/types/geofence.ts +41 -0
  44. package/src/types/permission.ts +43 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,47 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  Beekon is pre-1.0 — releases may contain breaking changes.
8
8
 
9
+ ## [0.1.2] - 2026-06-28
10
+
11
+ ### Added
12
+
13
+ - **Per-geofence local notifications.** A `BeekonGeofence` may now carry an
14
+ optional `notification` (`GeofenceNotification`) that the SDK renders natively
15
+ at crossing time — offline and even when the app is killed. It takes a
16
+ required `delivery` (`'local'` is the only implemented mode; `'cloud'` is
17
+ reserved) plus optional `onEnter` / `onExit` `NotificationContent` (`title`
18
+ and `body` required; optional `importance` defaulting to `'high'`, `deepLink`,
19
+ and a flat `data` string map). `onEnter` requires `notifyOnEntry` and `onExit`
20
+ requires `notifyOnExit`; in self-managed mode `delivery` must be `'local'`.
21
+ Adds the `GeofenceNotification`, `NotificationContent`, `NotificationDelivery`,
22
+ and `NotificationImportance` types. Requires the native SDKs ≥ 0.1.2.
23
+ - **`getRequiredPermissions()` — config-aware permission doctor.** Resolves the
24
+ OS permissions the *current configuration* needs as a
25
+ `PermissionRequirement[]`, each marked `satisfied` against the live grant —
26
+ the companion to the location-only `getPermissionStatus()` snapshot. Each
27
+ entry carries `permission` (`BeekonPermission`: `location` /
28
+ `backgroundLocation` / `activityRecognition` / `notifications`), `importance`
29
+ (`required` / `recommended`), `satisfied`, and a human-readable `rationale`,
30
+ reporting activity-recognition and, on Android, notifications too. Read-only;
31
+ never prompts — Beekon still never *requests* permission. Call after
32
+ `configure()`; in cloud mode it returns the conservative list. Adds the
33
+ `PermissionRequirement` and `PermissionImportance` types. Requires the native
34
+ SDKs ≥ 0.1.2.
35
+
36
+ ## [0.1.1] - 2026-06-23
37
+
38
+ ### Changed
39
+
40
+ - **Native SDK pins bumped to 0.1.1** — Android Maven coordinate and iOS
41
+ xcframework (`fetch-beekonkit.sh` `VERSION` + `EXPECTED_SHA`) now track
42
+ BeekonKit / `beekon` 0.1.1 (device control plane, build-time license gate,
43
+ and related native fixes since 0.1.0).
44
+
45
+ ### Added
46
+
47
+ - **Build-gate product id** — native registration now declares the React Native
48
+ wrapper product id for build-time license enforcement (build-gate-v1).
49
+
9
50
  ## [0.1.0] - 2026-06-15
10
51
 
11
52
  ### Changed
package/README.md CHANGED
@@ -30,7 +30,7 @@ Capture location in the foreground and background, keep a queryable history on t
30
30
  - Foreground and background tracking that survives app termination
31
31
  - Battery-aware capture: time/distance filtering, accuracy presets, auto-pause while stationary
32
32
  - On-device history you can query at any time
33
- - Geofencing with enter and exit events
33
+ - Geofencing with enter and exit events, plus optional per-geofence local notifications that fire offline and even when the app is killed
34
34
  - Optional batched server sync with automatic retry
35
35
  - Diagnostic logging: query/export an on-device buffer, stream live entries
36
36
  - Activity detection (walking, running, cycling, automotive)
@@ -107,12 +107,38 @@ await Beekon.addGeofences([
107
107
  ]);
108
108
  const offGeofence = Beekon.onGeofenceEvent((e) => console.log(e.geofenceId, e.type));
109
109
 
110
+ // Per-geofence local notification — rendered natively at crossing time,
111
+ // offline and even when the app is killed. Add `notification` to a geofence:
112
+ await Beekon.addGeofences([
113
+ {
114
+ id: 'home',
115
+ latitude: 37.331,
116
+ longitude: -122.031,
117
+ radiusMeters: 150,
118
+ notification: {
119
+ delivery: 'local', // 'local' is the only implemented mode ('cloud' reserved)
120
+ onEnter: { title: 'Welcome home', body: 'You arrived', deepLink: 'myapp://home' },
121
+ onExit: { title: 'On the move', body: 'You left home', importance: 'default' },
122
+ },
123
+ },
124
+ ]);
125
+
110
126
  // One-shot fix, independent of tracking (null on timeout)
111
127
  const fix = await Beekon.getCurrentLocation();
112
128
 
113
129
  // Pre-start permission check (the app still owns the request)
114
130
  const status = await Beekon.getPermissionStatus();
115
131
 
132
+ // Config-aware "permission doctor" — the OS permissions the CURRENT config
133
+ // needs, each marked `satisfied` against the live grant (never prompts).
134
+ // Call after configure(). Reports activity-recognition and, on Android,
135
+ // notifications — not just location.
136
+ const required = await Beekon.getRequiredPermissions();
137
+ for (const r of required) {
138
+ // r.permission, r.importance ('required' | 'recommended'), r.satisfied, r.rationale
139
+ if (!r.satisfied) console.log(`${r.permission} (${r.importance}): ${r.rationale}`);
140
+ }
141
+
116
142
  // Diagnostics — runtime level, live tail, NDJSON export for bug reports
117
143
  await Beekon.setLogLevel('debug');
118
144
  const logPath = await Beekon.exportLog();
@@ -180,7 +206,7 @@ func application(
180
206
 
181
207
  ## API at a glance
182
208
 
183
- Methods: `configure` · `start` · `stop` · `resumeIfNeeded` · `getCurrentLocation` · `getLocations` · `deleteLocations` · `pendingUploadCount` · `sync` · `setExtras` · `addGeofences` · `removeGeofences` · `listGeofences` · `getPermissionStatus` · `licenseStatus` · `getLog` · `exportLog` · `clearLog` · `setLogLevel` · `log`.
209
+ Methods: `configure` · `start` · `stop` · `resumeIfNeeded` · `getCurrentLocation` · `getLocations` · `deleteLocations` · `pendingUploadCount` · `sync` · `setExtras` · `addGeofences` · `removeGeofences` · `listGeofences` · `getPermissionStatus` · `getRequiredPermissions` · `licenseStatus` · `getLog` · `exportLog` · `clearLog` · `setLogLevel` · `log`.
184
210
 
185
211
  Subscriptions (each returns an unsubscribe function): `onState` · `onLocation` · `onGeofenceEvent` · `onSyncStatus` · `onAuthTokens` · `onLicenseStatus` · `onLog`.
186
212
 
@@ -79,7 +79,7 @@ dependencies {
79
79
  // the SDK reaches v1 stability. 0.0.7 is the release that ships the license
80
80
  // API (setWrapperInfo / BeekonConfig.licenseKey / Beekon.licenseStatus);
81
81
  // 0.0.8 embeds the production ES256 verification keyset.
82
- implementation "io.github.beekonlabs:beekon:0.1.0"
82
+ implementation "io.github.beekonlabs:beekon:0.1.2"
83
83
  // Kotlin coroutines — required for collecting Beekon's StateFlow/SharedFlow.
84
84
  // Beekon already depends on coroutines transitively, but declaring it here
85
85
  // makes the dependency intent explicit.
@@ -1,2 +1,12 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!--
3
+ Declares the build-time license-gate product id (build-gate-v1 §6): a static
4
+ <meta-data> that manifest-merges into the host so the gate validates against
5
+ the `rn` product, mirroring the runtime setWrapperInfo("rn", …). No runtime effect.
6
+ -->
7
+ <application>
8
+ <meta-data
9
+ android:name="in.wayq.beekon.product"
10
+ android:value="rn" />
11
+ </application>
2
12
  </manifest>
@@ -18,8 +18,10 @@ import `in`.wayq.beekon.Beekon
18
18
  import `in`.wayq.beekon.BeekonConfig
19
19
  import `in`.wayq.beekon.BeekonException
20
20
  import `in`.wayq.beekon.BeekonGeofence
21
+ import `in`.wayq.beekon.BeekonPermission
21
22
  import `in`.wayq.beekon.BeekonState
22
23
  import `in`.wayq.beekon.GeofenceEvent
24
+ import `in`.wayq.beekon.GeofenceNotification
23
25
  import `in`.wayq.beekon.LicenseStatus
24
26
  import `in`.wayq.beekon.Location
25
27
  import `in`.wayq.beekon.LocationQuality
@@ -29,6 +31,11 @@ import `in`.wayq.beekon.LogEntry
29
31
  import `in`.wayq.beekon.LogLevel
30
32
  import `in`.wayq.beekon.MotionState
31
33
  import `in`.wayq.beekon.NotificationConfig
34
+ import `in`.wayq.beekon.NotificationContent
35
+ import `in`.wayq.beekon.NotificationDelivery
36
+ import `in`.wayq.beekon.NotificationImportance
37
+ import `in`.wayq.beekon.PermissionImportance
38
+ import `in`.wayq.beekon.PermissionRequirement
32
39
  import `in`.wayq.beekon.PermissionStatus
33
40
  import `in`.wayq.beekon.StationaryMode
34
41
  import `in`.wayq.beekon.StopReason
@@ -38,6 +45,7 @@ import `in`.wayq.beekon.SyncStatus
38
45
  import `in`.wayq.beekon.Transition
39
46
  import java.time.Instant
40
47
  import kotlinx.coroutines.CoroutineScope
48
+ import org.json.JSONObject
41
49
  import kotlinx.coroutines.Dispatchers
42
50
  import kotlinx.coroutines.SupervisorJob
43
51
  import kotlinx.coroutines.cancel
@@ -241,6 +249,13 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
241
249
  promise.resolve(permissionStatusToWire(Beekon.getPermissionStatus()))
242
250
  }
243
251
 
252
+ override fun getRequiredPermissions(promise: Promise) {
253
+ // Synchronous, non-throwing read on the native SDK.
254
+ val arr: WritableArray = Arguments.createArray()
255
+ for (r in Beekon.getRequiredPermissions()) arr.pushMap(permissionRequirementToWire(r))
256
+ promise.resolve(arr)
257
+ }
258
+
244
259
  // ---------------------------------------------------------------------------
245
260
  // Diagnostic logs (spec diagnostics/log-format-v1)
246
261
  // ---------------------------------------------------------------------------
@@ -446,6 +461,7 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
446
461
  radiusMeters = m.getDouble("radiusMeters"),
447
462
  notifyOnEntry = m.getBoolean("notifyOnEntry"),
448
463
  notifyOnExit = m.getBoolean("notifyOnExit"),
464
+ notification = GeofenceNotificationWire.decode(m.getString("notificationJson")),
449
465
  )
450
466
  }
451
467
  return out
@@ -494,6 +510,7 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
494
510
  m.putDouble("radiusMeters", g.radiusMeters)
495
511
  m.putBoolean("notifyOnEntry", g.notifyOnEntry)
496
512
  m.putBoolean("notifyOnExit", g.notifyOnExit)
513
+ g.notification?.let { m.putString("notificationJson", GeofenceNotificationWire.encode(it)) }
497
514
  return m
498
515
  }
499
516
 
@@ -540,6 +557,27 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
540
557
  PermissionStatus.Accuracy.Reduced -> "reduced"
541
558
  }
542
559
 
560
+ private fun permissionRequirementToWire(r: PermissionRequirement): WritableMap {
561
+ val m = Arguments.createMap()
562
+ m.putString("permission", permissionKindToWire(r.permission))
563
+ m.putString("importance", permissionImportanceToWire(r.importance))
564
+ m.putBoolean("satisfied", r.satisfied)
565
+ m.putString("rationale", r.rationale)
566
+ return m
567
+ }
568
+
569
+ private fun permissionKindToWire(p: BeekonPermission): String = when (p) {
570
+ BeekonPermission.Location -> "location"
571
+ BeekonPermission.BackgroundLocation -> "backgroundLocation"
572
+ BeekonPermission.ActivityRecognition -> "activityRecognition"
573
+ BeekonPermission.Notifications -> "notifications"
574
+ }
575
+
576
+ private fun permissionImportanceToWire(i: PermissionImportance): String = when (i) {
577
+ PermissionImportance.Required -> "required"
578
+ PermissionImportance.Recommended -> "recommended"
579
+ }
580
+
543
581
  private fun syncStatusToWire(s: SyncStatus): WritableMap {
544
582
  val m = Arguments.createMap()
545
583
  when (s) {
@@ -705,6 +743,72 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
705
743
  // Reported to the native verifier via setWrapperInfo (diagnostics only — the
706
744
  // verifier consumes only the product). Keep in sync with package.json
707
745
  // "version" on release.
708
- private const val WRAPPER_VERSION = "0.1.0"
746
+ private const val WRAPPER_VERSION = "0.1.2"
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Wrapper-local (de)serialization for [GeofenceNotification] in the canonical JSON shape shared
752
+ * with the native SDKs. The SDK's own `GeofenceNotificationJson` is `internal`, so the module
753
+ * cannot reuse it; this mirrors it exactly. Tolerant: unknown enums fall back to defaults and a
754
+ * malformed/empty payload yields null.
755
+ */
756
+ private object GeofenceNotificationWire {
757
+ fun encode(n: GeofenceNotification): String {
758
+ val obj = JSONObject()
759
+ obj.put("delivery", if (n.delivery == NotificationDelivery.Cloud) "cloud" else "local")
760
+ n.onEnter?.let { obj.put("onEnter", contentToJson(it)) }
761
+ n.onExit?.let { obj.put("onExit", contentToJson(it)) }
762
+ return obj.toString()
763
+ }
764
+
765
+ fun decode(json: String?): GeofenceNotification? {
766
+ if (json.isNullOrBlank()) return null
767
+ return runCatching {
768
+ val obj = JSONObject(json)
769
+ val onEnter = contentFromJson(obj.optJSONObject("onEnter"))
770
+ val onExit = contentFromJson(obj.optJSONObject("onExit"))
771
+ if (onEnter == null && onExit == null) {
772
+ null
773
+ } else {
774
+ val delivery = if (obj.optString("delivery", "local") == "cloud") {
775
+ NotificationDelivery.Cloud
776
+ } else {
777
+ NotificationDelivery.Local
778
+ }
779
+ GeofenceNotification(onEnter = onEnter, onExit = onExit, delivery = delivery)
780
+ }
781
+ }.getOrNull()
782
+ }
783
+
784
+ private fun contentToJson(c: NotificationContent): JSONObject {
785
+ val obj = JSONObject()
786
+ obj.put("title", c.title)
787
+ obj.put("body", c.body)
788
+ obj.put("importance", if (c.importance == NotificationImportance.Default) "default" else "high")
789
+ c.deepLink?.let { obj.put("deepLink", it) }
790
+ if (c.data.isNotEmpty()) {
791
+ val data = JSONObject()
792
+ for ((k, v) in c.data) data.put(k, v)
793
+ obj.put("data", data)
794
+ }
795
+ return obj
796
+ }
797
+
798
+ private fun contentFromJson(obj: JSONObject?): NotificationContent? {
799
+ if (obj == null) return null
800
+ val title = obj.optString("title", "")
801
+ val body = obj.optString("body", "")
802
+ if (title.isEmpty() || body.isEmpty()) return null
803
+ val importance = if (obj.optString("importance", "high") == "default") {
804
+ NotificationImportance.Default
805
+ } else {
806
+ NotificationImportance.High
807
+ }
808
+ val deepLink = if (obj.has("deepLink") && !obj.isNull("deepLink")) obj.optString("deepLink") else null
809
+ val data = obj.optJSONObject("data")?.let { d ->
810
+ buildMap { for (key in d.keys()) put(key, d.optString(key)) }
811
+ } ?: emptyMap()
812
+ return NotificationContent(title = title, body = body, importance = importance, deepLink = deepLink, data = data)
709
813
  }
710
814
  }
@@ -37,7 +37,7 @@ import BeekonKit
37
37
  // Reported to the native verifier via setWrapperInfo (diagnostics only — the
38
38
  // verifier consumes only the product). Keep in sync with package.json
39
39
  // "version" on release.
40
- private static let wrapperVersion = "0.1.0"
40
+ private static let wrapperVersion = "0.1.2"
41
41
 
42
42
  @objc public init(
43
43
  onState: @escaping (NSDictionary) -> Void,
@@ -353,6 +353,18 @@ import BeekonKit
353
353
  resolve(permissionStatusToWire(Beekon.shared.getPermissionStatus()))
354
354
  }
355
355
 
356
+ @objc public func getRequiredPermissionsWithResolver(
357
+ _ resolve: @escaping @Sendable (Any?) -> Void,
358
+ rejecter _: @escaping @Sendable (String?, String?, Error?) -> Void
359
+ ) {
360
+ // `getRequiredPermissions()` is actor-isolated (`await`).
361
+ Task { [weak self] in
362
+ guard let self = self else { return }
363
+ let requirements = await Beekon.shared.getRequiredPermissions()
364
+ resolve(requirements.map { self.permissionRequirementToWire($0) })
365
+ }
366
+ }
367
+
356
368
  // MARK: - Diagnostic logs (spec diagnostics/log-format-v1)
357
369
 
358
370
  @objc public func getLogFromMs(
@@ -504,7 +516,12 @@ import BeekonKit
504
516
  longitude: lng,
505
517
  radiusMeters: radius,
506
518
  notifyOnEntry: (m["notifyOnEntry"] as? NSNumber)?.boolValue ?? true,
507
- notifyOnExit: (m["notifyOnExit"] as? NSNumber)?.boolValue ?? true
519
+ notifyOnExit: (m["notifyOnExit"] as? NSNumber)?.boolValue ?? true,
520
+ // Native `GeofenceNotification` is Codable in the canonical shape; a malformed
521
+ // string degrades to nil rather than failing the add.
522
+ notification: (m["notificationJson"] as? String).flatMap {
523
+ try? JSONDecoder().decode(GeofenceNotification.self, from: Data($0.utf8))
524
+ }
508
525
  )
509
526
  )
510
527
  }
@@ -573,14 +590,19 @@ import BeekonKit
573
590
  }
574
591
 
575
592
  private func geofenceToWire(_ g: BeekonGeofence) -> NSDictionary {
576
- return [
577
- "id": g.id,
578
- "lat": g.latitude,
579
- "lng": g.longitude,
580
- "radiusMeters": g.radiusMeters,
581
- "notifyOnEntry": g.notifyOnEntry,
582
- "notifyOnExit": g.notifyOnExit,
583
- ]
593
+ let m = NSMutableDictionary()
594
+ m["id"] = g.id
595
+ m["lat"] = g.latitude
596
+ m["lng"] = g.longitude
597
+ m["radiusMeters"] = g.radiusMeters
598
+ m["notifyOnEntry"] = g.notifyOnEntry
599
+ m["notifyOnExit"] = g.notifyOnExit
600
+ if let notification = g.notification,
601
+ let data = try? JSONEncoder().encode(notification),
602
+ let json = String(data: data, encoding: .utf8) {
603
+ m["notificationJson"] = json
604
+ }
605
+ return m
584
606
  }
585
607
 
586
608
  private func geofenceEventToWire(_ e: GeofenceEvent) -> NSDictionary {
@@ -630,6 +652,34 @@ import BeekonKit
630
652
  }
631
653
  }
632
654
 
655
+ private func permissionRequirementToWire(_ r: PermissionRequirement) -> NSDictionary {
656
+ return [
657
+ "permission": permissionKindToWire(r.permission),
658
+ "importance": permissionImportanceToWire(r.importance),
659
+ "satisfied": r.satisfied,
660
+ "rationale": r.rationale,
661
+ ]
662
+ }
663
+
664
+ private func permissionKindToWire(_ p: PermissionRequirement.Permission) -> String {
665
+ switch p {
666
+ case .location: return "location"
667
+ case .backgroundLocation: return "backgroundLocation"
668
+ case .activityRecognition: return "activityRecognition"
669
+ case .notifications: return "notifications"
670
+ // BeekonKit enums are non-frozen (library-evolution binary); handle unknowns.
671
+ @unknown default: return "location"
672
+ }
673
+ }
674
+
675
+ private func permissionImportanceToWire(_ i: PermissionRequirement.Importance) -> String {
676
+ switch i {
677
+ case .required: return "required"
678
+ case .recommended: return "recommended"
679
+ @unknown default: return "recommended"
680
+ }
681
+ }
682
+
633
683
  private func stateToWire(_ s: BeekonState) -> NSDictionary {
634
684
  switch s {
635
685
  case .idle: