@wayq/beekon-rn 0.0.7 → 0.0.9

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 (63) hide show
  1. package/BeekonRn.podspec +1 -1
  2. package/CHANGELOG.md +43 -0
  3. package/README.md +67 -5
  4. package/android/build.gradle +3 -2
  5. package/android/src/main/java/in/wayq/beekonrn/BeekonRnModule.kt +106 -8
  6. package/ios/BeekonRn.mm +35 -0
  7. package/ios/BeekonRn.swift +140 -8
  8. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/BeekonKit +0 -0
  9. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Info.plist +0 -0
  10. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.abi.json +6048 -3656
  11. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  12. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.swiftinterface +95 -18
  13. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/BeekonKit +0 -0
  14. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Info.plist +0 -0
  15. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.abi.json +6048 -3656
  16. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  17. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +95 -18
  18. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +6048 -3656
  19. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  20. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +95 -18
  21. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/_CodeSignature/CodeResources +1 -1
  22. package/lib/module/NativeBeekonRn.js.map +1 -1
  23. package/lib/module/beekon.js +97 -1
  24. package/lib/module/beekon.js.map +1 -1
  25. package/lib/module/index.js +4 -0
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/internal/licenseNudge.js +64 -0
  28. package/lib/module/internal/licenseNudge.js.map +1 -0
  29. package/lib/module/internal/mappers.js +56 -8
  30. package/lib/module/internal/mappers.js.map +1 -1
  31. package/lib/module/types/config.js +73 -1
  32. package/lib/module/types/config.js.map +1 -1
  33. package/lib/module/types/log.js +4 -0
  34. package/lib/module/types/log.js.map +1 -0
  35. package/lib/typescript/src/NativeBeekonRn.d.ts +47 -2
  36. package/lib/typescript/src/NativeBeekonRn.d.ts.map +1 -1
  37. package/lib/typescript/src/beekon.d.ts +46 -1
  38. package/lib/typescript/src/beekon.d.ts.map +1 -1
  39. package/lib/typescript/src/index.d.ts +4 -2
  40. package/lib/typescript/src/index.d.ts.map +1 -1
  41. package/lib/typescript/src/internal/licenseNudge.d.ts +38 -0
  42. package/lib/typescript/src/internal/licenseNudge.d.ts.map +1 -0
  43. package/lib/typescript/src/internal/mappers.d.ts +3 -1
  44. package/lib/typescript/src/internal/mappers.d.ts.map +1 -1
  45. package/lib/typescript/src/types/config.d.ts +90 -7
  46. package/lib/typescript/src/types/config.d.ts.map +1 -1
  47. package/lib/typescript/src/types/enums.d.ts +8 -0
  48. package/lib/typescript/src/types/enums.d.ts.map +1 -1
  49. package/lib/typescript/src/types/log.d.ts +22 -0
  50. package/lib/typescript/src/types/log.d.ts.map +1 -0
  51. package/lib/typescript/src/types/state.d.ts +4 -1
  52. package/lib/typescript/src/types/state.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/scripts/fetch-beekonkit.sh +4 -4
  55. package/src/NativeBeekonRn.ts +49 -2
  56. package/src/beekon.ts +100 -1
  57. package/src/index.tsx +7 -1
  58. package/src/internal/licenseNudge.ts +80 -0
  59. package/src/internal/mappers.ts +67 -7
  60. package/src/types/config.ts +99 -7
  61. package/src/types/enums.ts +9 -0
  62. package/src/types/log.ts +22 -0
  63. package/src/types/state.ts +4 -0
package/BeekonRn.podspec CHANGED
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
11
11
  s.authors = package["author"]
12
12
 
13
13
  # iOS 17.0 floor reflects this wrapper's React Native baseline (RN 0.85), not
14
- # a BeekonKit constraint — BeekonKit 0.0.7 itself supports iOS 13+ (with
14
+ # a BeekonKit constraint — BeekonKit 0.0.9 itself supports iOS 13+ (with
15
15
  # 13–16 fallback paths). Kept at 17.0 to avoid any consumer regression; do not
16
16
  # lower below React Native's own minimum.
17
17
  s.platforms = { :ios => "17.0" }
