@tsachit/react-native-geo-service 1.0.3 → 1.0.5

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.
package/README.md CHANGED
@@ -83,13 +83,17 @@ In your app's **`index.js`** (top level, outside any component):
83
83
 
84
84
  ```js
85
85
  import { AppRegistry } from 'react-native';
86
+ import RNGSAppRegistry from '@tsachit/react-native-geo-service';
86
87
  import App from './App';
87
88
 
88
89
  AppRegistry.registerComponent('YourApp', () => App);
89
90
 
90
91
  // Handles location events when the React context is not active.
91
92
  // Runs even when the app is killed (foreground service must be running).
92
- AppRegistry.registerHeadlessTask('GeoServiceHeadlessTask', () => async (location) => {
93
+ // Using RNGSAppRegistry.registerHeadlessTask() is preferred over
94
+ // AppRegistry.registerHeadlessTask() directly — it automatically keeps
95
+ // the debug panel's session store in sync while the app is killed.
96
+ RNGSAppRegistry.registerHeadlessTask(async (location) => {
93
97
  console.log('[Background] Location:', location);
94
98
  // Send to your server using a pre-stored auth token (e.g. SecureStore/Keychain).
95
99
  // Do not rely on in-memory state — this JS context is isolated.
@@ -188,10 +192,12 @@ const tracking = await RNGeoService.isTracking();
188
192
 
189
193
  ### Register headless task via the module
190
194
 
191
- Alternatively to `AppRegistry.registerHeadlessTask`, you can use the helper:
195
+ Use `registerHeadlessTask()` from the package instead of `AppRegistry.registerHeadlessTask()` directly it wraps your handler to automatically keep the debug panel's session metrics in sync while the app is killed:
192
196
 
193
197
  ```ts
