@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wayq/beekon-rn",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
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",
@@ -20,6 +20,7 @@
20
20
  "scripts/fetch-beekonkit.sh",
21
21
  "*.podspec",
22
22
  "LICENSE.txt",
23
+ "CHANGELOG.md",
23
24
  "!ios/build",
24
25
  "!android/build",
25
26
  "!android/gradle",
@@ -37,7 +38,8 @@
37
38
  "fetch-beekonkit": "bash scripts/fetch-beekonkit.sh",
38
39
  "prepare": "yarn fetch-beekonkit && bob build",
39
40
  "typecheck": "tsc",
40
- "lint": "eslint \"**/*.{js,ts,tsx}\""
41
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
42
+ "test": "jest"
41
43
  },
42
44
  "keywords": [
43
45
  "react-native",
@@ -45,9 +47,12 @@
45
47
  "android",
46
48
  "location",
47
49
  "tracking",
50
+ "location-tracking",
48
51
  "gps",
52
+ "gps-tracking",
49
53
  "background-location",
50
54
  "geolocation",
55
+ "geofencing",
51
56
  "beekon",
52
57
  "turbo-module"
53
58
  ],
@@ -71,12 +76,15 @@
71
76
  "@eslint/js": "^10.0.1",
72
77
  "@react-native/babel-preset": "0.85.0",
73
78
  "@react-native/eslint-config": "0.85.0",
79
+ "@types/jest": "^29.5.14",
74
80
  "@types/react": "^19.2.0",
81
+ "babel-jest": "^29.7.0",
75
82
  "del-cli": "^7.0.0",
76
83
  "eslint": "^9.39.4",
77
84
  "eslint-config-prettier": "^10.1.8",
78
85
  "eslint-plugin-ft-flow": "^3.0.11",
79
86
  "eslint-plugin-prettier": "^5.5.5",
87
+ "jest": "^29.7.0",
80
88
  "prettier": "^3.8.1",
81
89
  "react": "19.2.3",
82
90
  "react-native": "0.85.0",
@@ -13,12 +13,12 @@
13
13
 
14
14
  set -euo pipefail
15
15
 
16
- VERSION="0.0.5"
16
+ VERSION="0.0.7"
17
17
  URL="https://github.com/wayqteam/beekon-ios-binary/releases/download/v${VERSION}/BeekonKit.xcframework.zip"
18
- # SHA256 of the v0.0.5 BeekonKit.xcframework.zip. Matches the SwiftPM
19
- # `binaryTarget` checksum in beekon-ios-binary's Package.swift at tag v0.0.5
18
+ # SHA256 of the v0.0.7 BeekonKit.xcframework.zip. Matches the SwiftPM
19
+ # `binaryTarget` checksum in beekon-ios-binary's Package.swift at tag v0.0.7
20
20
  # (SwiftPM's compute-checksum is the SHA256 of the zip).
21
- EXPECTED_SHA="437bf493d7de19d8df9599da097be162796a8304eb2d7826cf2505c33d4603f7"
21
+ EXPECTED_SHA="48803b540f063f609230f44a0f5b7faca125847db9f4628d0bf5d31d351a762b"
22
22
 
23
23
  ROOT="$(cd "$(dirname "$0")/.." && pwd)"
24
24
  DEST_DIR="${ROOT}/ios/Frameworks"
@@ -27,17 +27,47 @@ export type WireKeyValue = {
27
27
  value: string;
28
28
  };
29
29
 
