@wayq/beekon-rn 0.0.3 → 0.0.6

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 (84) hide show
  1. package/BeekonRn.podspec +5 -3
  2. package/CHANGELOG.md +103 -0
  3. package/README.md +326 -52
  4. package/android/build.gradle +9 -4
  5. package/android/src/main/java/in/wayq/beekonrn/BeekonRnModule.kt +411 -59
  6. package/ios/BeekonRn.mm +103 -24
  7. package/ios/BeekonRn.swift +465 -61
  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 +11424 -1279
  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 +262 -36
  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 +11424 -1279
  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 +262 -36
  19. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +11424 -1279
  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 +262 -36
  22. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/_CodeSignature/CodeResources +2 -80
  23. package/lib/module/NativeBeekonRn.js +35 -7
  24. package/lib/module/NativeBeekonRn.js.map +1 -1
  25. package/lib/module/beekon.js +246 -45
  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 +166 -25
  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/config.js +2 -0
  33. package/lib/module/types/enums.js +2 -0
  34. package/lib/module/types/enums.js.map +1 -0
  35. package/lib/module/types/error.js +20 -4
  36. package/lib/module/types/error.js.map +1 -1
  37. package/lib/module/types/geofence.js +2 -0
  38. package/lib/module/types/geofence.js.map +1 -0
  39. package/lib/module/types/location.js +2 -0
  40. package/lib/module/types/sync.js +2 -0
  41. package/lib/module/types/sync.js.map +1 -0
  42. package/lib/typescript/src/NativeBeekonRn.d.ts +150 -20
  43. package/lib/typescript/src/NativeBeekonRn.d.ts.map +1 -1
  44. package/lib/typescript/src/beekon.d.ts +110 -33
  45. package/lib/typescript/src/beekon.d.ts.map +1 -1
  46. package/lib/typescript/src/index.d.ts +6 -2
  47. package/lib/typescript/src/index.d.ts.map +1 -1
  48. package/lib/typescript/src/internal/mappers.d.ts +16 -6
  49. package/lib/typescript/src/internal/mappers.d.ts.map +1 -1
  50. package/lib/typescript/src/types/auth.d.ts +99 -0
  51. package/lib/typescript/src/types/auth.d.ts.map +1 -0
  52. package/lib/typescript/src/types/config.d.ts +66 -20
  53. package/lib/typescript/src/types/config.d.ts.map +1 -1
  54. package/lib/typescript/src/types/enums.d.ts +62 -0
  55. package/lib/typescript/src/types/enums.d.ts.map +1 -0
  56. package/lib/typescript/src/types/error.d.ts +21 -5
  57. package/lib/typescript/src/types/error.d.ts.map +1 -1
  58. package/lib/typescript/src/types/geofence.d.ts +36 -0
  59. package/lib/typescript/src/types/geofence.d.ts.map +1 -0
  60. package/lib/typescript/src/types/location.d.ts +22 -8
  61. package/lib/typescript/src/types/location.d.ts.map +1 -1
  62. package/lib/typescript/src/types/state.d.ts +13 -4
  63. package/lib/typescript/src/types/state.d.ts.map +1 -1
  64. package/lib/typescript/src/types/sync.d.ts +27 -0
  65. package/lib/typescript/src/types/sync.d.ts.map +1 -0
  66. package/package.json +8 -5
  67. package/scripts/fetch-beekonkit.sh +5 -5
  68. package/src/NativeBeekonRn.ts +165 -20
  69. package/src/beekon.ts +278 -48
  70. package/src/index.tsx +24 -2
  71. package/src/internal/mappers.ts +242 -27
  72. package/src/types/auth.ts +101 -0
  73. package/src/types/config.ts +68 -20
  74. package/src/types/enums.ts +80 -0
  75. package/src/types/error.ts +23 -5
  76. package/src/types/geofence.ts +37 -0
  77. package/src/types/location.ts +28 -8
  78. package/src/types/state.ts +13 -3
  79. package/src/types/sync.ts +23 -0
  80. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeDirectory +0 -0
  81. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeRequirements +0 -0
  82. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeResources +0 -296
  83. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeSignature +0 -0
  84. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/_CodeSignature/CodeResources +0 -146