194
- RNGeoService.registerHeadlessTask(async (location) => {
198
+ import RNGSAppRegistry from '@tsachit/react-native-geo-service';
199
+
200
+ RNGSAppRegistry.registerHeadlessTask(async (location) => {
195
201
  await sendToServer(location);
196
202
  });
197
203
  ```
@@ -246,7 +252,7 @@ Subscribe to location updates. Call `.remove()` on the returned subscription to
246
252
  Subscribe to location errors (e.g. permission revoked mid-session).
247
253
 
248
254
  ### `registerHeadlessTask(handler)` *(Android only)*
249
- Register a function to handle location events when the app is not in the foreground.
255
+ Register a function to handle location events when the app is not in the foreground. Preferred over `AppRegistry.registerHeadlessTask()` directly — automatically keeps `GeoSessionStore` in sync so the debug panel shows accurate Geopoints counts while the app is killed.
250
256
 
251
257
  ### `getBatteryInfo(): Promise<BatteryInfo>`
252
258
  Returns battery and session tracking metrics. See [Debug mode](#debug-mode) below.
@@ -343,7 +349,8 @@ await RNGeoService.stop(); // panel hides automatically
343
349
 
344
350
  | Minimized | Opened |
345
351
  |--------|-------------|
346
- | <img width="349" height="261" alt="image" src="https://github.com/user-attachments/assets/a6b43b93-7a93-485d-a68d-f4e4fe658011" /> | <img width="363" height="348" alt="image" src="https://github.com/user-attachments/assets/9c02ebc9-28f2-4983-982b-810c98a32dfe" /> |
352
+ | <img width="349" height="261" alt="image" src="https://github.com/user-attachments/assets/a6b43b93-7a93-485d-a68d-f4e4fe658011" /> | <img width="321" height="325" alt="image" src="https://github.com/user-attachments/assets/8715d657-0984-46c3-bf38-d782968ddc99" /> |
353
+
347
354
 
348
355
  ### Debug panel behaviour
349
356
 
@@ -352,17 +359,20 @@ The panel is a **draggable, minimizable floating overlay** that starts minimized
352
359
  - **Tap the 📍 circle** to expand
353
360
  - **Drag** by holding the striped header bar
354
361
  - **Minimize** with the ⊖ button — collapses back to the 📍 circle
362
+ - **Geopoints updates in real time** on every location event — no need to wait for the poll interval
363
+ - **"↺ Reset stats"** at the bottom right clears all accumulated data; Geopoints, elapsed time, battery drain, and the start timestamp all reset to zero
355
364
 
356
- **Metrics shown:**
365
+ **Metrics shown** (all values are cumulative across app restarts — see [GeoSessionStore](#geosessionstore)):
357
366
 
358
367
  | Metric | Description |
359
368
  |--------|-------------|
360
- | Tracking for | How long the current session has been running |
361
- | Geopoints | Total locations received |
369
+ | Started | Local date/time the very first tracking session began |
370
+ | Tracking for | Cumulative duration across all sessions |
371
+ | Geopoints | Total locations received across all sessions |
362
372
  | Updates/min | Average frequency of location updates |
363
- | GPS active | % of session time the GPS chip was on vs idle |
373
+ | GPS active | % of total time the GPS chip was on vs idle |
364
374
  | Battery now | Current device battery level |
365
- | Drained | Total device battery % dropped since `start()` |
375
+ | Drained | Total device battery % dropped since first `start()` |
366
376
  | Drain rate | Battery consumed per hour (total device, not just location) |
367
377
 
368
378
  **Smart suggestions** are shown automatically:
@@ -385,6 +395,27 @@ import { GeoDebugPanel } from '@tsachit/react-native-geo-service';
385
395
  <GeoDebugPanel pollInterval={15000} />
386
396
  ```
387
397
 
398
+ ### GeoSessionStore
399
+
400
+ All debug panel metrics are stored in-memory on the native side and would normally reset every time tracking restarts (app killed, OS killed the service, device rebooted). `GeoSessionStore` persists snapshots to `AsyncStorage` so the panel shows **cumulative totals** across sessions.
401
+
402
+ Requires [`@react-native-async-storage/async-storage`](https://github.com/react-native-async-storage/async-storage) to be installed in your app (optional peer dependency — the panel silently skips persistence if it is not present).
403
+
404
+ **Session boundaries** are detected automatically: when `batteryLevelAtStart` changes between snapshots, the previous session is archived before the new one begins. This prevents double-counting when the Android foreground service keeps running after the app is reopened.
405
+
406
+ The **"↺ Reset stats"** button inside the panel clears all accumulated data and the recorded start time so you can re-measure from scratch.
407
+
408
+ If you use `RNGSAppRegistry.registerHeadlessTask()`, `GeoSessionStore` is updated automatically on each headless location event — no extra code required. If you register via `AppRegistry.registerHeadlessTask()` directly, you can increment the counter manually:
409
+
410
+ ```ts
411
+ import { GeoSessionStore } from '@tsachit/react-native-geo-service';
412
+
413
+ AppRegistry.registerHeadlessTask('GeoServiceHeadlessTask', () => async (location) => {
414
+ await sendToServer(location);
415
+ await GeoSessionStore.onHeadlessLocation();
416
+ });
417
+ ```
418
+
388
419
  ---
389
420
 
390
421
  ## Headless mode explained
@@ -49,9 +49,14 @@ exports.GeoDebugPanel = void 0;
49
49
  const react_1 = __importStar(require("react"));
50
50
  const react_native_1 = require("react-native");
51
51
  const index_1 = __importDefault(require("./index"));
52
+ const GeoSessionStore_1 = require("./GeoSessionStore");
52
53
  const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = react_native_1.Dimensions.get('window');
53
54
  const PANEL_WIDTH = Math.round(SCREEN_WIDTH * 0.95);
54
55
  const PILL_SIZE = 50;
56
+ // Conservative estimate used when the panel hasn't rendered yet (onLayout not fired)
57
+ const PANEL_ESTIMATED_HEIGHT = 440;
58
+ const PANEL_BOTTOM_MARGIN = 20;
59
+ const PILL_INITIAL_BOTTOM_MARGIN = 120;
55
60
  // ─── Formatting helpers ──────────────────────────────────────────────────────
56
61
  function formatBattery(level) {
57
62
  return level < 0 ? 'N/A' : `${level.toFixed(1)}%`;
@@ -152,17 +157,37 @@ function makePanResponder(pan, lastPos, maxWidth, maxHeight, onTap) {
152
157
  });
153
158
  }
154
159
  // ─── Component ───────────────────────────────────────────────────────────────
160
+ function formatStartedAt(ts) {
161
+ if (!ts)
162
+ return '—';
163
+ const d = new Date(ts);
164
+ return d.toLocaleString(undefined, {
165
+ month: 'short', day: 'numeric',
166
+ hour: '2-digit', minute: '2-digit',
167
+ });
168
+ }
155
169
  const GeoDebugPanel = ({ pollInterval = 30000 }) => {
156
- var _a, _b, _c, _d, _e, _f, _g;
170
+ var _a, _b, _c, _d, _e;
157
171
  const [info, setInfo] = (0, react_1.useState)(null);
158
172
  const [minimized, setMinimized] = (0, react_1.useState)(true);
159
- const pan = (0, react_1.useRef)(new react_native_1.Animated.ValueXY({ x: 8, y: SCREEN_HEIGHT - 320 })).current;
160
- const lastPos = (0, react_1.useRef)({ x: 8, y: SCREEN_HEIGHT - 320 });
173
+ const [storeData, setStoreData] = (0, react_1.useState)({
174
+ accumulated: { updateCount: 0, elapsedSeconds: 0, gpsActiveSeconds: 0, drain: 0 },
175
+ lastSnapshot: null,
176
+ trackingStartedAt: null,
177
+ });
178
+ // Live count updated on every location event so Geopoints doesn't wait for the poll
179
+ const [realtimeCount, setRealtimeCount] = (0, react_1.useState)(0);
180
+ // Native updateCount value at the last reset — subtracted so the display starts from 0
181
+ const countBaseline = (0, react_1.useRef)(0);
182
+ const initialY = SCREEN_HEIGHT - PANEL_ESTIMATED_HEIGHT - PILL_INITIAL_BOTTOM_MARGIN;
183
+ const pan = (0, react_1.useRef)(new react_native_1.Animated.ValueXY({ x: 8, y: initialY })).current;
184
+ const lastPos = (0, react_1.useRef)({ x: 8, y: initialY });
161
185
  const panelHeight = (0, react_1.useRef)(0);
162
186
  const pillPanResponder = (0, react_1.useRef)(makePanResponder(pan, lastPos, PILL_SIZE, PILL_SIZE, () => {
187
+ const expandedHeight = panelHeight.current > 0 ? panelHeight.current : PANEL_ESTIMATED_HEIGHT;
163
188
  const clamped = {
164
189
  x: Math.min(lastPos.current.x, SCREEN_WIDTH - PANEL_WIDTH),
165
- y: Math.min(lastPos.current.y, SCREEN_HEIGHT - panelHeight.current),
190
+ y: Math.min(lastPos.current.y, SCREEN_HEIGHT - expandedHeight - PANEL_BOTTOM_MARGIN),
166
191
  };
167
192
  lastPos.current = clamped;
168
193
  pan.setValue(clamped);
@@ -188,13 +213,43 @@ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
188
213
  try {
189
214
  const data = yield index_1.default.getBatteryInfo();
190
215
  setInfo(data);
216
+ // Persist snapshot and reload accumulated so totals stay up to date
217
+ yield GeoSessionStore_1.GeoSessionStore.saveSnapshot(data);
218
+ const store = yield GeoSessionStore_1.GeoSessionStore.load();
219
+ setStoreData(store);
191
220
  }
192
221
  catch (_) { }
193
222
  }), []);
223
+ const handleReset = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
224
+ var _a;
225
+ yield GeoSessionStore_1.GeoSessionStore.clear();
226
+ setStoreData({ accumulated: { updateCount: 0, elapsedSeconds: 0, gpsActiveSeconds: 0, drain: 0 }, lastSnapshot: null, trackingStartedAt: null });
227
+ // Capture current native count as baseline so post-reset display starts from 0
228
+ countBaseline.current = (_a = info === null || info === void 0 ? void 0 : info.updateCount) !== null && _a !== void 0 ? _a : 0;
229
+ setRealtimeCount(0);
230
+ }), [info]);
194
231
  (0, react_1.useEffect)(() => {
232
+ // Load accumulated history on mount
233
+ GeoSessionStore_1.GeoSessionStore.load().then(setStoreData).catch(() => { });
195
234
  refresh();
196
235
  const id = setInterval(refresh, pollInterval);
197
- return () => clearInterval(id);
236
+ // Real-time Geopoints counter — increments on every location event without waiting for the poll
237
+ const locationSub = index_1.default.onLocation(() => {
238
+ setRealtimeCount(c => c + 1);
239
+ });
240
+ // Save snapshot when app is sent to background so stats survive unexpected kills
241
+ const appStateSub = react_native_1.AppState.addEventListener('change', status => {
242
+ if (status === 'background' || status === 'inactive') {
243
+ index_1.default.getBatteryInfo()
244
+ .then(GeoSessionStore_1.GeoSessionStore.saveSnapshot)
245
+ .catch(() => { });
246
+ }
247
+ });
248
+ return () => {
249
+ clearInterval(id);
250
+ locationSub.remove();
251
+ appStateSub.remove();
252
+ };
198
253
  }, [refresh, pollInterval]);
