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

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.
@@ -352,17 +358,19 @@ The panel is a **draggable, minimizable floating overlay** that starts minimized
352
358
  - **Tap the 📍 circle** to expand
353
359
  - **Drag** by holding the striped header bar
354
360
  - **Minimize** with the ⊖ button — collapses back to the 📍 circle
361
+ - **Geopoints updates in real time** on every location event — no need to wait for the poll interval
355
362
 
356
- **Metrics shown:**
363
+ **Metrics shown** (all values are cumulative across app restarts — see [GeoSessionStore](#geosessionstore)):
357
364
 
358
365
  | Metric | Description |
359
366
  |--------|-------------|
360
- | Tracking for | How long the current session has been running |
361
- | Geopoints | Total locations received |
367
+ | Started | Local date/time the very first tracking session began |
368
+ | Tracking for | Cumulative duration across all sessions |
369
+ | Geopoints | Total locations received across all sessions |
362
370
  | Updates/min | Average frequency of location updates |
363
- | GPS active | % of session time the GPS chip was on vs idle |
371
+ | GPS active | % of total time the GPS chip was on vs idle |
364
372
  | Battery now | Current device battery level |
365
- | Drained | Total device battery % dropped since `start()` |
373
+ | Drained | Total device battery % dropped since first `start()` |
366
374
  | Drain rate | Battery consumed per hour (total device, not just location) |
367
375
 
368
376
  **Smart suggestions** are shown automatically:
@@ -385,6 +393,27 @@ import { GeoDebugPanel } from '@tsachit/react-native-geo-service';
385
393
  <GeoDebugPanel pollInterval={15000} />
386
394
  ```
387
395
 
396
+ ### GeoSessionStore
397
+
398
+ 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.
399
+
400
+ 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).
401
+
402
+ **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.
403
+
404
+ The **"↺ Reset stats"** button inside the panel clears all accumulated data and the recorded start time so you can re-measure from scratch.
405
+
406
+ 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:
407
+
408
+ ```ts
409
+ import { GeoSessionStore } from '@tsachit/react-native-geo-service';
410
+
411
+ AppRegistry.registerHeadlessTask('GeoServiceHeadlessTask', () => async (location) => {
412
+ await sendToServer(location);
413
+ await GeoSessionStore.onHeadlessLocation();
414
+ });
415
+ ```
416
+
388
417
  ---
389
418
 
390
419
  ## 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,35 @@ 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
+ const initialY = SCREEN_HEIGHT - PANEL_ESTIMATED_HEIGHT - PILL_INITIAL_BOTTOM_MARGIN;
181
+ const pan = (0, react_1.useRef)(new react_native_1.Animated.ValueXY({ x: 8, y: initialY })).current;
182
+ const lastPos = (0, react_1.useRef)({ x: 8, y: initialY });
161
183
  const panelHeight = (0, react_1.useRef)(0);
162
184
  const pillPanResponder = (0, react_1.useRef)(makePanResponder(pan, lastPos, PILL_SIZE, PILL_SIZE, () => {
185
+ const expandedHeight = panelHeight.current > 0 ? panelHeight.current : PANEL_ESTIMATED_HEIGHT;
163
186
  const clamped = {
164
187
  x: Math.min(lastPos.current.x, SCREEN_WIDTH - PANEL_WIDTH),
165
- y: Math.min(lastPos.current.y, SCREEN_HEIGHT - panelHeight.current),
188
+ y: Math.min(lastPos.current.y, SCREEN_HEIGHT - expandedHeight - PANEL_BOTTOM_MARGIN),
166
189
  };
167
190
  lastPos.current = clamped;
168
191
  pan.setValue(clamped);
@@ -188,13 +211,40 @@ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
188
211
  try {
189
212
  const data = yield index_1.default.getBatteryInfo();
190
213
  setInfo(data);
214
+ // Persist snapshot and reload accumulated so totals stay up to date
215
+ yield GeoSessionStore_1.GeoSessionStore.saveSnapshot(data);
216
+ const store = yield GeoSessionStore_1.GeoSessionStore.load();
217
+ setStoreData(store);
191
218
  }
192
219
  catch (_) { }
193
220
  }), []);
221
+ const handleReset = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
222
+ yield GeoSessionStore_1.GeoSessionStore.clear();
223
+ setStoreData({ accumulated: { updateCount: 0, elapsedSeconds: 0, gpsActiveSeconds: 0, drain: 0 }, lastSnapshot: null, trackingStartedAt: null });
224
+ setRealtimeCount(0);
225
+ }), []);
194
226
  (0, react_1.useEffect)(() => {
227
+ // Load accumulated history on mount
228
+ GeoSessionStore_1.GeoSessionStore.load().then(setStoreData).catch(() => { });
195
229
  refresh();
196
230
  const id = setInterval(refresh, pollInterval);
197
- return () => clearInterval(id);
231
+ // Real-time Geopoints counter — increments on every location event without waiting for the poll
232
+ const locationSub = index_1.default.onLocation(() => {
233
+ setRealtimeCount(c => c + 1);
234
+ });
235
+ // Save snapshot when app is sent to background so stats survive unexpected kills
236
+ const appStateSub = react_native_1.AppState.addEventListener('change', status => {
237
+ if (status === 'background' || status === 'inactive') {
238
+ index_1.default.getBatteryInfo()
239
+ .then(GeoSessionStore_1.GeoSessionStore.saveSnapshot)
240
+ .catch(() => { });
241
+ }
242
+ });
243
+ return () => {
244
+ clearInterval(id);
245
+ locationSub.remove();
246
+ appStateSub.remove();
247
+ };
198
248
  }, [refresh, pollInterval]);
199
249
  // ── Minimized pill ──
200
250
  if (minimized) {
@@ -203,16 +253,25 @@ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
203
253
  </react_native_1.Animated.View>);
204
254
  }
205
255
  // 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;
256
+ const sessionElapsed = (_a = info === null || info === void 0 ? void 0 : info.trackingElapsedSeconds) !== null && _a !== void 0 ? _a : 0;
257
+ const sessionGpsActive = (_b = info === null || info === void 0 ? void 0 : info.gpsActiveSeconds) !== null && _b !== void 0 ? _b : 0;
258
+ const sessionDrain = (_c = info === null || info === void 0 ? void 0 : info.drainSinceStart) !== null && _c !== void 0 ? _c : 0;
259
+ const level = (_d = info === null || info === void 0 ? void 0 : info.level) !== null && _d !== void 0 ? _d : -1;
260
+ // Use the higher of the native poll count vs the live subscription count
261
+ // so we never show a number lower than what the native side knows about
262
+ const sessionUpdates = Math.max((_e = info === null || info === void 0 ? void 0 : info.updateCount) !== null && _e !== void 0 ? _e : 0, realtimeCount);
263
+ // Combine current session with accumulated history from previous sessions
264
+ const acc = storeData.accumulated;
265
+ const elapsed = acc.elapsedSeconds + sessionElapsed;
266
+ const updates = acc.updateCount + sessionUpdates;
267
+ const gpsActive = acc.gpsActiveSeconds + sessionGpsActive;
268
+ const drain = acc.drain + sessionDrain;
269
+ const upm = elapsed > 0 ? updates / (elapsed / 60) : 0;
270
+ const drainRate = elapsed > 0 ? drain / (elapsed / 3600) : 0;
213
271
  const safeInfo = info
214
272
  ? Object.assign(Object.assign({}, info), { trackingElapsedSeconds: elapsed, updateCount: updates, updatesPerMinute: upm, gpsActiveSeconds: gpsActive, drainRatePerHour: drainRate }) : null;
215
273
  const suggestions = safeInfo ? getSuggestions(safeInfo) : [];
274
+ const startedAt = formatStartedAt(storeData.trackingStartedAt);
216
275
  // ── Expanded panel ──
217
276
  return (<react_native_1.Animated.View style={[styles.container, { transform: pan.getTranslateTransform() }]} onLayout={e => { panelHeight.current = e.nativeEvent.layout.height; }}>
218
277
  {/* Drag header */}
@@ -231,7 +290,8 @@ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
231
290
  {info ? (<>
232
291
  {/* ── Metrics grid ── */}
233
292
  <react_native_1.View style={styles.grid}>
234
- <MetricBox label="Tracking for" value={elapsed > 0 ? formatElapsed(elapsed) : '—'} desc="session duration"/>
293
+ <MetricBox label="Started" value={startedAt} desc="first session began"/>
294
+ <MetricBox label="Tracking for" value={elapsed > 0 ? formatElapsed(elapsed) : '—'} desc="cumulative duration"/>
235
295
  <MetricBox label="Geopoints" value={`${updates}`} desc="total locations received"/>
236
296
  <MetricBox label="Updates/min" value={upm > 0 ? upm.toFixed(1) : '—'} desc="higher = more battery"/>
237
297
  <MetricBox label="GPS active" value={elapsed > 0 ? formatGpsPercent(gpsActive, elapsed) : '—'} desc="lower = adaptive saving"/>
@@ -248,6 +308,13 @@ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
248
308
  <react_native_1.Text style={[styles.suggestionText, { color: s.color }]}>{s.text}</react_native_1.Text>
249
309
  </react_native_1.View>))}
250
310
  </>) : (<react_native_1.Text style={styles.label}>Waiting for data…</react_native_1.Text>)}
311
+
312
+ {/* ── Reset button ── */}
313
+ <react_native_1.View style={styles.resetRow}>
314
+ <react_native_1.TouchableOpacity onPress={handleReset} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
315
+ <react_native_1.Text style={styles.resetBtn}>↺ Reset stats</react_native_1.Text>
316
+ </react_native_1.TouchableOpacity>
317
+ </react_native_1.View>
251
318
  </react_native_1.View>
252
319
  </react_native_1.Animated.View>);
253
320
  };
@@ -298,7 +365,7 @@ const styles = react_native_1.StyleSheet.create({
298
365
  minWidth: '30%', flex: 1,
299
366
  alignItems: 'center',
300
367
  },
301
- metricValue: { color: '#fff', fontWeight: 'bold', fontSize: 15 },
368
+ metricValue: { color: '#fff', fontWeight: 'bold', fontSize: 15, textAlign: 'center' },
302
369
  metricLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 9, marginTop: 2, textAlign: 'center' },
303
370
  metricSub: { color: 'rgba(255,255,255,0.3)', fontSize: 8, textAlign: 'center' },
304
371
  // Suggestions
@@ -307,6 +374,9 @@ const styles = react_native_1.StyleSheet.create({
307
374
  suggestion: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 5, gap: 6 },
308
375
  suggestionEmoji: { fontSize: 12, lineHeight: 16 },
309
376
  suggestionText: { fontSize: 11, lineHeight: 16, flex: 1 },
377
+ // Reset
378
+ resetRow: { alignItems: 'flex-end', marginTop: 10 },
379
+ resetBtn: { color: 'rgba(255,255,255,0.45)', fontSize: 13, paddingVertical: 6, paddingHorizontal: 10 },
310
380
  // Minimized pill
311
381
  pill: {
312
382
  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.4",
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",