package/CHANGELOG.md CHANGED
@@ -6,6 +6,49 @@ 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.0.9] - 2026-06-14
10
+
11
+ ### Added
12
+
13
+ - **Cloud mode** (cloud-mode-v1): point a project key (`bkproj_…`) at Beekon
14
+ Cloud and let the server own tracking config, geofences, and the license.
15
+ Built via `BeekonConfig.cloud(...)`; cloud config takes the project key and an
16
+ optional `endpoint` (default `https://api.getbeekon.com`). Requires the native
17
+ SDKs ≥ 0.0.9.
18
+ - **Diagnostic logging** (log-format-v1): `getLog` / `exportLog` / `clearLog` /
19
+ `setLogLevel` / `log` plus the `onLog` subscription and `LogEntry` / `LogLevel`
20
+ types; `logLevel` on each config arm sets the threshold. Requires the native
21
+ SDKs ≥ 0.0.9.
22
+ - **`SyncConfig.syncThreshold`** ([beekon#20]): pending-fix count that triggers
23
+ an early upload of regular fixes ahead of the `intervalSeconds` schedule (not
24
+ subject to the Android ~15 min WorkManager floor). `0` (the default) leaves
25
+ regular fixes to the schedule. Independent of this setting, a geofence event
26
+ and a session stop with pending fixes always flush immediately while sync is
27
+ configured. Requires the native SDKs ≥ 0.0.9.
28
+
29
+ ### Changed
30
+
31
+ - **BREAKING:** `BeekonConfig` is now a sealed two-arm discriminated union
32
+ (`CloudConfig | SelfManagedConfig`) built via `BeekonConfig.cloud(...)` /
33
+ `BeekonConfig.selfManaged(...)`; the flat config object is gone and a `mode`
34
+ discriminator (`'cloud'` | `'selfManaged'`) is now required. Tracking params,
35
+ `sync`, and `licenseKey` live only on the `selfManaged` arm.
36
+
37
+ [beekon#20]: https://github.com/wayqteam/beekon/issues/20
38
+
39
+ ## [0.0.8]
40
+
41
+ Built against the native **0.0.8** API; requires native ≥ 0.0.8 at runtime.
42
+ No binding API changes.
43
+
44
+ ### Changed
45
+
46
+ - Native pins bumped to `0.0.8` (Maven `io.github.wayqteam:beekon`, `BeekonKit`
47
+ xcframework). 0.0.8 embeds the production ES256 license verification keyset,
48
+ so genuine `license-format-v1` tokens resolve to `licensed` / `evaluation`
49
+ on-device, and hardens wire/license conformance. The license surface remains a
50
+ pure pass-through — no validation in the binding.
51
+
9
52
  ## [0.0.7]
10
53
 
11
54
  Built against the native **0.0.7** API; requires native ≥ 0.0.7 at runtime.
package/README.md CHANGED
@@ -33,6 +33,8 @@ Capture GPS in the foreground and background, keep a queryable history on the de
33
33
  - On-device history you can query at any time
34
34
  - Geofencing with enter and exit events
35
35
  - Optional batched server sync with automatic retry
36
+ - Beekon Cloud mode: point at a project with a `bkproj_` key and let the server own tracking config, geofences, and license
37
+ - Diagnostic logging: query/export an on-device log buffer and stream live entries for field debugging
36
38
  - Activity detection (walking, running, cycling, automotive)
37
39
  - Built for the React Native New Architecture
38
40
 
@@ -133,6 +135,7 @@ const offLocation = Beekon.onLocation((location) => {
133
135
 
134
136
  // Configure is optional. Request OS permissions before starting.
135
137
  await Beekon.configure({
138
+ mode: 'selfManaged',
136
139
  minTimeBetweenLocationsSeconds: 30,
137
140
  minDistanceBetweenLocationsMeters: 100,
138
141
  });
@@ -151,7 +154,7 @@ offLocation();
151
154
  Beekon.onState((state) => {
152
155
  if (state.kind === 'stopped') {
153
156
  // 'user' | 'permissionDenied' | 'locationServicesDisabled'
154
- // | 'locationUnavailable' | 'system'
157
+ // | 'locationUnavailable' | 'cloudModeUnavailable' | 'system'
155
158
  console.log('stopped:', state.reason);
156
159
  }
157
160
  });
@@ -190,6 +193,7 @@ Add a `sync` config to upload recorded fixes and geofence events to your backend
190
193
 
191
194
  ```ts
192
195
  await Beekon.configure({
196
+ mode: 'selfManaged',
193
197
  sync: {
194
198
  url: 'https://api.example.com/locations',
195
199
  headers: { Authorization: 'Bearer <token>' },
@@ -212,6 +216,7 @@ The supplied tokens are a **seed**: the SDK owns and rotates the live token set
212
216
 
213
217
  ```ts
214
218
  await Beekon.configure({
219
+ mode: 'selfManaged',
215
220
  sync: {
216
221
  url: 'https://api.example.com/locations',
217
222
  auth: {
@@ -237,9 +242,33 @@ const offAuth = Beekon.onAuthTokens((tokens) => {
237
242
 
238
243
  The refresh recipe is persisted in plaintext to survive a cold launch — **do not** put static client secrets in `refreshHeaders` or `refreshPayload`. Requires the native SDK ≥ 0.0.6; with an older native binary the recipe is ignored and only static `sync.headers` apply.
239
244
 
245
+ ### License
246
+
247
+ Supply a Beekon license token (a `license-format-v1` JWS) with `licenseKey`. It is the highest-priority channel, overriding the platform manifest value (`in.wayq.beekon.license` meta-data on Android, `BeekonLicenseKey` in `Info.plist` on iOS); `undefined`, blank, or whitespace-only means unset. A token is app-id- and product-bound, so it is safe to commit to source control.
248
+
249
+ ```ts
250
+ await Beekon.configure({
251
+ mode: 'selfManaged',
252
+ licenseKey: '<license-format-v1 JWS>',
253
+ });
254
+ ```
255
+
256
+ The license is **purely observational** — no status ever blocks, degrades, or delays the SDK. Read the current platform's status once with `licenseStatus()`, or subscribe with `onLicenseStatus` (the latest status is delivered immediately):
257
+
258
+ ```ts
259
+ const status = await Beekon.licenseStatus();
260
+ console.log('license:', status.status); // 'evaluation' | 'licensed' | ...
261
+
262
+ const offLicense = Beekon.onLicenseStatus((s) => {
263
+ if (s.status === 'licensed') console.log('tier:', s.tier);
264
+ });
265
+ ```
266
+
267
+ Android and iOS validate independently and may legally report different statuses for the same app (for example a license scoped to `["android"]` reads `licensed` on Android and `invalid` / `productMismatch` on iOS). The value reflects only the platform you are running on.
268
+
240
269
  ## Configuration
241
270
 
242
- All options are optional; defaults are shown.
271
+ `BeekonConfig` is a sealed two-arm union — pass `mode: 'selfManaged'` (the options below, all optional) or `mode: 'cloud'` (`{ projectKey, endpoint?, notification?, logLevel? }`, where the server owns tracking/geofences/license). Self-managed options (defaults shown):
243
272
 
244
273
  | Option | Default | Description |
245
274
  |---|---|---|
@@ -249,8 +278,9 @@ All options are optional; defaults are shown.
249
278
  | `whenStationary` | `'pause'` | `'keepTracking'` \| `'pause'` \| `'pauseWithCheckIns'`. |
250
279
  | `stationaryRadiusMeters` | `5` | Distance the device must move to count as moving again. |
251
280
  | `detectActivity` | `false` | Detect physical activity. Requires the motion permission. |
252
- | `sync` | – | Server upload settings: `{ url, headers?, intervalSeconds?, batchSize?, auth? }`. See [Token refresh](#token-refresh). |
281
+ | `sync` | – | Server upload settings: `{ url, headers?, intervalSeconds?, batchSize?, syncThreshold?, auth? }`. See [Token refresh](#token-refresh). |
253
282
  | `notification` | – | Android-only tracking notification: `{ title?, text?, smallIcon? }`. `smallIcon` is a drawable/mipmap resource name. |
283
+ | `licenseKey` | – | Beekon license token (`license-format-v1` JWS). Highest-priority supply channel; overrides the platform manifest value. See [License](#license). |
254
284
 
255
285
  A fix is recorded only when **both** the time and distance filters are satisfied.
256
286
 
@@ -271,6 +301,12 @@ A fix is recorded only when **both** the time and distance filters are satisfied
271
301
  | `addGeofences(geofences)` | Register circular regions. |
272
302
  | `removeGeofences(ids)` | Unregister geofences by id. |
273
303
  | `listGeofences()` | Currently registered geofences. |
304
+ | `licenseStatus()` | Current platform's license status (a `LicenseStatus`). Observational only. See [License](#license). |
305
+ | `getLog(from, to)` | Persisted diagnostic log entries in a date range (oldest first). |
306
+ | `exportLog()` | Serialize the log buffer to an NDJSON file; resolves the file path. |
307
+ | `clearLog()` | Delete all persisted diagnostic log entries. |
308
+ | `setLogLevel(level)` | Change the diagnostic log verbosity threshold at runtime. |
309
+ | `log(level, message)` | Write a host-app breadcrumb into the SDK log pipeline (category `app`). |
274
310
 
275
311
  Subscriptions — each returns an unsubscribe function:
276
312
 
@@ -281,6 +317,8 @@ Subscriptions — each returns an unsubscribe function:
281
317
  | `onGeofenceEvent(cb)` | Geofence enter and exit events. |
282
318
  | `onSyncStatus(cb)` | Upload status. The current status is delivered immediately. |
283
319
  | `onAuthTokens(cb)` | Token rotations from native refresh (see [Token refresh](#token-refresh)). The latest rotation is delivered immediately. |
320
+ | `onLicenseStatus(cb)` | License status changes (see [License](#license)). The current status is delivered immediately. |
321
+ | `onLog(cb)` | Diagnostic log entries as they are recorded (broadcast, no replay). |
284
322
 
285
323
  `getLocations`, `deleteLocations`, `pendingUploadCount`, and `addGeofences` reject with a `BeekonError` on failure — check `error.kind` (`'storage'` or `'invalidGeofence'`). `getCurrentLocation` rejects with `error.kind` `'permissionDenied'`, `'locationServicesDisabled'`, or `'locationUnavailable'` on a precondition failure. Other methods resolve normally; tracking-lifecycle problems are reported through `onState` rather than thrown.
286
324
 
@@ -312,7 +350,7 @@ type BeekonState =
312
350
  kind: 'stopped';
313
351
  reason:
314
352
  | 'user' | 'permissionDenied' | 'locationServicesDisabled'
315
- | 'locationUnavailable' | 'system';
353
+ | 'locationUnavailable' | 'cloudModeUnavailable' | 'system';
316
354
  };
317
355
 
318
356
  type SyncStatus =
@@ -365,6 +403,30 @@ type AuthTokens = {
365
403
  expiresAt: Date | null;
366
404
  epoch: number; // monotonic generation, bumped on each refresh
367
405
  };
406
+
407
+ // Returned by `licenseStatus()` / delivered by `onLicenseStatus`. Observational
408
+ // only — no status ever blocks the SDK. Use `status` as the discriminator.
409
+ type LicenseStatus =
410
+ | { status: 'notDetermined' }
411
+ | { status: 'evaluation' }
412
+ | { status: 'expired' }
413
+ | { status: 'updateEntitlementLapsed' }
414
+ | { status: 'licensed'; tier: string; entitlements: string[] }
415
+ | { status: 'invalid'; reason: LicenseInvalidReason };
416
+
417
+ type LicenseInvalidReason =
418
+ | 'malformed' | 'unknownKey' | 'badSignature'
419
+ | 'appIdMismatch' | 'productMismatch' | 'unsupportedVersion';
420
+
421
+ type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug' | 'verbose';
422
+
423
+ type LogEntry = {
424
+ id: string; // UUIDv7
425
+ timestamp: Date;
426
+ level: LogLevel;
427
+ category: string; // e.g. 'location', 'sync'; host breadcrumbs use 'app'
428
+ message: string;
429
+ };
368
430
  ```
369
431
 
370
432
  Optional numeric fields on `Location` are `null` when the device did not report them — never `0`.
@@ -381,7 +443,7 @@ A complete, runnable example is in the [`example/`](./example) directory.
381
443
 
382
444
  ## License
383
445
 
384
- Proprietary — evaluation use only without a separate written agreement with wayqteam. See [LICENSE.txt](./LICENSE.txt).
446
+ Proprietary — evaluation use only without a separate written agreement with WayQ Technologies Pvt Ltd. See [LICENSE.txt](./LICENSE.txt).
385
447
 
386
448
  ---
387
449
 
@@ -77,8 +77,9 @@ dependencies {
77
77
  // Native Beekon SDK — published from beekon-android/ via Maven Central.
78
78
  // Pinned exact in v0.x to avoid surprise breakage; loosen to a range when
79
79
  // the SDK reaches v1 stability. 0.0.7 is the release that ships the license
80
- // API (setWrapperInfo / BeekonConfig.licenseKey / Beekon.licenseStatus).
81
- implementation "io.github.wayqteam:beekon:0.0.7"
80
+ // API (setWrapperInfo / BeekonConfig.licenseKey / Beekon.licenseStatus);
81
+ // 0.0.8 embeds the production ES256 verification keyset.
82
+ implementation "io.github.wayqteam:beekon:0.0.9"
82
83
  // Kotlin coroutines — required for collecting Beekon's StateFlow/SharedFlow.
83
84
  // Beekon already depends on coroutines transitively, but declaring it here
84
85
  // makes the dependency intent explicit.
@@ -25,6 +25,8 @@ import `in`.wayq.beekon.Location
25
25
  import `in`.wayq.beekon.LocationQuality
26
26
  import `in`.wayq.beekon.LocationTrigger
27
27
  import `in`.wayq.beekon.LocationUnavailableReason
28
+ import `in`.wayq.beekon.LogEntry
29
+ import `in`.wayq.beekon.LogLevel
28
30
  import `in`.wayq.beekon.MotionState
29
31
  import `in`.wayq.beekon.NotificationConfig
30
32
  import `in`.wayq.beekon.StationaryMode
@@ -64,6 +66,7 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
64
66
  scope.launch { Beekon.syncStatus.collect { emitOnSyncStatus(syncStatusToWire(it)) } }
65
67
  scope.launch { Beekon.authChanges.collect { emitOnAuthTokens(tokenRefreshToWire(it)) } }
66
68
  scope.launch { Beekon.licenseStatus.collect { emitOnLicenseStatus(licenseStatusToWire(it)) } }
69
+ scope.launch { Beekon.logs.collect { emitOnLog(logEntryToWire(it)) } }
67
70
  }
68
71
 
69
72
  // ---------------------------------------------------------------------------
@@ -232,6 +235,55 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
232
235
  promise.resolve(licenseStatusToWire(Beekon.licenseStatus.value))
233
236
  }
234
237
 
238
+ // ---------------------------------------------------------------------------
239
+ // Diagnostic logs (spec diagnostics/log-format-v1)
240
+ // ---------------------------------------------------------------------------
241
+
242
+ override fun getLog(fromMs: Double, toMs: Double, promise: Promise) {
243
+ scope.launch {
244
+ try {
245
+ val from = Instant.ofEpochMilli(fromMs.toLong())
246
+ val to = Instant.ofEpochMilli(toMs.toLong())
247
+ val arr: WritableArray = Arguments.createArray()
248
+ for (e in Beekon.getLog(from, to)) arr.pushMap(logEntryToWire(e))
249
+ promise.resolve(arr)
250
+ } catch (t: Throwable) {
251
+ promise.reject(errorCode(t), t.message ?: "getLog failed", t)
252
+ }
253
+ }
254
+ }
255
+
256
+ override fun exportLog(promise: Promise) {
257
+ scope.launch {
258
+ try {
259
+ promise.resolve(Beekon.exportLog())
260
+ } catch (t: Throwable) {
261
+ promise.reject(errorCode(t), t.message ?: "exportLog failed", t)
262
+ }
263
+ }
264
+ }
265
+
266
+ override fun clearLog(promise: Promise) {
267
+ scope.launch {
268
+ try {
269
+ Beekon.clearLog()
270
+ promise.resolve(null)
271
+ } catch (t: Throwable) {
272
+ promise.reject(errorCode(t), t.message ?: "clearLog failed", t)
273
+ }
274
+ }
275
+ }
276
+
277
+ override fun setLogLevel(level: String, promise: Promise) {
278
+ Beekon.setLogLevel(toLogLevel(level))
279
+ promise.resolve(null)
280
+ }
281
+
282
+ override fun log(level: String, message: String, promise: Promise) {
283
+ Beekon.log(toLogLevel(level), message)
284
+ promise.resolve(null)
285
+ }
286
+
235
287
  override fun invalidate() {
236
288
  super.invalidate()
237
289
  scope.cancel()
@@ -241,20 +293,47 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
241
293
  // Mappers: wire (ReadableMap/Array) → Kotlin
242
294
  // ---------------------------------------------------------------------------
243
295
 
296
+ // GATED on beekon native 0.0.9 (sealed BeekonConfig) — does not compile against
297
+ // the pinned 0.0.8 dep (the flat BeekonConfig(...) constructor). Switches on the
298
+ // wire `mode` (cloud-mode-v1 §2) to build the matching sealed arm. A cloud
299
+ // configuration with an empty/invalid projectKey throws
300
+ // BeekonException.InvalidConfiguration, which the configure() catch turns into a
301
+ // promise rejection via errorCode().
244
302
  private fun wireToConfig(map: ReadableMap): BeekonConfig {
245
- val sync =
246
- if (map.hasKey("sync") && !map.isNull("sync")) {
247
- map.getMap("sync")?.let { wireToSyncConfig(it) }
248
- } else {
249
- null
250
- }
251
303
  val notification =
252
304
  if (map.hasKey("notification") && !map.isNull("notification")) {
253
305
  map.getMap("notification")?.let { wireToNotification(it) }
254
306
  } else {
255
307
  null
256
308
  }
257
- return BeekonConfig(
309
+
310
+ if (optString(map, "mode") == "cloud") {
311
+ val projectKey = optString(map, "projectKey") ?: ""
312
+ val endpoint = optString(map, "endpoint")
313
+ // Omitted endpoint → fall back to the constructor's baked-in default.
314
+ return if (endpoint != null) {
315
+ BeekonConfig.Cloud(
316
+ projectKey = projectKey,
317
+ endpoint = endpoint,
318
+ notification = notification ?: NotificationConfig(),
319
+ logLevel = toLogLevel(optString(map, "logLevel")),
320
+ )
321
+ } else {
322
+ BeekonConfig.Cloud(
323
+ projectKey = projectKey,
324
+ notification = notification ?: NotificationConfig(),
325
+ logLevel = toLogLevel(optString(map, "logLevel")),
326
+ )
327
+ }
328
+ }
329
+
330
+ val sync =
331
+ if (map.hasKey("sync") && !map.isNull("sync")) {
332
+ map.getMap("sync")?.let { wireToSyncConfig(it) }
333
+ } else {
334
+ null
335
+ }
336
+ return BeekonConfig.SelfManaged(
258
337
  minTimeBetweenLocationsSeconds =
259
338
  map.getDouble("minTimeBetweenLocationsSeconds").toLong(),
260
339
  minDistanceBetweenLocationsMeters =
@@ -268,6 +347,7 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
268
347
  // Passed through verbatim; absent/null means unset — the SDK falls through
269
348
  // to the manifest meta-data, then evaluation (license-format-v1 §9).
270
349
  licenseKey = optString(map, "licenseKey"),
350
+ logLevel = toLogLevel(optString(map, "logLevel")),
271
351
  )
272
352
  }
273
353
 
@@ -283,6 +363,7 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
283
363
  headers = entriesToMap(map.getArray("headers")),
284
364
  intervalSeconds = map.getDouble("intervalSeconds").toLong(),
285
365
  batchSize = map.getDouble("batchSize").toInt(),
366
+ syncThreshold = if (map.hasKey("syncThreshold")) map.getDouble("syncThreshold").toInt() else 0,
286
367
  auth = auth,
287
368
  )
288
369
  }
@@ -484,6 +565,16 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
484
565
  return m
485
566
  }
486
567
 
568
+ private fun logEntryToWire(e: LogEntry): WritableMap {
569
+ val m = Arguments.createMap()
570
+ m.putString("id", e.id)
571
+ m.putDouble("timestampMs", e.timestamp.toEpochMilli().toDouble())
572
+ m.putString("level", e.level.wire)
573
+ m.putString("category", e.category)
574
+ m.putString("message", e.message)
575
+ return m
576
+ }
577
+
487
578
  private fun putNullableDouble(map: WritableMap, key: String, value: Double?) {
488
579
  if (value == null) map.putNull(key) else map.putDouble(key, value)
489
580
  }
@@ -514,11 +605,18 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
514
605
  else -> AuthBodyFormat.Form
515
606
  }
516
607
 
608
+ // Wire token (lowercase) -> native LogLevel; unknown/absent defaults to Info.
609
+ private fun toLogLevel(s: String?): LogLevel =
610
+ LogLevel.fromWire(s ?: "") ?: LogLevel.Info
611
+
612
+ // GATED on beekon native 0.0.9 (StopReason.CloudModeUnavailable) — the new arm
613
+ // does not exist on the pinned 0.0.8 dep, so this `when` is non-exhaustive there.
517
614
  private fun stopReasonToWire(r: StopReason): String = when (r) {
518
615
  StopReason.User -> "user"
519
616
  StopReason.PermissionDenied -> "permissionDenied"
520
617
  StopReason.LocationServicesDisabled -> "locationServicesDisabled"
521
618
  StopReason.LocationUnavailable -> "locationUnavailable"
619
+ StopReason.CloudModeUnavailable -> "cloudModeUnavailable"
522
620
  StopReason.System -> "system"
523
621
  }
524
622
 
@@ -580,6 +678,6 @@ class BeekonRnModule(reactContext: ReactApplicationContext) :
580
678
  // Reported to the native verifier via setWrapperInfo (diagnostics only — the
581
679
  // verifier consumes only the product). Keep in sync with package.json
582
680
  // "version" on release.
583
- private const val WRAPPER_VERSION = "0.0.7"
681
+ private const val WRAPPER_VERSION = "0.0.9"
584
682
  }
585
683
  }
package/ios/BeekonRn.mm CHANGED
@@ -35,6 +35,9 @@
35
35
  }
36
36
  onLicenseStatus:^(NSDictionary *_Nonnull ls) {
37
37
  [weakSelf emitOnLicenseStatus:ls];
38
+ }
39
+ onLog:^(NSDictionary *_Nonnull e) {
40
+ [weakSelf emitOnLog:e];
38
41
  }];
39
42
  }
40
43
  return self;
@@ -147,6 +150,38 @@
147
150
  [_impl licenseStatusWithResolver:resolve rejecter:reject];
148
151
  }
149
152
 
153
+ // MARK: - Diagnostic logs
154
+
155
+ - (void)getLog:(double)fromMs
156
+ toMs:(double)toMs
157
+ resolve:(RCTPromiseResolveBlock)resolve
158
+ reject:(RCTPromiseRejectBlock)reject {
159
+ [_impl getLogFromMs:fromMs toMs:toMs resolver:resolve rejecter:reject];
160
+ }
161
+
162
+ - (void)exportLog:(RCTPromiseResolveBlock)resolve
163
+ reject:(RCTPromiseRejectBlock)reject {
164
+ [_impl exportLogWithResolver:resolve rejecter:reject];
165
+ }
166
+
167
+ - (void)clearLog:(RCTPromiseResolveBlock)resolve
168
+ reject:(RCTPromiseRejectBlock)reject {
169
+ [_impl clearLogWithResolver:resolve rejecter:reject];
170
+ }
171
+
172
+ - (void)setLogLevel:(NSString *)level
173
+ resolve:(RCTPromiseResolveBlock)resolve
174
+ reject:(RCTPromiseRejectBlock)reject {
175
+ [_impl setLogLevel:level resolver:resolve rejecter:reject];
176
+ }
177
+
178
+ - (void)log:(NSString *)level
179
+ message:(NSString *)message
180
+ resolve:(RCTPromiseResolveBlock)resolve
181
+ reject:(RCTPromiseRejectBlock)reject {
182
+ [_impl logWithLevel:level message:message resolver:resolve rejecter:reject];
183
+ }
184
+
150
185
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
151
186
  (const facebook::react::ObjCTurboModule::InitParams &)params {
152
187
  return std::make_shared<facebook::react::NativeBeekonRnSpecJSI>(params);