199
254
  // ── Minimized pill ──
200
255
  if (minimized) {
@@ -203,16 +258,27 @@ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
203
258
  </react_native_1.Animated.View>);
204
259
  }
205
260
  // Safely normalise — native side returns undefined for new fields until rebuilt
206
- const elapsed = (_a = info === null || info === void 0 ? void 0 : info.trackingElapsedSeconds) !== null && _a !== void 0 ? _a : 0;
207
- const updates = (_b = info === null || info === void 0 ? void 0 : info.updateCount) !== null && _b !== void 0 ? _b : 0;
208
- const upm = (_c = info === null || info === void 0 ? void 0 : info.updatesPerMinute) !== null && _c !== void 0 ? _c : 0;
209
- const gpsActive = (_d = info === null || info === void 0 ? void 0 : info.gpsActiveSeconds) !== null && _d !== void 0 ? _d : 0;
210
- const level = (_e = info === null || info === void 0 ? void 0 : info.level) !== null && _e !== void 0 ? _e : -1;
211
- const drain = (_f = info === null || info === void 0 ? void 0 : info.drainSinceStart) !== null && _f !== void 0 ? _f : 0;
212
- const drainRate = (_g = info === null || info === void 0 ? void 0 : info.drainRatePerHour) !== null && _g !== void 0 ? _g : 0;
261
+ const sessionElapsed = (_a = info === null || info === void 0 ? void 0 : info.trackingElapsedSeconds) !== null && _a !== void 0 ? _a : 0;
262
+ const sessionGpsActive = (_b = info === null || info === void 0 ? void 0 : info.gpsActiveSeconds) !== null && _b !== void 0 ? _b : 0;
263
+ const sessionDrain = (_c = info === null || info === void 0 ? void 0 : info.drainSinceStart) !== null && _c !== void 0 ? _c : 0;
264
+ const level = (_d = info === null || info === void 0 ? void 0 : info.level) !== null && _d !== void 0 ? _d : -1;
265
+ // Subtract baseline so the count restarts from 0 after a reset.
266
+ // Use the higher of the adjusted native count vs the live subscription count
267
+ // so we never show a number lower than what the native side knows about.
268
+ const nativeCountSinceReset = Math.max(0, ((_e = info === null || info === void 0 ? void 0 : info.updateCount) !== null && _e !== void 0 ? _e : 0) - countBaseline.current);
269
+ const sessionUpdates = Math.max(nativeCountSinceReset, realtimeCount);
270
+ // Combine current session with accumulated history from previous sessions
271
+ const acc = storeData.accumulated;
272
+ const elapsed = acc.elapsedSeconds + sessionElapsed;
273
+ const updates = acc.updateCount + sessionUpdates;
274
+ const gpsActive = acc.gpsActiveSeconds + sessionGpsActive;
275
+ const drain = acc.drain + sessionDrain;
276
+ const upm = elapsed > 0 ? updates / (elapsed / 60) : 0;
277
+ const drainRate = elapsed > 0 ? drain / (elapsed / 3600) : 0;
213
278
  const safeInfo = info