@@ -1,60 +1,205 @@
1
1
  /**
2
- * Codegen TurboModule spec — DO NOT export Wire* types from `src/index.tsx`.
3
- * The public TS API in `src/beekon.ts` and `src/types/` is what consumers use;
4
- * mappers in `src/internal/mappers.ts` convert between the two.
2
+ * Codegen TurboModule spec — the single source React Native's Codegen reads to
3
+ * generate the native interface. **DO NOT export Wire* types from
4
+ * `src/index.tsx`.** The public TS API in `src/beekon.ts` and `src/types/` is
5
+ * what consumers use; mappers in `src/internal/mappers.ts` convert between the
6
+ * two.
5
7
  *
6
- * Wire types are flat (primitives + nested flat objects) because Codegen's
7
- * TS-to-Kotlin/ObjC type derivation has limited support for unions and
8
- * recursive structures. The state variants are encoded as a `type`
9
- * discriminator string with sibling fields nullable per variant.
8
+ * Wire types are deliberately **flat** (primitives + flat nested objects +
9
+ * arrays) because Codegen's TSKotlin/ObjC derivation has no support for unions
10
+ * or recursive structures:
11
+ *
12
+ * - **Enums travel as plain `string`** (e.g. `accuracyMode`, `quality`); the
13
+ * mappers translate to/from the public string-literal unions with a defensive
14
+ * fallback, so an unknown value never crashes a subscriber.
15
+ * - **Sum types** (`WireState`, `WireSyncStatus`) are a `type` discriminator
16
+ * string plus sibling fields nullable per variant.
17
+ * - **String maps** (`headers`, `extras`) travel as `WireKeyValue[]` pairs —
18
+ * Codegen has no portable dictionary type.
19
+ * - **Timestamps** are epoch-milliseconds; intervals are seconds.
10
20
  */
11
21
  import type { CodegenTypes, TurboModule } from 'react-native';
12
22
  import { TurboModuleRegistry } from 'react-native';
13
23
 
