@tsachit/react-native-geo-service 1.0.0 → 1.0.2

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.
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.GeoDebugPanel = void 0;
49
+ const react_1 = __importStar(require("react"));
50
+ const react_native_1 = require("react-native");
51
+ const index_1 = __importDefault(require("./index"));
52
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = react_native_1.Dimensions.get('window');
53
+ const PANEL_WIDTH = Math.round(SCREEN_WIDTH * 0.95);
54
+ const PILL_SIZE = 50;
55
+ // ─── Formatting helpers ──────────────────────────────────────────────────────
56
+ function formatBattery(level) {
57
+ return level < 0 ? 'N/A' : `${level.toFixed(1)}%`;
58
+ }
59
+ function formatElapsed(seconds) {
60
+ if (seconds < 60)
61
+ return `${Math.round(seconds)}s`;
62
+ const m = Math.floor(seconds / 60);
63
+ const s = Math.round(seconds % 60);
64
+ if (m < 60)
65
+ return `${m}m ${s}s`;
66
+ const h = Math.floor(m / 60);
67
+ return `${h}h ${m % 60}m`;
68
+ }
69
+ function formatGpsPercent(gpsActiveSeconds, elapsedSeconds) {
70
+ if (elapsedSeconds <= 0)
71
+ return 'N/A';
72
+ return `${Math.round((gpsActiveSeconds / elapsedSeconds) * 100)}%`;
73
+ }
74
+ function getSuggestions(info) {
75
+ var _a, _b, _c, _d, _e;
76
+ const suggestions = [];
77
+ const elapsed = (_a = info.trackingElapsedSeconds) !== null && _a !== void 0 ? _a : 0;
78
+ const updates = (_b = info.updateCount) !== null && _b !== void 0 ? _b : 0;
79
+ const upm = (_c = info.updatesPerMinute) !== null && _c !== void 0 ? _c : 0;
80
+ const gpsActive = (_d = info.gpsActiveSeconds) !== null && _d !== void 0 ? _d : 0;
81
+ const drainRate = (_e = info.drainRatePerHour) !== null && _e !== void 0 ? _e : 0;
82
+ const gpsPercent = elapsed > 0 ? (gpsActive / elapsed) * 100 : 0;
83
+ // Not enough data yet
84
+ if (updates === 0) {
85
+ suggestions.push({ emoji: '⏳', color: '#aaa', text: 'No location updates received yet — make sure tracking has started and permission is granted' });
86
+ return suggestions;
87
+ }
88
+ if (elapsed < 60) {
89
+ suggestions.push({ emoji: '⏳', color: '#aaa', text: `${Math.round(elapsed)}s into session — rate metrics need ~1 min to stabilise` });
90
+ }
91
+ // ── Frequency ──
92
+ if (upm > 20) {
93
+ suggestions.push({ emoji: '🔴', color: '#f55', text: `Very frequent updates (${upm.toFixed(1)}/min) — increase minDistanceMeters or updateIntervalMs to reduce battery drain` });
94
+ }
95
+ else if (upm > 8) {
96
+ suggestions.push({ emoji: '⚠️', color: '#fa0', text: `Frequent updates (${upm.toFixed(1)}/min) — consider increasing minDistanceMeters` });
97
+ }
98
+ else if (upm > 0) {
99
+ suggestions.push({ emoji: '✅', color: '#4c4', text: `Update frequency is good (${upm.toFixed(1)}/min)` });
100
+ }
101
+ // ── GPS active time (only meaningful after 60s) ──
102
+ if (elapsed >= 60) {
103
+ if (gpsPercent > 80) {
104
+ suggestions.push({ emoji: '🔴', color: '#f55', text: `GPS on ${Math.round(gpsPercent)}% of the time — enable adaptiveAccuracy or switch to coarseTracking to save battery` });
105
+ }
106
+ else if (gpsPercent > 50) {
107
+ suggestions.push({ emoji: '⚠️', color: '#fa0', text: `GPS on ${Math.round(gpsPercent)}% of time — device is mostly moving, adaptive accuracy is active` });
108
+ }
109
+ else if (gpsPercent > 0) {
110
+ suggestions.push({ emoji: '✅', color: '#4c4', text: `GPS on ${Math.round(gpsPercent)}% of time — adaptive accuracy is saving battery` });
111
+ }
112
+ }
113
+ // ── Drain rate ──
114
+ if (drainRate > 8) {
115
+ suggestions.push({ emoji: '🔴', color: '#f55', text: `High drain (${drainRate.toFixed(1)}%/hr) — try accuracy: 'balanced' or increase update intervals` });
116
+ }
117
+ else if (drainRate > 4) {
118
+ suggestions.push({ emoji: '⚠️', color: '#fa0', text: `Moderate drain (${drainRate.toFixed(1)}%/hr) — normal for active GPS, consider adaptiveAccuracy` });
119
+ }
120
+ else if (drainRate > 0) {
121
+ suggestions.push({ emoji: '✅', color: '#4c4', text: `Low drain (${drainRate.toFixed(1)}%/hr) — battery usage is efficient` });
122
+ }
123
+ return suggestions;
124
+ }
125
+ // ─── Pan responder factory ───────────────────────────────────────────────────
126
+ function makePanResponder(pan, lastPos, maxWidth, maxHeight, onTap) {
127
+ const didDrag = { current: false };
128
+ return react_native_1.PanResponder.create({
129
+ onStartShouldSetPanResponder: () => true,
130
+ onMoveShouldSetPanResponder: () => true,
131
+ onPanResponderGrant: () => {
132
+ didDrag.current = false;
133
+ pan.setOffset({ x: lastPos.current.x, y: lastPos.current.y });
134
+ pan.setValue({ x: 0, y: 0 });
135
+ },
136
+ onPanResponderMove: (_, g) => {
137
+ if (Math.abs(g.dx) > 4 || Math.abs(g.dy) > 4)
138
+ didDrag.current = true;
139
+ react_native_1.Animated.event([null, { dx: pan.x, dy: pan.y }], { useNativeDriver: false })(_, g);
140
+ },
141
+ onPanResponderRelease: (_, g) => {
142
+ pan.flattenOffset();
143
+ if (!didDrag.current && onTap) {
144
+ onTap();
145
+ return;
146
+ }
147
+ const newX = Math.max(0, Math.min(lastPos.current.x + g.dx, SCREEN_WIDTH - maxWidth));
148
+ const newY = Math.max(0, Math.min(lastPos.current.y + g.dy, SCREEN_HEIGHT - maxHeight));
149
+ lastPos.current = { x: newX, y: newY };
150
+ pan.setValue({ x: newX, y: newY });
151
+ },
152
+ });
153
+ }
154
+ // ─── Component ───────────────────────────────────────────────────────────────
155
+ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
156
+ var _a, _b, _c, _d, _e, _f, _g;
157
+ const [info, setInfo] = (0, react_1.useState)(null);
158
+ 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 });
161
+ const panelHeight = (0, react_1.useRef)(0);
162
+ const pillPanResponder = (0, react_1.useRef)(makePanResponder(pan, lastPos, PILL_SIZE, PILL_SIZE, () => {
163
+ const clamped = {
164
+ x: Math.min(lastPos.current.x, SCREEN_WIDTH - PANEL_WIDTH),
165
+ y: Math.min(lastPos.current.y, SCREEN_HEIGHT - panelHeight.current),
166
+ };
167
+ lastPos.current = clamped;
168
+ pan.setValue(clamped);
169
+ setMinimized(false);
170
+ })).current;
171
+ const headerPanResponder = (0, react_1.useRef)(react_native_1.PanResponder.create({
172
+ onStartShouldSetPanResponder: () => false,
173
+ onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 4 || Math.abs(g.dy) > 4,
174
+ onPanResponderGrant: () => {
175
+ pan.setOffset({ x: lastPos.current.x, y: lastPos.current.y });
176
+ pan.setValue({ x: 0, y: 0 });
177
+ },
178
+ onPanResponderMove: react_native_1.Animated.event([null, { dx: pan.x, dy: pan.y }], { useNativeDriver: false }),
179
+ onPanResponderRelease: (_, g) => {
180
+ pan.flattenOffset();
181
+ const newX = Math.max(0, Math.min(lastPos.current.x + g.dx, SCREEN_WIDTH - PANEL_WIDTH));
182
+ const newY = Math.max(0, Math.min(lastPos.current.y + g.dy, SCREEN_HEIGHT - panelHeight.current));
183
+ lastPos.current = { x: newX, y: newY };
184
+ pan.setValue({ x: newX, y: newY });
185
+ },
186
+ })).current;
187
+ const refresh = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
188
+ try {
189
+ const data = yield index_1.default.getBatteryInfo();
190
+ setInfo(data);
191
+ }
192
+ catch (_) { }
193
+ }), []);
194
+ (0, react_1.useEffect)(() => {
195
+ refresh();
196
+ const id = setInterval(refresh, pollInterval);
197
+ return () => clearInterval(id);
198
+ }, [refresh, pollInterval]);
199
+ // ── Minimized pill ──
200
+ if (minimized) {
201
+ return (<react_native_1.Animated.View style={[styles.pill, { transform: pan.getTranslateTransform() }]} {...pillPanResponder.panHandlers}>
202
+ <react_native_1.Text style={styles.pillText}>📍</react_native_1.Text>
203
+ </react_native_1.Animated.View>);
204
+ }
205
+ // 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;
213
+ const safeInfo = info
214
+ ? Object.assign(Object.assign({}, info), { trackingElapsedSeconds: elapsed, updateCount: updates, updatesPerMinute: upm, gpsActiveSeconds: gpsActive, drainRatePerHour: drainRate }) : null;
215
+ const suggestions = safeInfo ? getSuggestions(safeInfo) : [];
216
+ // ── Expanded panel ──
217
+ return (<react_native_1.Animated.View style={[styles.container, { transform: pan.getTranslateTransform() }]} onLayout={e => { panelHeight.current = e.nativeEvent.layout.height; }}>
218
+ {/* Drag header */}
219
+ <react_native_1.View style={styles.header} {...headerPanResponder.panHandlers}>
220
+ <react_native_1.View style={styles.dragPill}/>
221
+ <react_native_1.View style={styles.titleRow}>
222
+ <react_native_1.Text style={styles.title}>📍 GeoService Debug</react_native_1.Text>
223
+ <react_native_1.TouchableOpacity onPress={() => setMinimized(true)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
224
+ <react_native_1.Text style={styles.minimizeBtn}>⊖</react_native_1.Text>
225
+ </react_native_1.TouchableOpacity>
226
+ </react_native_1.View>
227
+ <react_native_1.Text style={styles.dragHint}>⠿ hold & drag to move</react_native_1.Text>
228
+ </react_native_1.View>
229
+
230
+ <react_native_1.View style={styles.body}>
231
+ {info ? (<>
232
+ {/* ── Metrics grid ── */}
233
+ <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"/>
236
+ <MetricBox label="Updates/min" value={upm > 0 ? upm.toFixed(1) : '—'} desc="higher = more battery"/>
237
+ <MetricBox label="GPS active" value={elapsed > 0 ? formatGpsPercent(gpsActive, elapsed) : '—'} desc="lower = adaptive saving"/>
238
+ <MetricBox label="Battery now" value={formatBattery(level)} desc={info.isCharging ? '⚡ charging' : 'not charging'}/>
239
+ <MetricBox label="Drained" value={level < 0 ? 'N/A' : `${drain.toFixed(2)}%`} desc="since tracking started"/>
240
+ <MetricBox label="Drain rate" value={drainRate > 0 ? `${drainRate.toFixed(1)}%/hr` : '—'} desc="whole device, not just GPS"/>
241
+ </react_native_1.View>
242
+
243
+ {/* ── Suggestions ── */}
244
+ <react_native_1.View style={styles.divider}/>
245
+ <react_native_1.Text style={styles.sectionLabel}>SUGGESTIONS</react_native_1.Text>
246
+ {suggestions.map((s, i) => (<react_native_1.View key={i} style={styles.suggestion}>
247
+ <react_native_1.Text style={styles.suggestionEmoji}>{s.emoji}</react_native_1.Text>
248
+ <react_native_1.Text style={[styles.suggestionText, { color: s.color }]}>{s.text}</react_native_1.Text>
249
+ </react_native_1.View>))}
250
+ </>) : (<react_native_1.Text style={styles.label}>Waiting for data…</react_native_1.Text>)}
251
+ </react_native_1.View>
252
+ </react_native_1.Animated.View>);
253
+ };
254
+ exports.GeoDebugPanel = GeoDebugPanel;
255
+ // ─── MetricBox sub-component ─────────────────────────────────────────────────
256
+ const MetricBox = ({ label, value, desc }) => (<react_native_1.View style={styles.metricBox}>
257
+ <react_native_1.Text style={styles.metricValue}>{value}</react_native_1.Text>
258
+ <react_native_1.Text style={styles.metricLabel}>{label}</react_native_1.Text>
259
+ {desc && <react_native_1.Text style={styles.metricSub}>{desc}</react_native_1.Text>}
260
+ </react_native_1.View>);
261
+ // ─── Styles ──────────────────────────────────────────────────────────────────
262
+ const styles = react_native_1.StyleSheet.create({
263
+ container: {
264
+ position: 'absolute',
265
+ width: PANEL_WIDTH,
266
+ backgroundColor: 'rgba(20,20,20,0.92)',
267
+ borderRadius: 12,
268
+ overflow: 'hidden',
269
+ borderWidth: 1,
270
+ borderColor: 'rgba(255,255,255,0.15)',
271
+ },
272
+ header: {
273
+ paddingTop: 6,
274
+ paddingBottom: 8,
275
+ paddingHorizontal: 12,
276
+ backgroundColor: 'rgba(255,255,255,0.07)',
277
+ alignItems: 'center',
278
+ },
279
+ dragPill: {
280
+ width: 40, height: 4, borderRadius: 2,
281
+ backgroundColor: 'rgba(255,255,255,0.5)',
282
+ marginBottom: 8,
283
+ },
284
+ titleRow: {
285
+ flexDirection: 'row', justifyContent: 'space-between',
286
+ alignItems: 'center', width: '100%',
287
+ },
288
+ title: { color: '#fff', fontWeight: 'bold', fontSize: 12 },
289
+ minimizeBtn: { color: '#aaa', fontSize: 18 },
290
+ dragHint: { color: 'rgba(255,255,255,0.35)', fontSize: 9, marginTop: 4 },
291
+ body: { padding: 12 },
292
+ label: { color: '#ddd', fontSize: 12 },
293
+ // Metrics grid
294
+ grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 10 },
295
+ metricBox: {
296
+ backgroundColor: 'rgba(255,255,255,0.06)',
297
+ borderRadius: 8, padding: 8,
298
+ minWidth: '30%', flex: 1,
299
+ alignItems: 'center',
300
+ },
301
+ metricValue: { color: '#fff', fontWeight: 'bold', fontSize: 15 },
302
+ metricLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 9, marginTop: 2, textAlign: 'center' },
303
+ metricSub: { color: 'rgba(255,255,255,0.3)', fontSize: 8, textAlign: 'center' },
304
+ // Suggestions
305
+ divider: { height: 1, backgroundColor: 'rgba(255,255,255,0.1)', marginBottom: 8 },
306
+ sectionLabel: { color: 'rgba(255,255,255,0.35)', fontSize: 9, letterSpacing: 1, marginBottom: 6 },
307
+ suggestion: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 5, gap: 6 },
308
+ suggestionEmoji: { fontSize: 12, lineHeight: 16 },
309
+ suggestionText: { fontSize: 11, lineHeight: 16, flex: 1 },
310
+ // Minimized pill
311
+ pill: {
312
+ position: 'absolute',
313
+ width: PILL_SIZE, height: PILL_SIZE, borderRadius: PILL_SIZE / 2,
314
+ backgroundColor: 'rgba(20,20,20,0.92)',
315
+ borderWidth: 1, borderColor: 'rgba(255,255,255,0.2)',
316
+ alignItems: 'center', justifyContent: 'center',
317
+ },
318
+ pillText: { fontSize: 24, textAlign: 'center' },
319
+ });
@@ -0,0 +1,2 @@
1
+ export declare function mountDebugOverlay(): void;
2
+ export declare function unmountDebugOverlay(): void;
@@ -0,0 +1,22 @@
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/index.d.ts CHANGED
@@ -1,8 +1,12 @@
1
- import { GeoServiceConfig, GeoSubscription, Location, LocationCallback, ErrorCallback } from './types';
1
+ import { GeoServiceConfig, GeoSubscription, Location, LocationCallback, ErrorCallback, BatteryInfo } from './types';
2
2
  export * from './types';