214
279
  ? Object.assign(Object.assign({}, info), { trackingElapsedSeconds: elapsed, updateCount: updates, updatesPerMinute: upm, gpsActiveSeconds: gpsActive, drainRatePerHour: drainRate }) : null;
215
280
  const suggestions = safeInfo ? getSuggestions(safeInfo) : [];
281
+ const startedAt = formatStartedAt(storeData.trackingStartedAt);
216
282
  // ── Expanded panel ──
217
283
  return (<react_native_1.Animated.View style={[styles.container, { transform: pan.getTranslateTransform() }]} onLayout={e => { panelHeight.current = e.nativeEvent.layout.height; }}>
218
284
  {/* Drag header */}
@@ -231,7 +297,8 @@ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
231
297
  {info ? (<>
232
298
  {/* ── Metrics grid ── */}
233
299
  <react_native_1.View style={styles.grid}>
234
- <MetricBox label="Tracking for" value={elapsed > 0 ? formatElapsed(elapsed) : '—'} desc="session duration"/>
300
+ <MetricBox label="Started" value={startedAt} desc="first session began"/>
301
+ <MetricBox label="Tracking for" value={elapsed > 0 ? formatElapsed(elapsed) : '—'} desc="cumulative duration"/>
235
302
  <MetricBox label="Geopoints" value={`${updates}`} desc="total locations received"/>
236
303
  <MetricBox label="Updates/min" value={upm > 0 ? upm.toFixed(1) : '—'} desc="higher = more battery"/>
237
304
  <MetricBox label="GPS active" value={elapsed > 0 ? formatGpsPercent(gpsActive, elapsed) : '—'} desc="lower = adaptive saving"/>
@@ -248,6 +315,13 @@ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
248
315
  <react_native_1.Text style={[styles.suggestionText, { color: s.color }]}>{s.text}</react_native_1.Text>
249
316
  </react_native_1.View>))}