14
- export type WireAndroidNotification = {
24
+ /** A single string map entry — see `headers` / `setExtras`. */
25
+ export type WireKeyValue = {
26
+ key: string;
27
+ value: string;
28
+ };
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
+
57
+ export type WireSyncConfig = {
58
+ url: string;
59
+ headers: WireKeyValue[];
60
+ intervalSeconds: number;
61
+ batchSize: number;
62
+ /** Token-refresh recipe; omitted keeps static-header auth. */
63
+ auth?: WireAuthConfig;
64
+ };
65
+
66
+ /** Android-only foreground-service notification overrides. iOS ignores it. */
67
+ export type WireNotificationConfig = {
15
68
  title?: string;
16
69
  text?: string;
17
- smallIconResName?: string;
70
+ smallIcon?: string;
18
71
  };
19
72
 
20
73
  export type WireConfig = {
21
- /** Required at the wire level — the TS facade applies defaults (30 / 100). */
22
- intervalSeconds: number;
23
- /** Required at the wire level — the TS facade applies defaults (30 / 100). */
24
- distanceMeters: number;
25
- /** Android-only. iOS native module ignores. */
26
- androidNotification?: WireAndroidNotification;
74
+ /** All fields required at the wire level — the TS facade applies defaults. */
75
+ minTimeBetweenLocationsSeconds: number;
76
+ minDistanceBetweenLocationsMeters: number;
77
+ /** One of: 'high' | 'balanced' | 'low'. */
78
+ accuracyMode: string;
79
+ /** One of: 'keepTracking' | 'pause' | 'pauseWithCheckIns'. */
80
+ whenStationary: string;
81
+ stationaryRadiusMeters: number;
82
+ detectActivity: boolean;
83
+ /** Omitted keeps tracking local-only. */
84
+ sync?: WireSyncConfig;
85
+ /** Android-only; the iOS native module ignores it. */
86
+ notification?: WireNotificationConfig;
27
87
  };
28
88
 
29
89
  export type WireLocation = {
90
+ id: string;
30
91
  lat: number;
31
92
  lng: number;
32
93
  timestampMs: number;
33
- /** `null` when the OS did not report a value (e.g. low-confidence fix). */
94
+ /** `null` when the OS did not report a value. */
34
95
  accuracy: number | null;
35
96
  speed: number | null;
36
97
  bearing: number | null;
37
98
  altitude: number | null;
99
+ /** One of: 'ok' | 'lowAccuracy' | 'implausibleSpeed'. */
100
+ quality: string;
101
+ /** One of: 'interval' | 'motion' | 'checkIn' | 'geofence' | 'manual'. */
102
+ trigger: string;
103
+ /** One of: 'moving' | 'stationary' | 'unknown'. */
104
+ motion: string;
105
+ /** Activity enum string, or `null` unless activity detection is enabled. */
106
+ activity: string | null;
107
+ isMock: boolean;
108
+ };
109
+
110
+ export type WireGeofence = {
111
+ id: string;
112
+ lat: number;
113
+ lng: number;
114
+ radiusMeters: number;
115
+ notifyOnEntry: boolean;
116
+ notifyOnExit: boolean;
117
+ };
118
+
119
+ export type WireGeofenceEvent = {
120
+ id: string;
121
+ geofenceId: string;
122
+ /** One of: 'enter' | 'exit'. */
123
+ type: string;
124
+ timestampMs: number;
38
125
  };
39
126
 
40
127
  export type WireState = {
41
128
  /** One of: 'idle' | 'tracking' | 'stopped'. */
42
129
  type: string;
43
130
  /**
44
- * Only populated when `type === 'stopped'`. One of:
45
- * 'user' | 'permissionDenied' | 'locationServicesDisabled' | 'system'.
131
+ * Only populated when `type === 'stopped'`. One of: 'user' |
132
+ * 'permissionDenied' | 'locationServicesDisabled' | 'locationUnavailable' |
133
+ * 'system'.
46
134
  */
47
135
  stopReason?: string;
48
136
  };
49
137
 
138
+ export type WireSyncStatus = {
139
+ /** One of: 'idle' | 'pending' | 'failed'. */
140
+ type: string;
141
+ /** Only populated when `type === 'failed'`. One of: 'auth' | 'rejected'. */
142
+ failure?: string;
143
+ };
144
+
145
+ /**
146
+ * A token set the SDK rotated, delivered on `onAuthTokens`. `expiresAtMs` is
147
+ * epoch milliseconds; both it and `refreshToken` are `null` when absent.
148
+ */
149
+ export type WireAuthTokens = {
150
+ accessToken: string;
151
+ refreshToken: string | null;
152
+ expiresAtMs: number | null;
153
+ epoch: number;
154
+ };
155
+
50
156
  export interface Spec extends TurboModule {
51
- configure(config: WireConfig): Promise<void>;
157
+ /**
158
+ * The config crosses as an untyped object (`ReadableMap` on Android,
159
+ * `NSDictionary` on iOS) and is parsed natively. Typing it as the nested
160
+ * {@link WireConfig} struct would force the iOS ObjC++ layer to walk Codegen's
161
+ * generated C++ structs; the facade still builds a `WireConfig`-shaped value
162
+ * via `configToWire`, so the shape is enforced upstream.
163
+ */
164
+ configure(config: CodegenTypes.UnsafeObject): Promise<void>;
52
165
  start(): Promise<void>;
53
166
  stop(): Promise<void>;
54
- history(fromMs: number, toMs: number): Promise<WireLocation[]>;
167
+ resumeIfNeeded(): Promise<void>;
168
+
169
+ /**
170
+ * One immediate fix. `accuracy` `''` uses the configured mode (a plain
171
+ * `string` rather than `string | null` for portable Codegen, matching
172
+ * `deleteLocations`). Resolves a 0- or 1-element array — empty means timeout /
173
+ * no fix — rather than a nullable struct, which Codegen does not portably
174
+ * support. Rejects with `PERMISSION_DENIED` / `LOCATION_SERVICES_DISABLED` /
175
+ * `LOCATION_UNAVAILABLE` on a precondition failure.
176
+ */
177
+ getCurrentLocation(
178
+ timeoutMs: number,
179
+ accuracy: string
180
+ ): Promise<WireLocation[]>;
181
+
182
+ getLocations(fromMs: number, toMs: number): Promise<WireLocation[]>;
183
+ /**
184
+ * A negative `beforeMs` deletes all stored locations (the wire encoding of
185
+ * the public `deleteLocations()` no-argument case). A plain `number` is used
186
+ * rather than `number | null` for portable Codegen support across platforms.
187
+ */
188
+ deleteLocations(beforeMs: number): Promise<number>;
189
+ pendingUploadCount(): Promise<number>;
190
+
191
+ sync(): Promise<void>;
192
+ setExtras(entries: WireKeyValue[]): Promise<void>;
193
+
194
+ addGeofences(geofences: WireGeofence[]): Promise<void>;
195
+ removeGeofences(ids: string[]): Promise<void>;
196
+ listGeofences(): Promise<WireGeofence[]>;
55
197
 
56
198
  readonly onState: CodegenTypes.EventEmitter<WireState>;
57
199
  readonly onLocation: CodegenTypes.EventEmitter<WireLocation>;
200
+ readonly onGeofenceEvent: CodegenTypes.EventEmitter<WireGeofenceEvent>;
201
+ readonly onSyncStatus: CodegenTypes.EventEmitter<WireSyncStatus>;
202
+ readonly onAuthTokens: CodegenTypes.EventEmitter<WireAuthTokens>;
58
203
  }
59
204
 
60
205
  export default TurboModuleRegistry.getEnforcing<Spec>('BeekonRn');
package/src/beekon.ts CHANGED
@@ -1,111 +1,341 @@
1
1
  import NativeBeekon from './NativeBeekonRn';
2
+ import type { AuthTokens } from './types/auth';
2
3
  import type { BeekonConfig } from './types/config';
3
- import type { BeekonState } from './types/state';
4
+ import type { AccuracyMode } from './types/enums';
5
+ import type { BeekonGeofence, GeofenceEvent } from './types/geofence';
4
6
  import type { Location } from './types/location';
7
+ import type { BeekonState } from './types/state';
8
+ import type { SyncStatus } from './types/sync';
5
9
  import {
6
10
  configToWire,
11
+ geofenceToWire,
12
+ recordToEntries,
13
+ rethrowAsBeekonError,
14
+ wireToAuthTokens,
15
+ wireToGeofence,
16
+ wireToGeofenceEvent,
7
17
  wireToLocation,
8
18
  wireToState,
9
- rethrowAsBeekonError,
19
+ wireToSyncStatus,
10
20
  } from './internal/mappers';
11
21
 
22
+ type Listener<T> = (value: T) => void;
23
+
12
24
  /**
13
- * Public facade for the Beekon SDK. Mirrors the native APIs:
25
+ * Fans the five native EventEmitters out to multiple JS subscribers and adds
26
+ * replay-1 semantics for `state` / `syncStatus` / `authTokens` (RN EventEmitters
27
+ * don't replay, so we cache the latest value and hand it to new subscribers
28
+ * immediately — matching the native StateFlow / AsyncStream contract).
29
+ * `location` / `geofenceEvent` are plain fan-out with no replay.
30
+ *
31
+ * Native subscriptions are opened once, on first use, and kept for the app
32
+ * lifetime — the native module collects its flows continuously regardless, so
33
+ * this only keeps the JS cache warm.
34
+ */
35
+ class EventHub {
36
+ private subscribed = false;
37
+ private readonly stateListeners = new Set<Listener<BeekonState>>();
38
+ private readonly locationListeners = new Set<Listener<Location>>();
39
+ private readonly geofenceListeners = new Set<Listener<GeofenceEvent>>();
40
+ private readonly syncListeners = new Set<Listener<SyncStatus>>();
41
+ private readonly authTokenListeners = new Set<Listener<AuthTokens>>();
42
+ private lastState: BeekonState | undefined;
43
+ private lastSyncStatus: SyncStatus | undefined;
44
+ private lastAuthTokens: AuthTokens | undefined;
45
+
46
+ /** Idempotent: opens the native subscriptions the first time it is called. */
47
+ ensureSubscribed(): void {
48
+ if (this.subscribed) return;
49
+ this.subscribed = true;
50
+ NativeBeekon.onState((w) => {
51
+ const s = wireToState(w);
52
+ this.lastState = s;
53
+ this.stateListeners.forEach((cb) => cb(s));
54
+ });
55
+ NativeBeekon.onLocation((w) => {
56
+ const l = wireToLocation(w);
57
+ this.locationListeners.forEach((cb) => cb(l));
58
+ });
59
+ NativeBeekon.onGeofenceEvent((w) => {
60
+ const g = wireToGeofenceEvent(w);
61
+ this.geofenceListeners.forEach((cb) => cb(g));
62
+ });
63
+ NativeBeekon.onSyncStatus((w) => {
64
+ const s = wireToSyncStatus(w);
65
+ this.lastSyncStatus = s;
66
+ this.syncListeners.forEach((cb) => cb(s));
67
+ });
68
+ NativeBeekon.onAuthTokens((w) => {
69
+ const t = wireToAuthTokens(w);
70
+ this.lastAuthTokens = t;
71
+ this.authTokenListeners.forEach((cb) => cb(t));
72
+ });
73
+ }
74
+
75
+ onState(cb: Listener<BeekonState>): () => void {
76
+ this.ensureSubscribed();
77
+ this.stateListeners.add(cb);
78
+ if (this.lastState !== undefined) cb(this.lastState);
79
+ return () => {
80
+ this.stateListeners.delete(cb);
81
+ };
82
+ }
83
+
84
+ onLocation(cb: Listener<Location>): () => void {
85
+ this.ensureSubscribed();
86
+ this.locationListeners.add(cb);
87
+ return () => {
88
+ this.locationListeners.delete(cb);
89
+ };
90
+ }
91
+
92
+ onGeofenceEvent(cb: Listener<GeofenceEvent>): () => void {
93
+ this.ensureSubscribed();
94
+ this.geofenceListeners.add(cb);
95
+ return () => {
96
+ this.geofenceListeners.delete(cb);
97
+ };
98
+ }
99
+
100
+ onSyncStatus(cb: Listener<SyncStatus>): () => void {
101
+ this.ensureSubscribed();
102
+ this.syncListeners.add(cb);
103
+ if (this.lastSyncStatus !== undefined) cb(this.lastSyncStatus);
104
+ return () => {
105
+ this.syncListeners.delete(cb);
106
+ };
107
+ }
108
+
109
+ onAuthTokens(cb: Listener<AuthTokens>): () => void {
110
+ this.ensureSubscribed();
111
+ this.authTokenListeners.add(cb);
112
+ if (this.lastAuthTokens !== undefined) cb(this.lastAuthTokens);
113
+ return () => {
114
+ this.authTokenListeners.delete(cb);
115
+ };
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Public facade for the Beekon SDK — a 1:1 mirror of the native APIs:
14
121
  *
15
122
  * - Android: `in.wayq.beekon.Beekon` (object)
16
123
  * - iOS: `BeekonKit.Beekon.shared` (actor)
17
124
  *
18
- * Lifecycle: `configure(config)` `start()` ... `stop()` (idempotent).
19
- * Subscribe to `onState` / `onLocation` for live updates; query
20
- * `history(from, to)` for persisted fixes.
125
+ * There is **no `initialize()`** the native SDKs auto-initialize. Lifecycle:
126
+ * `configure(config)` (optional; defaults 30s / 100m) → `start()`
127
+ * `stop()`. `start()` / `stop()` **never throw** — `onState` is the single
128
+ * source of truth for whether tracking is active and why it stopped.
21
129
  *
22
- * **Threading:** Methods are safe to call from any JS context. Subscribers'
130
+ * **Threading:** methods are safe to call from any JS context; subscriber
23
131
  * callbacks fire on the JS thread.
24
132
  *
25
- * **Persistence invariant:** the SDK persists every gated fix natively; JS is
26
- * a passive observer. In background, the JS engine is not guaranteed to be
27
- * alive — for past fixes, use `history()`.
133
+ * **Persistence invariant:** the SDK persists every gated fix natively; JS is a
134
+ * passive observer that is not guaranteed to be alive in the background. For
135
+ * past fixes use `getLocations()`.
28
136
  */
29
137
  class BeekonImpl {
138
+ private readonly hub = new EventHub();
139
+
30
140
  /**
31
141
  * Set tracking parameters. Optional — `start()` falls back to the previously
32
- * persisted config or the SDK default (30s / 100m) if never configured.
33
- *
34
- * While tracking, the new gate values take effect on the next admitted fix
35
- * without restarting the underlying location subscription.
142
+ * persisted config, or the SDK defaults if never configured. While tracking,
143
+ * gate thresholds take effect on the next admitted fix without restarting the
144
+ * location subscription; `accuracyMode` applies on the next `start()`.
36
145
  */
37
146
  async configure(config: BeekonConfig): Promise<void> {
147
+ this.hub.ensureSubscribed();
148
+ await NativeBeekon.configure(configToWire(config));
149
+ }
150
+
151
+ /**
152
+ * Begin tracking. Never throws — observe `onState` for the outcome
153
+ * (`stopped(reason)` on a permission / services / system problem). Beekon
154
+ * ships no permission API: request permissions in the host app, then observe
155
+ * `onState`.
156
+ */
157
+ async start(): Promise<void> {
158
+ this.hub.ensureSubscribed();
159
+ await NativeBeekon.start();
160
+ }
161
+
162
+ /** Stop tracking. Idempotent. Registered geofences keep firing. */
163
+ async stop(): Promise<void> {
164
+ this.hub.ensureSubscribed();
165
+ await NativeBeekon.stop();
166
+ }
167
+
168
+ /**
169
+ * Resume tracking if a session was active before the app was terminated. A
170
+ * no-op if the user explicitly stopped. Call early in app startup.
171
+ *
172
+ * Note: on a background relaunch the JS engine may not be running, so true
173
+ * cold-launch resume also requires native host wiring (Android: call
174
+ * `Beekon.start()` from `Application.onCreate`; iOS: `registerBackgroundTasks`
175
+ * in the AppDelegate). See the README.
176
+ */
177
+ async resumeIfNeeded(): Promise<void> {
178
+ this.hub.ensureSubscribed();
179
+ await NativeBeekon.resumeIfNeeded();
180
+ }
181
+
182
+ /**
183
+ * Return a single fresh fix on demand — for the moment a host needs an
184
+ * immediate position (e.g. the instant a trip starts).
185
+ *
186
+ * Independent of tracking: works whether or not a session is running and never
187
+ * starts, stops, or disturbs one. The fix is not persisted and does not appear
188
+ * on `onLocation` — it is returned here only, tagged `'manual'`.
189
+ *
190
+ * Resolves `null` if no usable fix arrives within `timeoutMs` (default
191
+ * 15000). `accuracy` overrides the configured mode for this call only;
192
+ * omitted uses the configured mode.
193
+ *
194
+ * Throws `BeekonError` with kind `'permissionDenied'`,
195
+ * `'locationServicesDisabled'`, or `'locationUnavailable'` on a precondition
196
+ * failure.
197
+ */
198
+ async getCurrentLocation(options?: {
199
+ timeoutMs?: number;
200
+ accuracy?: AccuracyMode;
201
+ }): Promise<Location | null> {
38
202
  try {
39
- await NativeBeekon.configure(configToWire(config));
203
+ const wires = await NativeBeekon.getCurrentLocation(
204
+ options?.timeoutMs ?? 15000,
205
+ options?.accuracy ?? ''
206
+ );
207
+ return wires.length === 0 ? null : wireToLocation(wires[0]!);
40
208
  } catch (e) {
41
209
  rethrowAsBeekonError(e);
42
210
  }
43
211
  }
44
212
 
45
213
  /**
46
- * Begin tracking. State transitions to `tracking`.
214
+ * Read persisted fixes in the inclusive range `[from, to]`, oldest first.
215
+ * Reads come from the SDK's local storage (Room on Android, GRDB on iOS) — the
216
+ * source of truth even when JS was asleep. With sync enabled, rows are deleted
217
+ * locally once the server accepts them, so this returns only un-uploaded fixes.
47
218
  *
48
- * Throws a `BeekonError` with kind:
49
- * - `'permissionDenied'` — location permission not granted.
50
- * - `'locationServicesDisabled'` — system location services off (or GMS
51
- * unavailable on Android).
219
+ * Throws `BeekonError` with kind `'storage'` on a database read failure.
52
220
  */
53
- async start(): Promise<void> {
221
+ async getLocations(from: Date, to: Date): Promise<Location[]> {
54
222
  try {
55
- await NativeBeekon.start();
223
+ const wires = await NativeBeekon.getLocations(
224
+ from.getTime(),
225
+ to.getTime()
226
+ );
227
+ return wires.map(wireToLocation);
56
228
  } catch (e) {
57
229
  rethrowAsBeekonError(e);
58
230
  }
59
231
  }
60
232
 
61
- /** Stop tracking. Idempotent. State transitions to `stopped(user)`. */
62
- async stop(): Promise<void> {
233
+ /**
234
+ * Delete stored locations captured before `before` (all of them when
235
+ * `before` is omitted). Returns the number of rows removed. Throws
236
+ * `BeekonError` with kind `'storage'` on a failure.
237
+ */
238
+ async deleteLocations(before?: Date): Promise<number> {
63
239
  try {
64
- await NativeBeekon.stop();
240
+ // Negative is the wire sentinel for "delete all" (see NativeBeekonRn.ts).
241
+ return await NativeBeekon.deleteLocations(before ? before.getTime() : -1);
65
242
  } catch (e) {
66
243
  rethrowAsBeekonError(e);
67
244
  }
68
245
  }
69
246
 
70
247
  /**
71
- * Read persisted fixes in the time range [from, to]. Returns fixes in
72
- * chronological order. Reads come from the SDK's local storage (Room on
73
- * Android, GRDB on iOS) — the source of truth even when JS was asleep.
74
- *
75
- * Retention: fixes older than 7 days OR beyond the most recent 100K are
76
- * pruned automatically.
77
- *
78
- * Throws `BeekonError` with kind `'storageFailure'` on database read failure.
248
+ * The number of stored locations not yet uploaded. Throws `BeekonError` with
249
+ * kind `'storage'` on a failure.
79
250
  */
80
- async history(from: Date, to: Date): Promise<Location[]> {
251
+ async pendingUploadCount(): Promise<number> {
81
252
  try {
82
- const wires = await NativeBeekon.history(from.getTime(), to.getTime());
83
- return wires.map(wireToLocation);
253
+ return await NativeBeekon.pendingUploadCount();
84
254
  } catch (e) {
85
255
  rethrowAsBeekonError(e);
86
256
  }
87
257
  }
88
258
 
89
259
  /**
90
- * Subscribe to state transitions. Returns an unsubscribe function. The
91
- * current state is delivered to new subscribers immediately (replay-1
92
- * semantics, matching the native `state` flow / AsyncStream).
260
+ * Request an immediate upload. Best-effort and self-retrying; a no-op when
261
+ * sync is not configured.
262
+ */
263
+ async sync(): Promise<void> {
264
+ await NativeBeekon.sync();
265
+ }
266
+
267
+ /** Set custom key/value fields (e.g. `user_id`) included with every upload. */
268
+ async setExtras(extras: Record<string, string>): Promise<void> {
269
+ await NativeBeekon.setExtras(recordToEntries(extras));
270
+ }
271
+
272
+ /**
273
+ * Register geofences. Re-adding an `id` replaces the existing one. Throws
274
+ * `BeekonError` with kind `'invalidGeofence'` if any entry fails validation —
275
+ * none are added when one is invalid.
276
+ */
277
+ async addGeofences(geofences: BeekonGeofence[]): Promise<void> {
278
+ try {
279
+ await NativeBeekon.addGeofences(geofences.map(geofenceToWire));
280
+ } catch (e) {
281
+ rethrowAsBeekonError(e);
282
+ }
283
+ }
284
+
285
+ /** Unregister geofences by id. Unknown ids are ignored. */
286
+ async removeGeofences(ids: string[]): Promise<void> {
287
+ await NativeBeekon.removeGeofences(ids);
288
+ }
289
+
290
+ /** The currently registered geofences. */
291
+ async listGeofences(): Promise<BeekonGeofence[]> {
292
+ const wires = await NativeBeekon.listGeofences();
293
+ return wires.map(wireToGeofence);
294
+ }
295
+
296
+ /**
297
+ * Subscribe to tracking-state transitions (`idle` / `tracking` /
298
+ * `stopped(reason)`). The current state is delivered to new subscribers
299
+ * immediately (replay-1). Returns an unsubscribe function.
93
300
  */
94
301
  onState(cb: (s: BeekonState) => void): () => void {
95
- const subscription = NativeBeekon.onState((wire) => cb(wireToState(wire)));
96
- return () => subscription.remove();
302
+ return this.hub.onState(cb);
97
303
  }
98
304
 
99
305
  /**
100
- * Subscribe to gated locations as they arrive. Returns an unsubscribe
101
- * function. Broadcast (no replay) only delivers while the JS engine is
102
- * alive. For fixes emitted while JS was asleep, use `history()`.
306
+ * Subscribe to gated locations as they arrive. Broadcast (no replay) — only
307
+ * delivers while the JS engine is alive. For fixes emitted while JS was
308
+ * asleep, use `getLocations()`. Returns an unsubscribe function.
103
309
  */
104
310
  onLocation(cb: (l: Location) => void): () => void {
105
- const subscription = NativeBeekon.onLocation((wire) =>
106
- cb(wireToLocation(wire))
107
- );
108
- return () => subscription.remove();
311
+ return this.hub.onLocation(cb);
312
+ }
313
+
314
+ /**
315
+ * Subscribe to geofence enter / exit crossings. Broadcast (no replay).
316
+ * Returns an unsubscribe function.
317
+ */
318
+ onGeofenceEvent(cb: (e: GeofenceEvent) => void): () => void {
319
+ return this.hub.onGeofenceEvent(cb);
320
+ }
321
+
322
+ /**
323
+ * Subscribe to upload health (`idle` / `pending` / `failed(reason)`). The
324
+ * current status is delivered to new subscribers immediately (replay-1).
325
+ * Returns an unsubscribe function.
326
+ */
327
+ onSyncStatus(cb: (s: SyncStatus) => void): () => void {
328
+ return this.hub.onSyncStatus(cb);
329
+ }
330
+
331
+ /**
332
+ * Subscribe to token rotations the SDK performed during a native refresh (see
333
+ * `SyncConfig.auth`). Mirror them into your own session store. The latest
334
+ * rotation is delivered to new subscribers immediately (replay-1). Sensitive —
335
+ * delivered in-process only, never logged. Returns an unsubscribe function.
336
+ */
337
+ onAuthTokens(cb: (t: AuthTokens) => void): () => void {
338
+ return this.hub.onAuthTokens(cb);
109
339
  }
110
340
  }
111
341
 
package/src/index.tsx CHANGED
@@ -1,5 +1,27 @@
1
1
  export { Beekon } from './beekon';
2
- export type { BeekonConfig, AndroidNotificationConfig } from './types/config';
3
- export type { BeekonState, StopReason } from './types/state';
2
+
3
+ export type {
4
+ BeekonConfig,
5
+ SyncConfig,
6
+ NotificationConfig,
7
+ } from './types/config';
8
+ export type {
9
+ AccuracyMode,
10
+ StationaryMode,
11
+ LocationTrigger,
12
+ LocationQuality,
13
+ MotionState,
14
+ ActivityType,
15
+ AuthStrategy,
16
+ AuthBodyFormat,
17
+ } from './types/enums';
18
+ export type { AuthConfig, AuthResponseMapping, AuthTokens } from './types/auth';
4
19
  export type { Location } from './types/location';
20
+ export type {
21
+ BeekonGeofence,
22
+ GeofenceEvent,
23
+ Transition,
24
+ } from './types/geofence';
25
+ export type { BeekonState, StopReason } from './types/state';
26
+ export type { SyncStatus, SyncFailure } from './types/sync';
5
27
  export { BeekonError, type BeekonErrorKind } from './types/error';