@tsachit/react-native-geo-service 1.0.2 → 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
@@ -6,7 +6,7 @@ Battery-efficient background geolocation for React Native — a lightweight, fre
6
6
  - Keeps tracking when the app is backgrounded or killed (headless mode)
7
7
  - Uses `FusedLocationProviderClient` on Android and `CLLocationManager` on iOS
8
8
  - **Adaptive accuracy** — GPS turns off automatically when the device is idle and wakes the moment movement is detected
9
- - **Debug panel** — draggable floating overlay that mounts automatically when `debug: true`, showing live metrics, GPS activity, and battery saving suggestions with no component needed in the app tree
9
+ - **Debug panel** — draggable floating overlay showing live metrics, GPS activity, and battery saving suggestions; add `<GeoDebugOverlay />` once and it self-manages based on `debug: true` and tracking state
10
10
  - Fully configurable from JavaScript — no API keys, no license required
11
11
 
12
12
  ---
@@ -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.
@@ -284,10 +290,10 @@ interface BatteryInfo {
284
290
  levelAtStart: number; // battery level when start() was called
285
291
  drainSinceStart: number; // total % dropped since start() (whole device)
286
292
 
287
- updateCount: number; // location fixes received this session
293
+ updateCount: number; // total location received this session
288
294
  trackingElapsedSeconds: number; // seconds since start() was called
289
295
  gpsActiveSeconds: number; // seconds the GPS chip was actively running
290
- updatesPerMinute: number; // average location fixes per minute
296
+ updatesPerMinute: number; // average total location per minute
291
297
  drainRatePerHour: number; // battery drain rate in %/hr (whole device)
292
298
  }
293
299
  ```
@@ -312,39 +318,38 @@ Set `debug: true` in `configure()` to enable debug features:
312
318
  - **iOS** — forces the blue location arrow in the status bar while tracking is active
313
319
  - **Android** — notification title changes to `[DEBUG] <title>` so you can confirm the foreground service is running
314
320
  - **Both** — verbose native logging via `console.log` / `Logcat`
315
- - **Both** — a floating debug panel appears automatically showing live metrics and battery saving suggestions
321
+ - **Both** — a floating debug panel shows live metrics and battery saving suggestions; add `<GeoDebugOverlay />` once to your component tree and it self-manages visibility
316
322
 
317
- ### Setup (one-time)
323
+ ### Setup
318
324
 
319
- Add one import to the **top** of your app's `index.js`, before `AppRegistry.registerComponent`. This registers the overlay host so the panel can mount itself automatically when `debug: true` no component needed anywhere in the app.
325
+ Add `<GeoDebugOverlay />` once to your component tree, co-located with wherever you call `RNGeoService.start()`. It self-manages visibility it only shows when `debug: true` is set in `configure()` and tracking is active.
320
326
 
321
- ```ts
322
- // index.js
323
- import 'react-native-gesture-handler';
324
- import '@tsachit/react-native-geo-service/debug-panel'; // add this once
325
- import { AppRegistry } from 'react-native';
326
- import App from './App';
327
- import { name as appName } from './app.json';
328
-
329
- AppRegistry.registerComponent(appName, () => App);
327
+ ```tsx
328
+ import { GeoDebugOverlay } from '@tsachit/react-native-geo-service';
329
+
330
+ // Render it alongside your navigation root or wherever tracking is used:
331
+ return (
332
+ <>
333
+ <YourNavigator />
334
+ <GeoDebugOverlay />
335
+ </>
336
+ );
330
337
  ```
331
338
 
332
- > **Why must it be in `index.js`?** React Native's `AppRegistry.setWrapperComponentProvider` must be called before `registerComponent` — the same reason `react-native-gesture-handler` must also be imported there. Placing it anywhere else (e.g. inside a hook or screen) is too late; the app root has already mounted.
333
-
334
- ### Auto-mount debug panel
335
-
336
- Once the setup import is in place, the panel mounts and unmounts automatically:
339
+ Then set `debug: true` in your config:
337
340
 