30
+ /** Flat wire form of `AuthResponseMapping`. Omitted keys use common-name detection. */
31
+ export type WireAuthResponseMapping = {
32
+ accessToken?: string;
33
+ refreshToken?: string;
34
+ expiresIn?: string;
35
+ expiresAt?: string;
36
+ };
37
+
38
+ /**
39
+ * Flat wire form of `AuthConfig` on {@link WireSyncConfig.auth}. Enums travel as
40
+ * strings (`strategy`: 'bearer'|'raw'; `refreshBodyFormat`: 'form'|'json'),
41
+ * string maps as `WireKeyValue[]`, and `expiresAtMs` is epoch milliseconds.
42
+ */
43
+ export type WireAuthConfig = {
44
+ accessToken?: string;
45
+ refreshToken?: string;
46
+ expiresAtMs?: number;
47
+ strategy: string;
48
+ refreshUrl?: string;
49
+ refreshPayload: WireKeyValue[];
50
+ refreshHeaders: WireKeyValue[];
51
+ refreshBodyFormat: string;
52
+ responseMapping: WireAuthResponseMapping;
53
+ skewMarginSeconds: number;
54
+ seedEpoch?: number;
55
+ };
56
+
30
57
  export type WireSyncConfig = {
31
58
  url: string;
32
59
  headers: WireKeyValue[];
33
60
  intervalSeconds: number;
34
61
  batchSize: number;
62
+ /** Token-refresh recipe; omitted keeps static-header auth. */
63
+ auth?: WireAuthConfig;
35
64
  };
36
65
 
37
66
  /** Android-only foreground-service notification overrides. iOS ignores it. */
38
67
  export type WireNotificationConfig = {
39
68
  title?: string;
40
69
  text?: string;
70
+ smallIcon?: string;
41
71
  };
42
72
 
