@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
@@ -16,6 +16,43 @@ export type BeekonGeofence = {
16
16
  notifyOnEntry?: boolean;
17
17
  /** Emit an event on exit. Default: `true`. */
18
18
  notifyOnExit?: boolean;
19
+ /**
20
+ * Optional per-geofence notification the SDK renders natively at crossing time —
21
+ * offline and even when the app is killed. Omit it to handle crossings yourself
22
+ * via `Beekon.onGeofenceEvent`. `onEnter` requires `notifyOnEntry`; `onExit`
23
+ * requires `notifyOnExit`; in self-managed mode `delivery` must be `'local'`.
24
+ */
25
+ notification?: GeofenceNotification;
26
+ };
27
+ /** Where a geofence-crossing notification is delivered. Mirrors `NotificationDelivery` on the natives. */
28
+ export type NotificationDelivery = 'local' | 'cloud';
29
+ /** Relative prominence of a geofence notification. Mirrors `NotificationImportance`. */
30
+ export type NotificationImportance = 'high' | 'default';
31
+ /** Per-direction content the SDK renders natively for a geofence crossing. */
32
+ export type NotificationContent = {
33
+ /** Notification title. */
34
+ title: string;
35
+ /** Notification body. */
36
+ body: string;
37
+ /** Relative prominence. Default `'high'`. */
38
+ importance?: NotificationImportance;
39
+ /** Optional deep-link URI delivered in the notification for the host to route on tap. */
40
+ deepLink?: string;
41
+ /** Optional flat string payload delivered in the notification for routing on tap. */
42
+ data?: Record<string, string>;
43
+ };
44
+ /**
45
+ * An optional per-geofence notification rendered natively at crossing time.
46
+ * Mirrors `GeofenceNotification` on both native SDKs. The presence of `onEnter` /
47
+ * `onExit` selects which directions notify.
48
+ */
49
+ export type GeofenceNotification = {
50
+ /** Delivery channel. `'local'` (offline, device-rendered) is the default and only implemented mode. */
51
+ delivery: NotificationDelivery;
52
+ /** Content rendered on entry. Requires `notifyOnEntry`. */
53
+ onEnter?: NotificationContent;
54
+ /** Content rendered on exit. Requires `notifyOnExit`. */
55
+ onExit?: NotificationContent;
19
56
  };
20
57
  /** Whether a {@link GeofenceEvent} marks entering or exiting a geofence. */
21
58
  export type Transition = 'enter' | 'exit';