338
341
  ```ts
339
- // Panel appears automatically when tracking starts
340
342
  await RNGeoService.configure({ debug: true, ... });
341
- await RNGeoService.start(); // panel is now visible
343
+ await RNGeoService.start(); // panel becomes visible automatically
342
344
 
343
- // Panel is removed when tracking stops
344
- await RNGeoService.stop();
345
+ await RNGeoService.stop(); // panel hides automatically
345
346
  ```
346
347
 
347
- No `<GeoDebugPanel />` or `<GeoDebugOverlay />` needed anywhere in the component tree.
348
+ > **Note:** `GeoDebugOverlay` is a standard React component — it renders nothing in production when `debug: false`. It is safe to leave in the tree at all times.
349
+
350
+ | Minimized | Opened |
351
+ |--------|-------------|
352
+ | <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" /> |
348
353
 
349
354
  ### Debug panel behaviour
350
355
 
@@ -353,17 +358,19 @@ The panel is a **draggable, minimizable floating overlay** that starts minimized
353
358
  - **Tap the 📍 circle** to expand
354
359
  - **Drag** by holding the striped header bar
355
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
356
362
 
357
- **Metrics shown:**
363
+ **Metrics shown** (all values are cumulative across app restarts — see [GeoSessionStore](#geosessionstore)):
358
364
 
359
365
  | Metric | Description |
360
366
  |--------|-------------|
361
- | Tracking for | How long the current session has been running |
362
- | Updates | Total location fixes 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 |
363
370
  | Updates/min | Average frequency of location updates |
364
- | 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 |
365
372
  | Battery now | Current device battery level |
366
- | Drained | Total device battery % dropped since `start()` |
373
+ | Drained | Total device battery % dropped since first `start()` |
367
374
  | Drain rate | Battery consumed per hour (total device, not just location) |
368
375
 
369
376
  **Smart suggestions** are shown automatically:
@@ -374,22 +381,39 @@ The panel is a **draggable, minimizable floating overlay** that starts minimized
374
381
  - 🔴 Drain rate > 8%/hr → try `'balanced'` accuracy or longer update intervals
375
382
  - ✅ All metrics in range → confirms settings are efficient
376
383
 
377
- > **Note:** Battery drain is measured at the whole-device level since iOS and Android do not expose per-app battery consumption via public APIs. Use GPS active % and updates/min as the primary indicators of how much the package itself is contributing.
384
+ > **Note:** Battery drain is measured at the whole-device level since iOS and Android do not expose per-app battery consumption via public APIs. Use GPS active % and updates/min as the primary indicators of how much this package contributes.
378
385
 
379
- ### Manual usage (optional)
386
+ ### Manual panel (optional)
380
387
 
381
- If you prefer to control rendering yourself, `GeoDebugPanel` and `GeoDebugOverlay` are also exported for direct use — the `debug-panel` setup import is still required for them to render correctly.
388
+ For a custom poll interval or always-visible panel, use `GeoDebugPanel` directly:
382
389
 
383
390
  ```tsx
384
- import { GeoDebugPanel, GeoDebugOverlay } from '@tsachit/react-native-geo-service';
385
-
386
- // Renders anywhere in your tree — self-hides when tracking is inactive:
387
- <GeoDebugOverlay />
391
+ import { GeoDebugPanel } from '@tsachit/react-native-geo-service';
388
392
 
389
- // Always-visible panel with custom poll interval:
390
393
  <GeoDebugPanel pollInterval={15000} />
391
394
  ```
392
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
+
393
417
  ---
394
418
 
395
419
  ## Headless mode explained
@@ -419,7 +443,7 @@ Upon relaunch, the module detects `UIApplicationLaunchOptionsLocationKey`, resto
419
443
  - On iOS, use `coarseTracking: true` if ~500m granularity is acceptable — uses cell towers only
420
444
  - On Android, increase `updateIntervalMs` (e.g. `10000`) to give FusedLocationProvider room to batch fixes
421
445
  - Set `motionActivity: 'automotiveNavigation'` or `'fitness'` so iOS applies activity-specific optimisations
422
- - Use the `GeoDebugPanel` to measure real-world impact and act on its suggestions
446
+ - Use the debug overlay (`debug: true`) to measure real-world impact and act on its suggestions
423
447
 
424
448
  ---
425
449
 
@@ -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,8 +290,9 @@ 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"/>
235
- <MetricBox label="Updates" value={`${updates}`} desc="location fixes received"/>
293
+ <MetricBox label="Started" value={startedAt} desc="first session began"/>
294
+ <MetricBox label="Tracking for" value={elapsed > 0 ? formatElapsed(elapsed) : '—'} desc="cumulative duration"/>
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"/>
238
298
  <MetricBox label="Battery now" value={formatBattery(level)} desc={info.isCharging ? '⚡ charging' : 'not charging'}/>
@@ -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
@@ -4,9 +4,6 @@ export declare function _isDebugMode(): boolean;
4
4
  /**
5
5
  * Configure the geo service. Call this before start().
6
6
  * Safe to call multiple times; subsequent calls update the config.
7
- *
8
- * When debug: true, a draggable debug overlay is mounted automatically —
9
- * no component needs to be added to the app.
10
7
  */
11
8
  declare function configure(config: GeoServiceConfig): Promise<void>;
12
9
  /**
@@ -89,3 +86,5 @@ declare const RNGeoService: {
89
86
  export default RNGeoService;
90
87
  export { GeoDebugPanel } from './GeoDebugPanel';
91
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);
@@ -61,28 +61,13 @@ function _isDebugMode() { return _debugMode; }
61
61
  /**
62
62
  * Configure the geo service. Call this before start().
63
63
  * Safe to call multiple times; subsequent calls update the config.
64
- *
65
- * When debug: true, a draggable debug overlay is mounted automatically —
66
- * no component needs to be added to the app.
67
64
  */
68
65
  function configure(config) {
69
66
  return __awaiter(this, void 0, void 0, function* () {
70
67
  var _a;
71
- const wasDebug = _debugMode;
72
68
  _debugMode = (_a = config.debug) !== null && _a !== void 0 ? _a : false;
73
69
  const merged = Object.assign(Object.assign({}, DEFAULT_CONFIG), config);
74
- const result = nativeModule.configure(merged);
75
- // Lazy require to avoid circular dependency at module init time.
76
- // autoDebug → GeoDebugOverlay → GeoDebugPanel → index (all lazy references).
77
- if (_debugMode && !wasDebug) {
78
- // eslint-disable-next-line @typescript-eslint/no-var-requires
79
- require('./autoDebug').mountDebugOverlay();
80
- }
81
- else if (!_debugMode && wasDebug) {
82
- // eslint-disable-next-line @typescript-eslint/no-var-requires
83
- require('./autoDebug').unmountDebugOverlay();
84
- }
85
- return result;
70
+ return nativeModule.configure(merged);
86
71
  });
87
72
  }
88
73
  /**
@@ -92,12 +77,7 @@ function configure(config) {
92
77
  */
93
78
  function start() {
94
79
  return __awaiter(this, void 0, void 0, function* () {
95
- const result = nativeModule.start();
96
- if (_debugMode) {
97
- // eslint-disable-next-line @typescript-eslint/no-var-requires
98
- require('./autoDebug').mountDebugOverlay();
99
- }
100
- return result;
80
+ return nativeModule.start();
101
81
  });
102
82
  }
103
83
  /**
@@ -105,12 +85,7 @@ function start() {
105
85
  */
106
86
  function stop() {
107
87
  return __awaiter(this, void 0, void 0, function* () {
108
- const result = nativeModule.stop();
109
- if (_debugMode) {
110
- // eslint-disable-next-line @typescript-eslint/no-var-requires
111
- require('./autoDebug').unmountDebugOverlay();
112
- }
113
- return result;
88
+ return nativeModule.stop();
114
89
  });
115
90
  }
116
91
  /**
@@ -173,7 +148,12 @@ function registerHeadlessTask(handler) {
173
148
  if (react_native_1.Platform.OS !== 'android')
174
149
  return;
175
150
  const taskName = DEFAULT_CONFIG.backgroundTaskName;
176
- 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
+ }));
177
157
  }
178
158
  /**
179
159
  * Returns battery information including current level and drain since tracking started.
@@ -210,3 +190,5 @@ var GeoDebugPanel_1 = require("./GeoDebugPanel");
210
190
  Object.defineProperty(exports, "GeoDebugPanel", { enumerable: true, get: function () { return GeoDebugPanel_1.GeoDebugPanel; } });
211
191
  var GeoDebugOverlay_1 = require("./GeoDebugOverlay");
212
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/lib/types.d.ts CHANGED
@@ -146,7 +146,7 @@ export interface BatteryInfo {
146
146
  levelAtStart: number;
147
147
  /** Percentage points drained since tracking started */
148
148
  drainSinceStart: number;
149
- /** How many location fixes have been delivered since start() */
149
+ /** How many total location have been delivered since start() */
150
150
  updateCount: number;
151
151
  /** Total seconds since start() was called */
152
152
  trackingElapsedSeconds: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsachit/react-native-geo-service",
3
- "version": "1.0.2",
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",
@@ -8,9 +8,7 @@
8
8
  "lib",
9
9
  "android",
10
10
  "ios",
11
- "react-native-geo-service.podspec",
12
- "debug-panel.js",
13
- "debug-panel.d.ts"
11
+ "react-native-geo-service.podspec"
14
12
  ],
15
13
  "scripts": {
16
14
  "build": "tsc",
@@ -27,7 +25,13 @@
27
25
  "author": "Sachit Karki",
28
26
  "license": "MIT",
29
27
  "peerDependencies": {
30
- "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
+ }
31
35
  },
32
36
  "devDependencies": {
33
37
  "@types/react": "^18.3.28",
@@ -41,8 +45,5 @@
41
45
  "bugs": {
42
46
  "url": "https://github.com/tsachit/react-native-geo-service/issues"
43
47
  },
44
- "homepage": "https://github.com/tsachit/react-native-geo-service#readme",
45
- "dependencies": {
46
- "react-native-root-siblings": "^5.0.1"
47
- }
48
+ "homepage": "https://github.com/tsachit/react-native-geo-service#readme"
48
49
  }
package/debug-panel.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};
package/debug-panel.js DELETED
@@ -1 +0,0 @@
1
- require('./lib/setup');
@@ -1,2 +0,0 @@
1
- export declare function mountDebugOverlay(): void;
2
- export declare function unmountDebugOverlay(): void;
package/lib/autoDebug.js DELETED
@@ -1,22 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.mountDebugOverlay = mountDebugOverlay;
7
- exports.unmountDebugOverlay = unmountDebugOverlay;
8
- const react_1 = __importDefault(require("react"));
9
- const react_native_root_siblings_1 = __importDefault(require("react-native-root-siblings"));
10
- const GeoDebugOverlay_1 = require("./GeoDebugOverlay");
11
- let sibling = null;
12
- function mountDebugOverlay() {
13
- if (!sibling) {
14
- sibling = new react_native_root_siblings_1.default(react_1.default.createElement(GeoDebugOverlay_1.GeoDebugOverlay));
15
- }
16
- }
17
- function unmountDebugOverlay() {
18
- if (sibling) {
19
- sibling.destroy();
20
- sibling = null;
21
- }
22
- }
package/lib/setup.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};
package/lib/setup.js DELETED
@@ -1,17 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- /**
4
- * Import this file ONCE at the top of your app's index.js, before
5
- * AppRegistry.registerComponent. This registers RootSiblingParent as
6
- * the app wrapper so GeoService can render the debug overlay automatically
7
- * when configure({ debug: true }) is called — no component needed in the tree.
8
- *
9
- * index.js:
10
- * import '@tsachit/react-native-geo-service/setup'; // ← add this line
11
- * import { AppRegistry } from 'react-native';
12
- * import App from './App';
13
- * AppRegistry.registerComponent('MyApp', () => App);
14
- */
15
- const react_native_1 = require("react-native");
16
- const react_native_root_siblings_1 = require("react-native-root-siblings");
17
- react_native_1.AppRegistry.setWrapperComponentProvider(() => react_native_root_siblings_1.RootSiblingParent);