43
73
  export type WireConfig = {
@@ -54,6 +84,13 @@ export type WireConfig = {
54
84
  sync?: WireSyncConfig;
55
85
  /** Android-only; the iOS native module ignores it. */
56
86
  notification?: WireNotificationConfig;
87
+ /**
88
+ * License token (license-format-v1 §9). Omitted/`undefined` means unset — the
89
+ * native SDK falls through to the manifest / Info.plist. The wrapper passes it
90
+ * through verbatim (no trimming or fallback). `configure` crosses as an untyped
91
+ * object, so this field documents the shape rather than altering codegen.
92
+ */
93
+ licenseKey?: string;
57
94
  };
58
95
 
59
96
  export type WireLocation = {
@@ -112,6 +149,40 @@ export type WireSyncStatus = {
112
149
  failure?: string;
113
150
  };
114
151
 
152
+ /**
153
+ * A token set the SDK rotated, delivered on `onAuthTokens`. `expiresAtMs` is
154
+ * epoch milliseconds; both it and `refreshToken` are `null` when absent.
155
+ */
156
+ export type WireAuthTokens = {
157
+ accessToken: string;
158
+ refreshToken: string | null;
159
+ expiresAtMs: number | null;
160
+ epoch: number;
161
+ };
162
+
163
+ /**
164
+ * Flat wire form of the current platform's native license status
165
+ * (license-format-v1 §8). `status` is the wire-stable identifier; `tier` and
166
+ * `entitlements` are populated only when `status === 'licensed'` (null / empty
167
+ * otherwise), and `reason` only when `status === 'invalid'`.
168
+ */
169
+ export type WireLicenseStatus = {
170
+ /**
171
+ * One of: 'notDetermined' | 'licensed' | 'evaluation' | 'expired' |
172
+ * 'updateEntitlementLapsed' | 'invalid'.
173
+ */
174
+ status: string;
175
+ /** The opaque pricing tier; `null` unless `status === 'licensed'`. */
176
+ tier: string | null;
177
+ /** Opaque feature-gate strings; empty unless `status === 'licensed'`. */
178
+ entitlements: string[];
179
+ /**
180
+ * `null` unless `status === 'invalid'`. One of: 'malformed' | 'unknownKey' |
181
+ * 'badSignature' | 'appIdMismatch' | 'productMismatch' | 'unsupportedVersion'.
182
+ */
183
+ reason: string | null;
184
+ };
185
+
115
186
  export interface Spec extends TurboModule {
116
187
  /**
117
188
  * The config crosses as an untyped object (`ReadableMap` on Android,
@@ -125,6 +196,19 @@ export interface Spec extends TurboModule {
125
196
  stop(): Promise<void>;
126
197
  resumeIfNeeded(): Promise<void>;
127
198
 
199
+ /**
200
+ * One immediate fix. `accuracy` `''` uses the configured mode (a plain
201
+ * `string` rather than `string | null` for portable Codegen, matching
202
+ * `deleteLocations`). Resolves a 0- or 1-element array — empty means timeout /
203
+ * no fix — rather than a nullable struct, which Codegen does not portably
204
+ * support. Rejects with `PERMISSION_DENIED` / `LOCATION_SERVICES_DISABLED` /
205
+ * `LOCATION_UNAVAILABLE` on a precondition failure.
206
+ */
207
+ getCurrentLocation(
208
+ timeoutMs: number,
209
+ accuracy: string
210
+ ): Promise<WireLocation[]>;
211
+
128
212
  getLocations(fromMs: number, toMs: number): Promise<WireLocation[]>;
129
213
  /**
130
214
  * A negative `beforeMs` deletes all stored locations (the wire encoding of
@@ -141,10 +225,19 @@ export interface Spec extends TurboModule {
141
225
  removeGeofences(ids: string[]): Promise<void>;
142
226
  listGeofences(): Promise<WireGeofence[]>;
143
227
 
228
+ /**
229
+ * The current platform's native license status (license-format-v1 §8). Each
230
+ * native SDK validates independently; this reflects only the platform you are
231
+ * running on — no cross-platform merging.
232
+ */
233
+ licenseStatus(): Promise<WireLicenseStatus>;
234
+
144
235
  readonly onState: CodegenTypes.EventEmitter<WireState>;
145
236
  readonly onLocation: CodegenTypes.EventEmitter<WireLocation>;
146
237
  readonly onGeofenceEvent: CodegenTypes.EventEmitter<WireGeofenceEvent>;
147
238
  readonly onSyncStatus: CodegenTypes.EventEmitter<WireSyncStatus>;
239
+ readonly onAuthTokens: CodegenTypes.EventEmitter<WireAuthTokens>;
240
+ readonly onLicenseStatus: CodegenTypes.EventEmitter<WireLicenseStatus>;
148
241
  }
149
242
 
150
243
  export default TurboModuleRegistry.getEnforcing<Spec>('BeekonRn');
package/src/beekon.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import NativeBeekon from './NativeBeekonRn';
2
+ import type { AuthTokens } from './types/auth';
2
3
  import type { BeekonConfig } from './types/config';
4
+ import type { AccuracyMode } from './types/enums';
3
5
  import type { BeekonGeofence, GeofenceEvent } from './types/geofence';
6
+ import type { LicenseStatus } from './types/license';
4
7
  import type { Location } from './types/location';
5
8
  import type { BeekonState } from './types/state';
6
9
  import type { SyncStatus } from './types/sync';
@@ -9,8 +12,10 @@ import {
9
12
  geofenceToWire,
10
13
  recordToEntries,
11
14
  rethrowAsBeekonError,
15
+ wireToAuthTokens,
12
16
  wireToGeofence,
13
17
  wireToGeofenceEvent,
18
+ wireToLicenseStatus,
14
19
  wireToLocation,
15
20
  wireToState,
16
21
  wireToSyncStatus,
@@ -19,11 +24,11 @@ import {
19
24
  type Listener<T> = (value: T) => void;
20
25
 
21
26
  /**
22
- * Fans the four native EventEmitters out to multiple JS subscribers and adds
23
- * replay-1 semantics for `state` / `syncStatus` (RN EventEmitters don't replay,
24
- * so we cache the latest value and hand it to new subscribers immediately —
25
- * matching the native StateFlow / AsyncStream contract). `location` /
26
- * `geofenceEvent` are plain fan-out with no replay.
27
+ * Fans the five native EventEmitters out to multiple JS subscribers and adds
28
+ * replay-1 semantics for `state` / `syncStatus` / `authTokens` (RN EventEmitters
29
+ * don't replay, so we cache the latest value and hand it to new subscribers
30
+ * immediately — matching the native StateFlow / AsyncStream contract).
31
+ * `location` / `geofenceEvent` are plain fan-out with no replay.
27
32
  *
28
33
  * Native subscriptions are opened once, on first use, and kept for the app
29
34
  * lifetime — the native module collects its flows continuously regardless, so
@@ -35,8 +40,12 @@ class EventHub {
35
40
  private readonly locationListeners = new Set<Listener<Location>>();
36
41
  private readonly geofenceListeners = new Set<Listener<GeofenceEvent>>();
37
42
  private readonly syncListeners = new Set<Listener<SyncStatus>>();
43
+ private readonly authTokenListeners = new Set<Listener<AuthTokens>>();
44
+ private readonly licenseListeners = new Set<Listener<LicenseStatus>>();
38
45
  private lastState: BeekonState | undefined;
39
46
  private lastSyncStatus: SyncStatus | undefined;
47
+ private lastAuthTokens: AuthTokens | undefined;
48
+ private lastLicenseStatus: LicenseStatus | undefined;
40
49
 
41
50
  /** Idempotent: opens the native subscriptions the first time it is called. */
42
51
  ensureSubscribed(): void {
@@ -60,6 +69,16 @@ class EventHub {
60
69
  this.lastSyncStatus = s;
61
70
  this.syncListeners.forEach((cb) => cb(s));
62
71
  });
72
+ NativeBeekon.onAuthTokens((w) => {
73
+ const t = wireToAuthTokens(w);
74
+ this.lastAuthTokens = t;
75
+ this.authTokenListeners.forEach((cb) => cb(t));
76
+ });
77
+ NativeBeekon.onLicenseStatus((w) => {
78
+ const s = wireToLicenseStatus(w);
79
+ this.lastLicenseStatus = s;
80
+ this.licenseListeners.forEach((cb) => cb(s));
81
+ });
63
82
  }
64
83
 
65
84
  onState(cb: Listener<BeekonState>): () => void {
@@ -95,6 +114,24 @@ class EventHub {
95
114
  this.syncListeners.delete(cb);
96
115
  };
97
116
  }
117
+
118
+ onAuthTokens(cb: Listener<AuthTokens>): () => void {
119
+ this.ensureSubscribed();
120
+ this.authTokenListeners.add(cb);
121
+ if (this.lastAuthTokens !== undefined) cb(this.lastAuthTokens);
122
+ return () => {
123
+ this.authTokenListeners.delete(cb);
124
+ };
125
+ }
126
+
127
+ onLicenseStatus(cb: Listener<LicenseStatus>): () => void {
128
+ this.ensureSubscribed();
129
+ this.licenseListeners.add(cb);
130
+ if (this.lastLicenseStatus !== undefined) cb(this.lastLicenseStatus);
131
+ return () => {
132
+ this.licenseListeners.delete(cb);
133
+ };
134
+ }
98
135
  }
99
136
 
100
137
  /**
@@ -160,6 +197,37 @@ class BeekonImpl {
160
197
  await NativeBeekon.resumeIfNeeded();
161
198
  }
162
199
 
200
+ /**
201
+ * Return a single fresh fix on demand — for the moment a host needs an
202
+ * immediate position (e.g. the instant a trip starts).
203
+ *
204
+ * Independent of tracking: works whether or not a session is running and never
205
+ * starts, stops, or disturbs one. The fix is not persisted and does not appear
206
+ * on `onLocation` — it is returned here only, tagged `'manual'`.
207
+ *
208
+ * Resolves `null` if no usable fix arrives within `timeoutMs` (default
209
+ * 15000). `accuracy` overrides the configured mode for this call only;
210
+ * omitted uses the configured mode.
211
+ *
212
+ * Throws `BeekonError` with kind `'permissionDenied'`,
213
+ * `'locationServicesDisabled'`, or `'locationUnavailable'` on a precondition
214
+ * failure.
215
+ */
216
+ async getCurrentLocation(options?: {
217
+ timeoutMs?: number;
218
+ accuracy?: AccuracyMode;
219
+ }): Promise<Location | null> {
220
+ try {
221
+ const wires = await NativeBeekon.getCurrentLocation(
222
+ options?.timeoutMs ?? 15000,
223
+ options?.accuracy ?? ''
224
+ );
225
+ return wires.length === 0 ? null : wireToLocation(wires[0]!);
226
+ } catch (e) {
227
+ rethrowAsBeekonError(e);
228
+ }
229
+ }
230
+
163
231
  /**
164
232
  * Read persisted fixes in the inclusive range `[from, to]`, oldest first.
165
233
  * Reads come from the SDK's local storage (Room on Android, GRDB on iOS) — the
@@ -181,7 +249,7 @@ class BeekonImpl {
181
249
  }
182
250
 
183
251
  /**
184
- * Delete stored locations captured at or before `before` (all of them when
252
+ * Delete stored locations captured before `before` (all of them when
185
253
  * `before` is omitted). Returns the number of rows removed. Throws
186
254
  * `BeekonError` with kind `'storage'` on a failure.
187
255
  */
@@ -243,6 +311,16 @@ class BeekonImpl {
243
311
  return wires.map(wireToGeofence);
244
312
  }
245
313
 
314
+ /**
315
+ * The current platform's license status (license-format-v1 §8). Android and
316
+ * iOS validate independently — this reflects the platform you are running on,
317
+ * with no cross-platform merging. Purely observational: a license never blocks,
318
+ * degrades, or delays the SDK. For live transitions use {@link onLicenseStatus}.
319
+ */
320
+ async licenseStatus(): Promise<LicenseStatus> {
321
+ return wireToLicenseStatus(await NativeBeekon.licenseStatus());
322
+ }
323
+
246
324
  /**
247
325
  * Subscribe to tracking-state transitions (`idle` / `tracking` /
248
326
  * `stopped(reason)`). The current state is delivered to new subscribers
@@ -277,6 +355,26 @@ class BeekonImpl {
277
355
  onSyncStatus(cb: (s: SyncStatus) => void): () => void {
278
356
  return this.hub.onSyncStatus(cb);
279
357
  }
358
+
359
+ /**
360
+ * Subscribe to token rotations the SDK performed during a native refresh (see
361
+ * `SyncConfig.auth`). Mirror them into your own session store. The latest
362
+ * rotation is delivered to new subscribers immediately (replay-1). Sensitive —
363
+ * delivered in-process only, never logged. Returns an unsubscribe function.
364
+ */
365
+ onAuthTokens(cb: (t: AuthTokens) => void): () => void {
366
+ return this.hub.onAuthTokens(cb);
367
+ }
368
+
369
+ /**
370
+ * Subscribe to license-status transitions for the current platform
371
+ * (license-format-v1 §8). The current status is delivered to new subscribers
372
+ * immediately (replay-1), then again on each transition (the first validation
373
+ * is lazy). Purely observational. Returns an unsubscribe function.
374
+ */
375
+ onLicenseStatus(cb: (s: LicenseStatus) => void): () => void {
376
+ return this.hub.onLicenseStatus(cb);
377
+ }
280
378
  }
281
379
 
282
380
  export const Beekon = new BeekonImpl();
package/src/index.tsx CHANGED
@@ -12,7 +12,10 @@ export type {
12
12
  LocationQuality,
13
13
  MotionState,
14
14
  ActivityType,
15
+ AuthStrategy,
16
+ AuthBodyFormat,
15
17
  } from './types/enums';
18
+ export type { AuthConfig, AuthResponseMapping, AuthTokens } from './types/auth';
16
19
  export type { Location } from './types/location';
17
20
  export type {
18
21
  BeekonGeofence,
@@ -21,4 +24,5 @@ export type {
21
24
  } from './types/geofence';
22
25
  export type { BeekonState, StopReason } from './types/state';
23
26
  export type { SyncStatus, SyncFailure } from './types/sync';
27
+ export type { LicenseStatus, LicenseInvalidReason } from './types/license';
24
28
  export { BeekonError, type BeekonErrorKind } from './types/error';
@@ -1,7 +1,10 @@
1
+ import type { AuthConfig, AuthTokens } from '../types/auth';
1
2
  import type { BeekonConfig } from '../types/config';
2
3
  import type {
3
4
  AccuracyMode,
4
5
  ActivityType,
6
+ AuthBodyFormat,
7
+ AuthStrategy,
5
8
  LocationQuality,
6
9
  LocationTrigger,
7
10
  MotionState,
@@ -12,15 +15,19 @@ import type {
12
15
  GeofenceEvent,
13
16
  Transition,
14
17
  } from '../types/geofence';
18
+ import type { LicenseInvalidReason, LicenseStatus } from '../types/license';
15
19
  import type { Location } from '../types/location';
16
20
  import type { BeekonState, StopReason } from '../types/state';
17
21
  import type { SyncFailure, SyncStatus } from '../types/sync';
18
22
  import { BeekonError, type BeekonErrorKind } from '../types/error';
19
23
  import type {
24
+ WireAuthConfig,
25
+ WireAuthTokens,
20
26
  WireConfig,
21
27
  WireGeofence,
22
28
  WireGeofenceEvent,
23
29
  WireKeyValue,
30
+ WireLicenseStatus,
24
31
  WireLocation,
25
32
  WireState,
26
33
  WireSyncStatus,
@@ -37,6 +44,13 @@ const DEFAULTS = {
37
44
  detectActivity: false,
38
45
  syncIntervalSeconds: 300,
39
46
  syncBatchSize: 100,
47
+ authStrategy: 'bearer' as AuthStrategy,
48
+ authBodyFormat: 'form' as AuthBodyFormat,
49
+ authSkewMarginSeconds: 60,
50
+ authRefreshHeaders: { Authorization: 'Bearer {accessToken}' } as Record<
51
+ string,
52
+ string
53
+ >,
40
54
  };
41
55
 
42
56
  // --- Public → wire ---------------------------------------------------------
@@ -68,11 +82,20 @@ export function configToWire(config: BeekonConfig): WireConfig {
68
82
  headers: recordToEntries(sync.headers),
69
83
  intervalSeconds: sync.intervalSeconds ?? DEFAULTS.syncIntervalSeconds,
70
84
  batchSize: sync.batchSize ?? DEFAULTS.syncBatchSize,
85
+ auth: sync.auth ? authToWire(sync.auth) : undefined,
71
86
  }
72
87
  : undefined,
73
88
  notification: config.notification
74
- ? { title: config.notification.title, text: config.notification.text }
89
+ ? {
90
+ title: config.notification.title,
91
+ text: config.notification.text,
92
+ smallIcon: config.notification.smallIcon,
93
+ }
75
94
  : undefined,
95
+ // Passed through verbatim — `undefined` is omitted and the native SDK falls
96
+ // through to manifest/Info.plist. No wrapper-side trimming or fallback
97
+ // (blank/whitespace handling lives in the native SDK; spec §9).
98
+ licenseKey: config.licenseKey,
76
99
  };
77
100
  }
78
101
 
@@ -87,6 +110,31 @@ export function geofenceToWire(g: BeekonGeofence): WireGeofence {
87
110
  };
88
111
  }
89
112
 
113
+ // `expiresAt` (a `Date`) becomes epoch millis on the wire; the native side
114
+ // converts to epoch seconds. String maps travel as `WireKeyValue[]`.
115
+ export function authToWire(a: AuthConfig): WireAuthConfig {
116
+ return {
117
+ accessToken: a.accessToken,
118
+ refreshToken: a.refreshToken,
119
+ expiresAtMs: a.expiresAt?.getTime(),
120
+ strategy: a.strategy ?? DEFAULTS.authStrategy,
121
+ refreshUrl: a.refreshUrl,
122
+ refreshPayload: recordToEntries(a.refreshPayload),
123
+ refreshHeaders: recordToEntries(
124
+ a.refreshHeaders ?? DEFAULTS.authRefreshHeaders
125
+ ),
126
+ refreshBodyFormat: a.refreshBodyFormat ?? DEFAULTS.authBodyFormat,
127
+ responseMapping: {
128
+ accessToken: a.responseMapping?.accessToken,
129
+ refreshToken: a.responseMapping?.refreshToken,
130
+ expiresIn: a.responseMapping?.expiresIn,
131
+ expiresAt: a.responseMapping?.expiresAt,
132
+ },
133
+ skewMarginSeconds: a.skewMarginSeconds ?? DEFAULTS.authSkewMarginSeconds,
134
+ seedEpoch: a.seedEpoch,
135
+ };
136
+ }
137
+
90
138
  // --- Wire → public ---------------------------------------------------------
91
139
 
92
140
  export function wireToLocation(w: WireLocation): Location {
@@ -180,6 +228,60 @@ export function wireToSyncStatus(w: WireSyncStatus): SyncStatus {
180
228
  }
181
229
  }
182
230
 
231
+ /** `expiresAtMs` (epoch millis) becomes a `Date`; `null` propagates faithfully. */
232
+ export function wireToAuthTokens(w: WireAuthTokens): AuthTokens {
233
+ return {
234
+ accessToken: w.accessToken,
235
+ refreshToken: w.refreshToken,
236
+ expiresAt: w.expiresAtMs == null ? null : new Date(w.expiresAtMs),
237
+ epoch: w.epoch,
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Decode the wire-stable license status into the public discriminated union. The
243
+ * native side already decided the status (the wrapper performs no validation) —
244
+ * this only re-shapes the flat wire form and validates the enum strings.
245
+ */
246
+ export function wireToLicenseStatus(w: WireLicenseStatus): LicenseStatus {
247
+ switch (w.status) {
248
+ case 'licensed':
249
+ return {
250
+ status: 'licensed',
251
+ tier: w.tier ?? '',
252
+ entitlements: w.entitlements ?? [],
253
+ };
254
+ case 'invalid':
255
+ return {
256
+ status: 'invalid',
257
+ reason: oneOf<LicenseInvalidReason>(
258
+ w.reason,
259
+ [
260
+ 'malformed',
261
+ 'unknownKey',
262
+ 'badSignature',
263
+ 'appIdMismatch',
264
+ 'productMismatch',
265
+ 'unsupportedVersion',
266
+ ],
267
+ 'malformed'
268
+ ),
269
+ };
270
+ case 'evaluation':
271
+ return { status: 'evaluation' };
272
+ case 'expired':
273
+ return { status: 'expired' };
274
+ case 'updateEntitlementLapsed':
275
+ return { status: 'updateEntitlementLapsed' };
276
+ case 'notDetermined':
277
+ return { status: 'notDetermined' };
278
+ default:
279
+ // Forward-compat: an unknown status string means a native build newer than
280
+ // this wrapper — surface as notDetermined rather than throwing.
281
+ return { status: 'notDetermined' };
282
+ }
283
+ }
284
+
183
285
  function toStopReason(s: string | undefined): StopReason {
184
286
  return oneOf<StopReason>(
185
287
  s,
@@ -235,6 +337,12 @@ function codeToKind(code: string | undefined): BeekonErrorKind | undefined {
235
337
  return 'storage';
236
338
  case 'INVALID_GEOFENCE':
237
339
  return 'invalidGeofence';
340
+ case 'PERMISSION_DENIED':
341
+ return 'permissionDenied';
342
+ case 'LOCATION_SERVICES_DISABLED':
343
+ return 'locationServicesDisabled';
344
+ case 'LOCATION_UNAVAILABLE':
345
+ return 'locationUnavailable';
238
346
  default:
239
347
  return undefined;
240
348
  }
@@ -0,0 +1,101 @@
1
+ import type { AuthBodyFormat, AuthStrategy } from './enums';
2
+
3
+ /**
4
+ * Where the SDK reads rotated tokens from the JSON response to a refresh
5
+ * request. Mirrors the native `AuthResponseMapping` (iOS) / `ResponseMapping`
6
+ * (Android).
7
+ *
8
+ * Each value is a response key. An omitted field falls back to common-name
9
+ * detection: `access_token`/`accessToken`/`token` for the access token,
10
+ * `refresh_token`/`refreshToken` for a rotated refresh token, and `expires_in`
11
+ * (relative seconds) or `expires_at`/`expires` (absolute epoch seconds) for the
12
+ * expiry. The default (all-omitted) is pure common-name detection.
13
+ */
14
+ export type AuthResponseMapping = {
15
+ /** Response key holding the new access token. Omitted uses common-name detection. */
16
+ accessToken?: string;
17
+ /** Response key holding a rotated refresh token. Omitted uses common-name detection. */
18
+ refreshToken?: string;
19
+ /** Response key holding a relative lifetime in seconds. Omitted uses detection. */
20
+ expiresIn?: string;
21
+ /** Response key holding an absolute expiry in epoch seconds. Omitted uses detection. */
22
+ expiresAt?: string;
23
+ };
24
+
25
+ /**
26
+ * Declarative recipe for native token refresh, set on `SyncConfig.auth`.
27
+ * Mirrors the native `AuthConfig`.
28
+ *
29
+ * When present, the SDK attaches the access token to every upload (per
30
+ * {@link AuthConfig.strategy}), refreshes it **proactively** before
31
+ * {@link AuthConfig.expiresAt} and **reactively** on a `401`/`403`, then retries
32
+ * the upload — all natively, so it works in the background and on a cold launch
33
+ * with no host involvement. Omit {@link AuthConfig.refreshUrl} to disable
34
+ * refresh and keep the legacy behaviour (a `401`/`403` pauses sync and surfaces
35
+ * `SyncFailure 'auth'`).
36
+ *
37
+ * **Tokens are a seed, not config.** {@link AuthConfig.accessToken},
38
+ * {@link AuthConfig.refreshToken} and {@link AuthConfig.expiresAt} are supplied
39
+ * once; the SDK then owns and rotates the live token set in secure storage
40
+ * (Keychain / encrypted prefs). Observe rotations via `Beekon.onAuthTokens()` to
41
+ * mirror the tokens into your own session store.
42
+ *
43
+ * Requires the native SDK ≥ 0.0.6; with an older native binary the recipe is
44
+ * ignored and only static `SyncConfig.headers` apply.
45
+ */
46
+ export type AuthConfig = {
47
+ /** Initial access token. A seed: the SDK owns and rotates it after first persist. */
48
+ accessToken?: string;
49
+ /** Initial refresh token POSTed to {@link AuthConfig.refreshUrl}. A seed. */
50
+ refreshToken?: string;
51
+ /** Absolute expiry of {@link AuthConfig.accessToken}. Omitted disables proactive refresh. */
52
+ expiresAt?: Date;
53
+ /** How the access token is attached. Default: `'bearer'`. */
54
+ strategy?: AuthStrategy;
55
+ /** Authorization server's refresh endpoint. Omitted disables refresh. */
56
+ refreshUrl?: string;
57
+ /**
58
+ * Form fields POSTed to {@link AuthConfig.refreshUrl}. A value containing
59
+ * `{refreshToken}` is substituted with the current refresh token. Default: empty.
60
+ */
61
+ refreshPayload?: Record<string, string>;
62
+ /**
63
+ * Headers sent on the refresh request. A value containing `{accessToken}` is
64
+ * substituted with the current access token. Default:
65
+ * `{ Authorization: 'Bearer {accessToken}' }`.
66
+ */
67
+ refreshHeaders?: Record<string, string>;
68
+ /** Encoding of the refresh request body. Default: `'form'`. */
69
+ refreshBodyFormat?: AuthBodyFormat;
70
+ /** Where to read rotated tokens from the refresh response. Default: detection. */
71
+ responseMapping?: AuthResponseMapping;
72
+ /** Seconds before {@link AuthConfig.expiresAt} at which a proactive refresh fires. Default: `60`. */
73
+ skewMarginSeconds?: number;
74
+ /**
75
+ * Optional monotonic re-seed signal. When greater than the SDK's stored epoch,
76
+ * a re-supplied seed replaces the rotated token set (e.g. after the user
77
+ * re-authenticates). Omitted adopts a re-supplied seed only when no token has
78
+ * been stored yet.
79
+ */
80
+ seedEpoch?: number;
81
+ };
82
+
83
+ /**
84
+ * A token set the SDK rotated, delivered via `Beekon.onAuthTokens()`. Mirrors
85
+ * the native `AuthTokens` (iOS) / `TokenRefresh` (Android).
86
+ *
87
+ * Treat these as sensitive: they are delivered in-process only and are never
88
+ * logged or persisted to plaintext. Use them to mirror the SDK's current
89
+ * credentials into your own session store. The latest rotation is replayed to
90
+ * new subscribers (replay-1).
91
+ */
92
+ export type AuthTokens = {
93
+ /** The rotated access token now in use. */
94
+ accessToken: string;
95
+ /** The current refresh token (rotated if the server returned a new one), or `null`. */
96
+ refreshToken: string | null;
97
+ /** Absolute expiry of {@link AuthTokens.accessToken}, or `null` if the response carried none. */
98
+ expiresAt: Date | null;
99
+ /** The SDK's monotonic token generation, incremented on each successful refresh. */
100
+ epoch: number;
101
+ };