@@ -1 +1 @@
1
- {"version":3,"file":"geofence.d.ts","sourceRoot":"","sources":["../../../../src/types/geofence.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,yEAAyE;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1C;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,IAAI,EAAE,UAAU,CAAC;IACjB,kCAAkC;IAClC,SAAS,EAAE,IAAI,CAAC;CACjB,CAAC"}
1
+ {"version":3,"file":"geofence.d.ts","sourceRoot":"","sources":["../../../../src/types/geofence.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,yEAAyE;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAEF,0GAA0G;AAC1G,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,OAAO,CAAC;AAErD,wFAAwF;AACxF,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,SAAS,CAAC;AAExD,8EAA8E;AAC9E,MAAM,MAAM,mBAAmB,GAAG;IAChC,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,uGAAuG;IACvG,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,2DAA2D;IAC3D,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,yDAAyD;IACzD,MAAM,CAAC,EAAE,mBAAmB,CAAC;CAC9B,CAAC;AAEF,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1C;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,IAAI,EAAE,UAAU,CAAC;IACjB,kCAAkC;IAClC,SAAS,EAAE,IAAI,CAAC;CACjB,CAAC"}
@@ -39,4 +39,40 @@ export type PermissionStatus = {
39
39
  /** `true` when background tracking is authorized. */
40
40
  canTrackInBackground: boolean;
41
41
  };
42
+ /**
43
+ * A permission Beekon may require, normalized across platforms. Mirrors the
44
+ * native `BeekonPermission`.
45
+ *
46
+ * - `'location'` — foreground location; required for all tracking.
47
+ * - `'backgroundLocation'` — background revival, geofences, stationary resume.
48
+ * - `'activityRecognition'` — motion-based stationary detection.
49
+ * - `'notifications'` — the foreground-service notification (**Android only**).
50
+ */
51
+ export type BeekonPermission = 'location' | 'backgroundLocation' | 'activityRecognition' | 'notifications';
52
+ /**
53
+ * How badly a {@link PermissionRequirement} is needed.
54
+ *
55
+ * - `'required'` — tracking cannot start, or an explicitly-enabled feature is dead, without it.
56
+ * - `'recommended'` — an active feature silently degrades without it; tracking still runs.
57
+ */
58
+ export type PermissionImportance = 'required' | 'recommended';
59
+ /**
60
+ * One permission Beekon needs **for the current configuration**, each marked
61
+ * satisfied against the live grant — returned by `Beekon.getRequiredPermissions()`.
62
+ * Mirrors the native `PermissionRequirement`.
63
+ *
64
+ * Beekon never *requests* permission — the app owns that. This is the
65
+ * config-aware "doctor" companion to {@link PermissionStatus}: it reports
66
+ * activity-recognition (and, on Android, notifications) too.
67
+ */
68
+ export type PermissionRequirement = {
69
+ /** The normalized permission. */
70
+ permission: BeekonPermission;
71
+ /** Whether tracking/a feature breaks (`'required'`) or merely degrades (`'recommended'`) when absent. */
72
+ importance: PermissionImportance;
73
+ /** Whether the live OS grant covers it right now. */
74
+ satisfied: boolean;
75
+ /** Human-readable explanation of why Beekon needs it / what degrades without it. */
76
+ rationale: string;
77
+ };
42
78
  //# sourceMappingURL=permission.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../../../../src/types/permission.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,eAAe,GACvB,eAAe,GACf,QAAQ,GACR,YAAY,GACZ,YAAY,GACZ,YAAY,CAAC;AAEjB;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,CAAC;AAEpD;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iCAAiC;IACjC,KAAK,EAAE,eAAe,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,gEAAgE;IAChE,YAAY,EAAE,OAAO,CAAC;IACtB,qDAAqD;IACrD,oBAAoB,EAAE,OAAO,CAAC;CAC/B,CAAC"}
1
+ {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../../../../src/types/permission.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,eAAe,GACvB,eAAe,GACf,QAAQ,GACR,YAAY,GACZ,YAAY,GACZ,YAAY,CAAC;AAEjB;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,CAAC;AAEpD;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iCAAiC;IACjC,KAAK,EAAE,eAAe,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,gEAAgE;IAChE,YAAY,EAAE,OAAO,CAAC;IACtB,qDAAqD;IACrD,oBAAoB,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GACxB,UAAU,GACV,oBAAoB,GACpB,qBAAqB,GACrB,eAAe,CAAC;AAEpB;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG,aAAa,CAAC;AAE9D;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,iCAAiC;IACjC,UAAU,EAAE,gBAAgB,CAAC;IAC7B,yGAAyG;IACzG,UAAU,EAAE,oBAAoB,CAAC;IACjC,qDAAqD;IACrD,SAAS,EAAE,OAAO,CAAC;IACnB,oFAAoF;IACpF,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wayq/beekon-rn",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "React Native binding for the Beekon location SDK (Android + iOS).",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -13,12 +13,12 @@
13
13
 
14
14
  set -euo pipefail
15
15
 
16
- VERSION="0.1.0"
16
+ VERSION="0.1.2"
17
17
  URL="https://github.com/beekonlabs/beekon-ios-binary/releases/download/v${VERSION}/BeekonKit.xcframework.zip"
18
- # SHA256 of the v0.0.9 BeekonKit.xcframework.zip. Matches the SwiftPM
19
- # `binaryTarget` checksum in beekon-ios-binary's Package.swift at tag v0.0.9
18
+ # SHA256 of the v0.1.2 BeekonKit.xcframework.zip. Matches the SwiftPM
19
+ # `binaryTarget` checksum in beekon-ios-binary's Package.swift at tag v0.1.2
20
20
  # (SwiftPM's compute-checksum is the SHA256 of the zip).
21
- EXPECTED_SHA="8a39d9360a05f117226381036a10d3cfeb71237f99fd8d800cfafda42d9657f2"
21
+ EXPECTED_SHA="43b1f959699654020f32d3e8e4e1de6aacba8e32628c69a7477c1b2ba084a4a4"
22
22
 
23
23
  ROOT="$(cd "$(dirname "$0")/.." && pwd)"
24
24
  DEST_DIR="${ROOT}/ios/Frameworks"
@@ -156,6 +156,9 @@ export type WireGeofence = {
156
156
  radiusMeters: number;
157
157
  notifyOnEntry: boolean;
158
158
  notifyOnExit: boolean;
159
+ // Optional notification as a canonical JSON string (rendering is native-only).
160
+ // Flat string keeps Codegen happy and lets the payload schema grow without re-running codegen.
161
+ notificationJson?: string;
159
162
  };
160
163
 
161
164
  export type WireGeofenceEvent = {
@@ -233,6 +236,20 @@ export type WirePermissionStatus = {
233
236
  accuracy: string;
234
237
  };
235
238
 
239
+ /**
240
+ * Flat wire form of one `PermissionRequirement` (read-only). `permission` is one
241
+ * of 'location' | 'backgroundLocation' | 'activityRecognition' | 'notifications'
242
+ * ('notifications' Android-only); `importance` is 'required' | 'recommended'.
243
+ * The mappers translate to the public string-literal unions with a defensive
244
+ * fallback.
245
+ */
246
+ export type WirePermissionRequirement = {
247
+ permission: string;
248
+ importance: string;
249
+ satisfied: boolean;
250
+ rationale: string;
251
+ };
252
+
236
253
  export interface Spec extends TurboModule {
237
254
  /**
238
255
  * The config crosses as an untyped object (`ReadableMap` on Android,
@@ -288,6 +305,13 @@ export interface Spec extends TurboModule {
288
305
  */
289
306
  getPermissionStatus(): Promise<WirePermissionStatus>;
290
307
 
308
+ /**
309
+ * The permissions Beekon needs for the active configuration, each marked
310
+ * satisfied against the live grant (read-only; never prompts). The config-aware
311
+ * companion to `getPermissionStatus`. Call after `configure`.
312
+ */
313
+ getRequiredPermissions(): Promise<WirePermissionRequirement[]>;
314
+
291
315
  /** Persisted diagnostic log entries in `[fromMs, toMs]`, oldest first. */
292
316
  getLog(fromMs: number, toMs: number): Promise<WireLogEntry[]>;
293
317
  /** Serialize the whole log buffer to an NDJSON file; resolves its path. */
package/src/beekon.ts CHANGED
@@ -7,7 +7,10 @@ import type { BeekonGeofence, GeofenceEvent } from './types/geofence';
7
7
  import type { LicenseStatus } from './types/license';
8
8
  import type { Location } from './types/location';
9
9
  import type { LogEntry } from './types/log';
10
- import type { PermissionStatus } from './types/permission';
10
+ import type {
11
+ PermissionRequirement,
12
+ PermissionStatus,
13
+ } from './types/permission';
11
14
  import type { BeekonState } from './types/state';
12
15
  import type { SyncStatus } from './types/sync';
13
16
  import {
@@ -21,6 +24,7 @@ import {
21
24
  wireToLicenseStatus,
22
25
  wireToLocation,
23
26
  wireToLogEntry,
27
+ wireToPermissionRequirement,
24
28
  wireToPermissionStatus,
25
29
  wireToState,
26
30
  wireToSyncStatus,
@@ -368,6 +372,21 @@ class BeekonImpl {
368
372
  return wireToPermissionStatus(await NativeBeekon.getPermissionStatus());
369
373
  }
370
374
 
375
+ /**
376
+ * The permissions Beekon needs **for the current configuration**, each marked
377
+ * `satisfied` against the live grant — the config-aware "doctor" companion to
378
+ * `getPermissionStatus()`. Unlike that location-only snapshot, this reports
379
+ * activity-recognition (and, on Android, notifications) too, derived from the
380
+ * active mode and tracking config. Read-only; never prompts. Call after
381
+ * `configure()`; in cloud mode (server-owned config) it returns the
382
+ * conservative list.
383
+ */
384
+ async getRequiredPermissions(): Promise<PermissionRequirement[]> {
385
+ return (await NativeBeekon.getRequiredPermissions()).map(
386
+ wireToPermissionRequirement
387
+ );
388
+ }
389
+
371
390
  /**
372
391
  * Read persisted diagnostic log entries in the inclusive range `[from, to]`,
373
392
  * oldest first. Entries survive process death, so this recovers what the SDK
package/src/index.tsx CHANGED
@@ -26,6 +26,10 @@ export type { Location } from './types/location';
26
26
  export type {
27
27
  BeekonGeofence,
28
28
  GeofenceEvent,
29
+ GeofenceNotification,
30
+ NotificationContent,
31
+ NotificationDelivery,
32
+ NotificationImportance,
29
33
  Transition,
30
34
  } from './types/geofence';
31
35
  export type { BeekonState, StopReason } from './types/state';
@@ -33,6 +37,9 @@ export type {
33
37
  PermissionStatus,
34
38
  PermissionLevel,
35
39
  PermissionAccuracy,
40
+ BeekonPermission,
41
+ PermissionImportance,
42
+ PermissionRequirement,
36
43
  } from './types/permission';
37
44
  export type { SyncStatus, SyncFailure } from './types/sync';
38
45
  export type { LicenseStatus, LicenseInvalidReason } from './types/license';
@@ -15,13 +15,20 @@ import type { LogEntry } from '../types/log';
15
15
  import type {
16
16
  BeekonGeofence,
17
17
  GeofenceEvent,
18
+ GeofenceNotification,
19
+ NotificationContent,
20
+ NotificationDelivery,
21
+ NotificationImportance,
18
22
  Transition,
19
23
  } from '../types/geofence';
20
24
  import type { LicenseInvalidReason, LicenseStatus } from '../types/license';
21
25
  import type { Location } from '../types/location';
22
26
  import type {
27
+ BeekonPermission,
23
28
  PermissionAccuracy,
29
+ PermissionImportance,
24
30
  PermissionLevel,
31
+ PermissionRequirement,
25
32
  PermissionStatus,
26
33
  } from '../types/permission';
27
34
  import type { BeekonState, StopReason } from '../types/state';
@@ -37,6 +44,7 @@ import type {
37
44
  WireLicenseStatus,
38
45
  WireLocation,
39
46
  WireLogEntry,
47
+ WirePermissionRequirement,
40
48
  WirePermissionStatus,
41
49
  WireState,
42
50
  WireSyncStatus,
@@ -158,9 +166,90 @@ export function geofenceToWire(g: BeekonGeofence): WireGeofence {
158
166
  radiusMeters: g.radiusMeters,
159
167
  notifyOnEntry: g.notifyOnEntry ?? true,
160
168
  notifyOnExit: g.notifyOnExit ?? true,
169
+ notificationJson: notificationToJson(g.notification),
161
170
  };
162
171
  }
163
172
 
173
+ // Serialize a notification to the canonical JSON string the native SDKs understand
174
+ // (rendering is native-only). Optional keys are omitted; undefined ⇒ undefined.
175
+ function notificationToJson(
176
+ n: GeofenceNotification | undefined
177
+ ): string | undefined {
178
+ if (!n) return undefined;
179
+ const obj: Record<string, unknown> = {
180
+ delivery: n.delivery === 'cloud' ? 'cloud' : 'local',
181
+ };
182
+ if (n.onEnter) obj.onEnter = contentToObj(n.onEnter);
183
+ if (n.onExit) obj.onExit = contentToObj(n.onExit);
184
+ return JSON.stringify(obj);
185
+ }
186
+
187
+ function contentToObj(c: NotificationContent): Record<string, unknown> {
188
+ const obj: Record<string, unknown> = {
189
+ title: c.title,
190
+ body: c.body,
191
+ importance: c.importance === 'default' ? 'default' : 'high',
192
+ };
193
+ if (c.deepLink != null) obj.deepLink = c.deepLink;
194
+ if (c.data && Object.keys(c.data).length > 0) obj.data = c.data;
195
+ return obj;
196
+ }
197
+
198
+ // Parse a canonical notification JSON string. Tolerant: malformed input, an unknown
199
+ // enum, or content missing title/body degrades to undefined/defaults.
200
+ function notificationFromJson(
201
+ json: string | undefined
202
+ ): GeofenceNotification | undefined {
203
+ if (!json) return undefined;
204
+ let parsed: unknown;
205
+ try {
206
+ parsed = JSON.parse(json);
207
+ } catch {
208
+ return undefined;
209
+ }
210
+ if (typeof parsed !== 'object' || parsed === null) return undefined;
211
+ const obj = parsed as Record<string, unknown>;
212
+ const onEnter = contentFromObj(obj.onEnter);
213
+ const onExit = contentFromObj(obj.onExit);
214
+ if (!onEnter && !onExit) return undefined;
215
+ const delivery: NotificationDelivery =
216
+ obj.delivery === 'cloud' ? 'cloud' : 'local';
217
+ return {
218
+ delivery,
219
+ ...(onEnter ? { onEnter } : {}),
220
+ ...(onExit ? { onExit } : {}),
221
+ };
222
+ }
223
+
224
+ function contentFromObj(raw: unknown): NotificationContent | undefined {
225
+ if (typeof raw !== 'object' || raw === null) return undefined;
226
+ const r = raw as Record<string, unknown>;
227
+ if (
228
+ typeof r.title !== 'string' ||
229
+ typeof r.body !== 'string' ||
230
+ !r.title ||
231
+ !r.body
232
+ ) {
233
+ return undefined;
234
+ }
235
+ const importance: NotificationImportance =
236
+ r.importance === 'default' ? 'default' : 'high';
237
+ const data: Record<string, string> = {};
238
+ if (typeof r.data === 'object' && r.data !== null) {
239
+ for (const [k, v] of Object.entries(r.data as Record<string, unknown>)) {
240
+ if (typeof v === 'string') data[k] = v;
241
+ }
242
+ }
243
+ const content: NotificationContent = {
244
+ title: r.title,
245
+ body: r.body,
246
+ importance,
247
+ };
248
+ if (typeof r.deepLink === 'string') content.deepLink = r.deepLink;
249
+ if (Object.keys(data).length > 0) content.data = data;
250
+ return content;
251
+ }
252
+
164
253
  // `expiresAt` (a `Date`) becomes epoch millis on the wire; the native side
165
254
  // converts to epoch seconds. String maps travel as `WireKeyValue[]`.
166
255
  export function authToWire(a: AuthConfig): WireAuthConfig {
@@ -240,6 +329,7 @@ export function wireToGeofence(w: WireGeofence): BeekonGeofence {
240
329
  radiusMeters: w.radiusMeters,
241
330
  notifyOnEntry: w.notifyOnEntry,
242
331
  notifyOnExit: w.notifyOnExit,
332
+ notification: notificationFromJson(w.notificationJson),
243
333
  };
244
334
  }
245
335
 
@@ -300,6 +390,30 @@ export function wireToPermissionStatus(
300
390
  };
301
391
  }
302
392
 
393
+ export function wireToPermissionRequirement(
394
+ w: WirePermissionRequirement
395
+ ): PermissionRequirement {
396
+ return {
397
+ permission: oneOf<BeekonPermission>(
398
+ w.permission,
399
+ [
400
+ 'location',
401
+ 'backgroundLocation',
402
+ 'activityRecognition',
403
+ 'notifications',
404
+ ],
405
+ 'location'
406
+ ),
407
+ importance: oneOf<PermissionImportance>(
408
+ w.importance,
409
+ ['required', 'recommended'],
410
+ 'recommended'
411
+ ),
412
+ satisfied: w.satisfied,
413
+ rationale: w.rationale,
414
+ };
415
+ }
416
+
303
417
  export function wireToSyncStatus(w: WireSyncStatus): SyncStatus {
304
418
  switch (w.type) {
305
419
  case 'idle':
@@ -16,6 +16,47 @@ export type BeekonGeofence = {
16
16
  notifyOnEntry?: boolean;
17
17
  /** Emit an event on exit. Default: `true`. */
18
18
  notifyOnExit?: boolean;
19
+ /**
20
+ * Optional per-geofence notification the SDK renders natively at crossing time —
21
+ * offline and even when the app is killed. Omit it to handle crossings yourself
22
+ * via `Beekon.onGeofenceEvent`. `onEnter` requires `notifyOnEntry`; `onExit`
23
+ * requires `notifyOnExit`; in self-managed mode `delivery` must be `'local'`.
24
+ */
25
+ notification?: GeofenceNotification;
26
+ };
27
+
28
+ /** Where a geofence-crossing notification is delivered. Mirrors `NotificationDelivery` on the natives. */
29
+ export type NotificationDelivery = 'local' | 'cloud';
30
+
31
+ /** Relative prominence of a geofence notification. Mirrors `NotificationImportance`. */
32
+ export type NotificationImportance = 'high' | 'default';
33
+
34
+ /** Per-direction content the SDK renders natively for a geofence crossing. */
35
+ export type NotificationContent = {
36
+ /** Notification title. */
37
+ title: string;
38
+ /** Notification body. */
39
+ body: string;
40
+ /** Relative prominence. Default `'high'`. */
41
+ importance?: NotificationImportance;
42
+ /** Optional deep-link URI delivered in the notification for the host to route on tap. */
43
+ deepLink?: string;
44
+ /** Optional flat string payload delivered in the notification for routing on tap. */
45
+ data?: Record<string, string>;
46
+ };
47
+
48
+ /**
49
+ * An optional per-geofence notification rendered natively at crossing time.
50
+ * Mirrors `GeofenceNotification` on both native SDKs. The presence of `onEnter` /
51
+ * `onExit` selects which directions notify.
52
+ */
53
+ export type GeofenceNotification = {
54
+ /** Delivery channel. `'local'` (offline, device-rendered) is the default and only implemented mode. */
55
+ delivery: NotificationDelivery;
56
+ /** Content rendered on entry. Requires `notifyOnEntry`. */
57
+ onEnter?: NotificationContent;
58
+ /** Content rendered on exit. Requires `notifyOnExit`. */
59
+ onExit?: NotificationContent;
19
60
  };
20
61
 
21
62
  /** Whether a {@link GeofenceEvent} marks entering or exiting a geofence. */
@@ -46,3 +46,46 @@ export type PermissionStatus = {
46
46
  /** `true` when background tracking is authorized. */
47
47
  canTrackInBackground: boolean;
48
48
  };
49
+
50
+ /**
51
+ * A permission Beekon may require, normalized across platforms. Mirrors the
52
+ * native `BeekonPermission`.
53
+ *
54
+ * - `'location'` — foreground location; required for all tracking.
55
+ * - `'backgroundLocation'` — background revival, geofences, stationary resume.
56
+ * - `'activityRecognition'` — motion-based stationary detection.
57
+ * - `'notifications'` — the foreground-service notification (**Android only**).
58
+ */
59
+ export type BeekonPermission =
60
+ | 'location'
61
+ | 'backgroundLocation'
62
+ | 'activityRecognition'
63
+ | 'notifications';
64
+
65
+ /**
66
+ * How badly a {@link PermissionRequirement} is needed.
67
+ *
68
+ * - `'required'` — tracking cannot start, or an explicitly-enabled feature is dead, without it.
69
+ * - `'recommended'` — an active feature silently degrades without it; tracking still runs.
70
+ */
71
+ export type PermissionImportance = 'required' | 'recommended';
72
+
73
+ /**
74
+ * One permission Beekon needs **for the current configuration**, each marked
75
+ * satisfied against the live grant — returned by `Beekon.getRequiredPermissions()`.
76
+ * Mirrors the native `PermissionRequirement`.
77
+ *
78
+ * Beekon never *requests* permission — the app owns that. This is the
79
+ * config-aware "doctor" companion to {@link PermissionStatus}: it reports
80
+ * activity-recognition (and, on Android, notifications) too.
81
+ */
82
+ export type PermissionRequirement = {
83
+ /** The normalized permission. */
84
+ permission: BeekonPermission;
85
+ /** Whether tracking/a feature breaks (`'required'`) or merely degrades (`'recommended'`) when absent. */
86
+ importance: PermissionImportance;
87
+ /** Whether the live OS grant covers it right now. */
88
+ satisfied: boolean;
89
+ /** Human-readable explanation of why Beekon needs it / what degrades without it. */
90
+ rationale: string;
91
+ };