250
317
  </>) : (<react_native_1.Text style={styles.label}>Waiting for data…</react_native_1.Text>)}
318
+
319
+ {/* ── Reset button ── */}
320
+ <react_native_1.View style={styles.resetRow}>
321
+ <react_native_1.TouchableOpacity onPress={handleReset} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
322
+ <react_native_1.Text style={styles.resetBtn}>↺ Reset stats</react_native_1.Text>
323
+ </react_native_1.TouchableOpacity>
324
+ </react_native_1.View>
251
325
  </react_native_1.View>
252
326
  </react_native_1.Animated.View>);
253
327
  };
@@ -298,7 +372,7 @@ const styles = react_native_1.StyleSheet.create({
298
372
  minWidth: '30%', flex: 1,
299
373
  alignItems: 'center',
300
374
  },
301
- metricValue: { color: '#fff', fontWeight: 'bold', fontSize: 15 },
375
+ metricValue: { color: '#fff', fontWeight: 'bold', fontSize: 15, textAlign: 'center' },
302
376
  metricLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 9, marginTop: 2, textAlign: 'center' },
303
377
  metricSub: { color: 'rgba(255,255,255,0.3)', fontSize: 8, textAlign: 'center' },
304
378
  // Suggestions
@@ -307,6 +381,9 @@ const styles = react_native_1.StyleSheet.create({
307
381
  suggestion: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 5, gap: 6 },
308
382
  suggestionEmoji: { fontSize: 12, lineHeight: 16 },
309
383
  suggestionText: { fontSize: 11, lineHeight: 16, flex: 1 },
384
+ // Reset
385
+ resetRow: { alignItems: 'flex-end', marginTop: 10 },
386
+ resetBtn: { color: 'rgba(255,255,255,0.45)', fontSize: 13, paddingVertical: 6, paddingHorizontal: 10 },
310
387
  // Minimized pill
311
388
  pill: {
312
389
  position: 'absolute',
@@ -0,0 +1,65 @@
1
+ /**
2
+ * GeoSessionStore — persists debug panel metrics across app sessions.
3
+ *
4
+ * Metrics live in-memory on the native side and reset whenever tracking
5
+ * restarts (app killed, OS killed the service, device rebooted). This store
6
+ * snapshots values to AsyncStorage so the debug panel can show cumulative
7
+ * totals spanning multiple sessions.
8
+ *
9
+ * Storage key: @rn_geo_service/debug_session
10
+ *
11
+ * Schema:
12
+ * accumulated — sum of all fully-closed previous sessions
13
+ * lastSnapshot — most recent known state; used to detect session boundaries
14
+ * trackingStartedAt — Unix ms timestamp of the very first start() ever recorded
15
+ * (not overwritten until clear() is called)
16
+ */
17
+ import type { BatteryInfo } from './types';
18
+ export interface AccumulatedStats {
19
+ updateCount: number;
20
+ elapsedSeconds: number;
21
+ gpsActiveSeconds: number;
22
+ drain: number;
23
+ }
24
+ export interface SnapshotStats extends AccumulatedStats {
25
+ batteryLevelAtStart: number;
26
+ }
27
+ export interface StoreData {
28
+ accumulated: AccumulatedStats;
29
+ lastSnapshot: SnapshotStats | null;
30
+ /** Unix ms — when the very first session began. Null until first saveSnapshot(). */
31
+ trackingStartedAt: number | null;
32
+ }
33
+ /**
34
+ * Load accumulated stats + trackingStartedAt from storage.
35
+ * Returns zero defaults if nothing has been stored yet.
36
+ */
37
+ declare function load(): Promise<StoreData>;
38
+ /**
39
+ * Snapshot the current session's BatteryInfo into storage.
40
+ *
41
+ * - Detects session boundaries via batteryLevelAtStart:
42
+ * if it differs from the stored lastSnapshot, the previous session is
43
+ * archived into accumulated before storing the new snapshot.
44
+ * - Records trackingStartedAt on the very first call (never overwritten).
45
+ */
46
+ declare function saveSnapshot(info: BatteryInfo): Promise<void>;
47
+ /**
48
+ * Called from the Android headless task when the app is killed but the
49
+ * foreground service is still delivering location updates.
50
+ * Increments the updateCount in lastSnapshot by 1.
51
+ * Safe to call from a HeadlessJS context (no native bridge required).
52
+ */
53
+ declare function onHeadlessLocation(): Promise<void>;
54
+ /**
55
+ * Clear all accumulated data, lastSnapshot, and trackingStartedAt.
56
+ * The panel reverts to current-session-only view after calling this.
57
+ */
58
+ declare function clear(): Promise<void>;
59
+ export declare const GeoSessionStore: {
60
+ load: typeof load;
61
+ saveSnapshot: typeof saveSnapshot;
62
+ onHeadlessLocation: typeof onHeadlessLocation;
63
+ clear: typeof clear;
64
+ };
65
+ export {};
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ /**
3
+ * GeoSessionStore — persists debug panel metrics across app sessions.
4
+ *
5
+ * Metrics live in-memory on the native side and reset whenever tracking
6
+ * restarts (app killed, OS killed the service, device rebooted). This store
7
+ * snapshots values to AsyncStorage so the debug panel can show cumulative
8
+ * totals spanning multiple sessions.
9
+ *
10
+ * Storage key: @rn_geo_service/debug_session
11
+ *
12
+ * Schema:
13
+ * accumulated — sum of all fully-closed previous sessions
14
+ * lastSnapshot — most recent known state; used to detect session boundaries
15
+ * trackingStartedAt — Unix ms timestamp of the very first start() ever recorded
16
+ * (not overwritten until clear() is called)
17
+ */
18
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
19
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
20
+ return new (P || (P = Promise))(function (resolve, reject) {
21
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
22
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
23
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
24
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
25
+ });
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.GeoSessionStore = void 0;
29
+ const STORAGE_KEY = '@rn_geo_service/debug_session';
30
+ // Lazily required so the package doesn't hard-crash if AsyncStorage is not
31
+ // installed in the host app (it's a peerDependency).
32
+ function getStorage() {
33
+ try {
34
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
35
+ return require('@react-native-async-storage/async-storage').default;
36
+ }
37
+ catch (_) {
38
+ return null;
39
+ }
40
+ }
41
+ const ZERO_ACCUMULATED = {
42
+ updateCount: 0,
43
+ elapsedSeconds: 0,
44
+ gpsActiveSeconds: 0,
45
+ drain: 0,
46
+ };
47
+ function addStats(a, b) {
48
+ return {
49
+ updateCount: a.updateCount + b.updateCount,
50
+ elapsedSeconds: a.elapsedSeconds + b.elapsedSeconds,
51
+ gpsActiveSeconds: a.gpsActiveSeconds + b.gpsActiveSeconds,
52
+ drain: a.drain + b.drain,
53
+ };
54
+ }
55
+ function readRaw() {
56
+ return __awaiter(this, void 0, void 0, function* () {
57
+ const storage = getStorage();
58
+ if (!storage)
59
+ return { accumulated: Object.assign({}, ZERO_ACCUMULATED), lastSnapshot: null, trackingStartedAt: null };
60
+ try {
61
+ const raw = yield storage.getItem(STORAGE_KEY);
62
+ if (!raw)
63
+ return { accumulated: Object.assign({}, ZERO_ACCUMULATED), lastSnapshot: null, trackingStartedAt: null };
64
+ return JSON.parse(raw);
65
+ }
66
+ catch (_) {
67
+ return { accumulated: Object.assign({}, ZERO_ACCUMULATED), lastSnapshot: null, trackingStartedAt: null };
68
+ }
69
+ });
70
+ }
71
+ function writeRaw(data) {
72
+ return __awaiter(this, void 0, void 0, function* () {
73
+ const storage = getStorage();
74
+ if (!storage)
75
+ return;
76
+ try {
77
+ yield storage.setItem(STORAGE_KEY, JSON.stringify(data));
78
+ }
79
+ catch (_) { }
80
+ });
81
+ }
82
+ /**
83
+ * Load accumulated stats + trackingStartedAt from storage.
84
+ * Returns zero defaults if nothing has been stored yet.
85
+ */
86
+ function load() {
87
+ return __awaiter(this, void 0, void 0, function* () {
88
+ return readRaw();
89
+ });
90
+ }
91
+ /**
92
+ * Snapshot the current session's BatteryInfo into storage.
93
+ *
94
+ * - Detects session boundaries via batteryLevelAtStart:
95
+ * if it differs from the stored lastSnapshot, the previous session is
96
+ * archived into accumulated before storing the new snapshot.
97
+ * - Records trackingStartedAt on the very first call (never overwritten).
98
+ */
99
+ function saveSnapshot(info) {
100
+ return __awaiter(this, void 0, void 0, function* () {
101
+ var _a, _b, _c, _d, _e;
102
+ const data = yield readRaw();
103
+ const newSnapshot = {
104
+ updateCount: (_a = info.updateCount) !== null && _a !== void 0 ? _a : 0,
105
+ elapsedSeconds: (_b = info.trackingElapsedSeconds) !== null && _b !== void 0 ? _b : 0,
106
+ gpsActiveSeconds: (_c = info.gpsActiveSeconds) !== null && _c !== void 0 ? _c : 0,
107
+ drain: (_d = info.drainSinceStart) !== null && _d !== void 0 ? _d : 0,
108
+ batteryLevelAtStart: (_e = info.levelAtStart) !== null && _e !== void 0 ? _e : -1,
109
+ };
110
+ let { accumulated, trackingStartedAt } = data;
111
+ // Detect session boundary — a different batteryLevelAtStart means start() was called again
112
+ if (data.lastSnapshot !== null &&
113
+ data.lastSnapshot.batteryLevelAtStart !== newSnapshot.batteryLevelAtStart) {
114
+ accumulated = addStats(accumulated, data.lastSnapshot);
115
+ }
116
+ // Record start time once — on the very first snapshot ever, or after a clear()
117
+ if (trackingStartedAt === null) {
118
+ trackingStartedAt = Date.now();
119
+ }
120
+ yield writeRaw({ accumulated, lastSnapshot: newSnapshot, trackingStartedAt });
121
+ });
122
+ }
123
+ /**
124
+ * Called from the Android headless task when the app is killed but the
125
+ * foreground service is still delivering location updates.
126
+ * Increments the updateCount in lastSnapshot by 1.
127
+ * Safe to call from a HeadlessJS context (no native bridge required).
128
+ */
129
+ function onHeadlessLocation() {
130
+ return __awaiter(this, void 0, void 0, function* () {
131
+ var _a;
132
+ const data = yield readRaw();
133
+ const snap = (_a = data.lastSnapshot) !== null && _a !== void 0 ? _a : {
134
+ updateCount: 0,
135
+ elapsedSeconds: 0,
136
+ gpsActiveSeconds: 0,
137
+ drain: 0,
138
+ batteryLevelAtStart: -1,
139
+ };
140
+ yield writeRaw(Object.assign(Object.assign({}, data), { lastSnapshot: Object.assign(Object.assign({}, snap), { updateCount: snap.updateCount + 1 }) }));
141
+ });
142
+ }
143
+ /**
144
+ * Clear all accumulated data, lastSnapshot, and trackingStartedAt.
145
+ * The panel reverts to current-session-only view after calling this.
146
+ */
147
+ function clear() {
148
+ return __awaiter(this, void 0, void 0, function* () {
149
+ const storage = getStorage();
150
+ if (!storage)
151
+ return;
152
+ try {
153
+ yield storage.removeItem(STORAGE_KEY);
154
+ }
155
+ catch (_) { }
156
+ });
157
+ }
158
+ exports.GeoSessionStore = { load, saveSnapshot, onHeadlessLocation, clear };
package/lib/index.d.ts CHANGED
@@ -86,3 +86,5 @@ declare const RNGeoService: {
86
86
  export default RNGeoService;
87
87
  export { GeoDebugPanel } from './GeoDebugPanel';
88
88
  export { GeoDebugOverlay } from './GeoDebugOverlay';
89
+ export { GeoSessionStore } from './GeoSessionStore';
90
+ export type { AccumulatedStats, StoreData } from './GeoSessionStore';
package/lib/index.js CHANGED
@@ -23,7 +23,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
23
23
  });
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.GeoDebugOverlay = exports.GeoDebugPanel = void 0;
26
+ exports.GeoSessionStore = exports.GeoDebugOverlay = exports.GeoDebugPanel = void 0;
27
27
  exports._isDebugMode = _isDebugMode;
28
28
  const react_native_1 = require("react-native");
29
29
  __exportStar(require("./types"), exports);
@@ -148,7 +148,12 @@ function registerHeadlessTask(handler) {
148
148
  if (react_native_1.Platform.OS !== 'android')
149
149
  return;
150
150
  const taskName = DEFAULT_CONFIG.backgroundTaskName;
151
- react_native_1.AppRegistry.registerHeadlessTask(taskName, () => handler);
151
+ react_native_1.AppRegistry.registerHeadlessTask(taskName, () => (location) => __awaiter(this, void 0, void 0, function* () {
152
+ // Keep GeoSessionStore's updateCount in sync while the app is killed
153
+ const { GeoSessionStore } = require('./GeoSessionStore');
154
+ yield GeoSessionStore.onHeadlessLocation();
155
+ yield handler(location);
156
+ }));
152
157
  }
153
158
  /**
154
159
  * Returns battery information including current level and drain since tracking started.
@@ -185,3 +190,5 @@ var GeoDebugPanel_1 = require("./GeoDebugPanel");
185
190
  Object.defineProperty(exports, "GeoDebugPanel", { enumerable: true, get: function () { return GeoDebugPanel_1.GeoDebugPanel; } });
186
191
  var GeoDebugOverlay_1 = require("./GeoDebugOverlay");
187
192
  Object.defineProperty(exports, "GeoDebugOverlay", { enumerable: true, get: function () { return GeoDebugOverlay_1.GeoDebugOverlay; } });
193
+ var GeoSessionStore_1 = require("./GeoSessionStore");
194
+ Object.defineProperty(exports, "GeoSessionStore", { enumerable: true, get: function () { return GeoSessionStore_1.GeoSessionStore; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsachit/react-native-geo-service",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Battery-efficient background geolocation for React Native with headless support",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -25,7 +25,13 @@
25
25
  "author": "Sachit Karki",
26
26
  "license": "MIT",
27
27
  "peerDependencies": {
28
- "react-native": ">=0.60.0"
28
+ "react-native": ">=0.60.0",
29
+ "@react-native-async-storage/async-storage": ">=1.0.0"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "@react-native-async-storage/async-storage": {
33
+ "optional": true
34
+ }
29
35
  },
30
36
  "devDependencies": {
31
37
  "@types/react": "^18.3.28",