3
+ export declare function _isDebugMode(): boolean;
3
4
  /**
4
5
  * Configure the geo service. Call this before start().
5
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.
6
10
  */
7
11
  declare function configure(config: GeoServiceConfig): Promise<void>;
8
12
  /**
@@ -60,6 +64,16 @@ declare function onError(callback: ErrorCallback): GeoSubscription;
60
64
  * @platform android
61
65
  */
62
66
  declare function registerHeadlessTask(handler: (location: Location) => Promise<void>): void;
67
+ /**
68
+ * Returns battery information including current level and drain since tracking started.
69
+ * Only meaningful after start() has been called.
70
+ */
71
+ declare function getBatteryInfo(): Promise<BatteryInfo>;
72
+ /**
73
+ * Show or hide the status bar location indicator at runtime (iOS only).
74
+ * On Android this is a no-op — the foreground notification handles visibility.
75
+ */
76
+ declare function setLocationIndicator(show: boolean): Promise<void>;
63
77
  declare const RNGeoService: {
64
78
  configure: typeof configure;
65
79
  start: typeof start;
@@ -69,5 +83,9 @@ declare const RNGeoService: {
69
83
  onLocation: typeof onLocation;
70
84
  onError: typeof onError;
71
85
  registerHeadlessTask: typeof registerHeadlessTask;
86
+ getBatteryInfo: typeof getBatteryInfo;
87
+ setLocationIndicator: typeof setLocationIndicator;
72
88
  };
73
89
  export default RNGeoService;
90
+ export { GeoDebugPanel } from './GeoDebugPanel';
91
+ export { GeoDebugOverlay } from './GeoDebugOverlay';
package/lib/index.js CHANGED
@@ -23,6 +23,8 @@ 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;
27
+ exports._isDebugMode = _isDebugMode;
26
28
  const react_native_1 = require("react-native");
27
29
  __exportStar(require("./types"), exports);
28
30
  // The native bridge module registered as "RNGeoService" on both platforms
@@ -52,14 +54,35 @@ const DEFAULT_CONFIG = {
52
54
  idleSampleCount: 3,
53
55
  debug: false,
54
56
  };
57
+ // Tracks the debug flag set via configure() so GeoDebugOverlay can
58
+ // read it without the consuming app having to pass it as a prop.
59
+ let _debugMode = false;
60
+ function _isDebugMode() { return _debugMode; }
55
61
  /**
56
62
  * Configure the geo service. Call this before start().
57
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.
58
67
  */
59
68
  function configure(config) {
60
69
  return __awaiter(this, void 0, void 0, function* () {
70
+ var _a;
71
+ const wasDebug = _debugMode;
72
+ _debugMode = (_a = config.debug) !== null && _a !== void 0 ? _a : false;
61
73
  const merged = Object.assign(Object.assign({}, DEFAULT_CONFIG), config);
62
- return nativeModule.configure(merged);
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;
63
86
  });
64
87
  }
65
88
  /**
@@ -69,7 +92,12 @@ function configure(config) {
69
92
  */
70
93
  function start() {
71
94
  return __awaiter(this, void 0, void 0, function* () {
72
- return nativeModule.start();
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;
73
101
  });
74
102
  }
75
103
  /**
@@ -77,7 +105,12 @@ function start() {
77
105
  */
78
106
  function stop() {
79
107
  return __awaiter(this, void 0, void 0, function* () {
80
- return nativeModule.stop();
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;
81
114
  });
82
115
  }
83
116
  /**
@@ -142,6 +175,24 @@ function registerHeadlessTask(handler) {
142
175
  const taskName = DEFAULT_CONFIG.backgroundTaskName;
143
176
  react_native_1.AppRegistry.registerHeadlessTask(taskName, () => handler);
144
177
  }
178
+ /**
179
+ * Returns battery information including current level and drain since tracking started.
180
+ * Only meaningful after start() has been called.
181
+ */
182
+ function getBatteryInfo() {
183
+ return __awaiter(this, void 0, void 0, function* () {
184
+ return nativeModule.getBatteryInfo();
185
+ });
186
+ }
187
+ /**
188
+ * Show or hide the status bar location indicator at runtime (iOS only).
189
+ * On Android this is a no-op — the foreground notification handles visibility.
190
+ */
191
+ function setLocationIndicator(show) {
192
+ return __awaiter(this, void 0, void 0, function* () {
193
+ return nativeModule.setLocationIndicator(show);
194
+ });
195
+ }
145
196
  const RNGeoService = {
146
197
  configure,
147
198
  start,
@@ -151,5 +202,11 @@ const RNGeoService = {
151
202
  onLocation,
152
203
  onError,
153
204
  registerHeadlessTask,
205
+ getBatteryInfo,
206
+ setLocationIndicator,
154
207
  };
155
208
  exports.default = RNGeoService;
209
+ var GeoDebugPanel_1 = require("./GeoDebugPanel");
210
+ Object.defineProperty(exports, "GeoDebugPanel", { enumerable: true, get: function () { return GeoDebugPanel_1.GeoDebugPanel; } });
211
+ var GeoDebugOverlay_1 = require("./GeoDebugOverlay");
212
+ Object.defineProperty(exports, "GeoDebugOverlay", { enumerable: true, get: function () { return GeoDebugOverlay_1.GeoDebugOverlay; } });
package/lib/setup.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/lib/setup.js ADDED
@@ -0,0 +1,17 @@
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);
package/lib/types.d.ts CHANGED
@@ -137,3 +137,23 @@ export type ErrorCallback = (error: LocationError) => void;
137
137
  export interface GeoSubscription {
138
138
  remove(): void;
139
139
  }
140
+ export interface BatteryInfo {
141
+ /** Current battery level 0–100 */
142
+ level: number;
143
+ /** Whether the device is currently charging */
144
+ isCharging: boolean;
145
+ /** Battery level when tracking was started (0–100) */
146
+ levelAtStart: number;
147
+ /** Percentage points drained since tracking started */
148
+ drainSinceStart: number;
149
+ /** How many location fixes have been delivered since start() */
150
+ updateCount: number;
151
+ /** Total seconds since start() was called */
152
+ trackingElapsedSeconds: number;
153
+ /** Seconds the GPS chip was actively running (not in idle/low-power mode) */
154
+ gpsActiveSeconds: number;
155
+ /** Location updates per minute (averaged over entire session) */
156
+ updatesPerMinute: number;
157
+ /** Battery drain rate in percentage points per hour */
158
+ drainRatePerHour: number;
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsachit/react-native-geo-service",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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,7 +8,9 @@
8
8
  "lib",
9
9
  "android",
10
10
  "ios",
11
- "react-native-geo-service.podspec"
11
+ "react-native-geo-service.podspec",
12
+ "debug-panel.js",
13
+ "debug-panel.d.ts"
12
14
  ],
13
15
  "scripts": {
14
16
  "build": "tsc",
@@ -28,8 +30,9 @@
28
30
  "react-native": ">=0.60.0"
29
31
  },
30
32
  "devDependencies": {
31
- "@types/react-native": "^0.73.0",
32
- "typescript": "^5.0.0"
33
+ "@types/react": "^18.3.28",
34
+ "@types/react-native": "0.73.0",
35
+ "typescript": "5.9.3"
33
36
  },
34
37
  "repository": {
35
38
  "type": "git",
@@ -38,5 +41,8 @@
38
41
  "bugs": {
39
42
  "url": "https://github.com/tsachit/react-native-geo-service/issues"
40
43
  },
41
- "homepage": "https://github.com/tsachit/react-native-geo-service#readme"
44
+ "homepage": "https://github.com/tsachit/react-native-geo-service#readme",
45
+ "dependencies": {
46
+ "react-native-root-siblings": "^5.0.1"
47
+ }
42
48
  }