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

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,9 @@
1
+ import React from 'react';
2
+ /**
3
+ * Drop this anywhere in your component tree once.
4
+ * It renders the GeoDebugPanel automatically when:
5
+ * - debug: true was passed to configure()
6
+ * - location tracking is active (implies permission was granted)
7
+ * Nothing is rendered if debug is false or tracking hasn't started.
8
+ */
9
+ export declare const GeoDebugOverlay: React.FC;
@@ -0,0 +1,86 @@
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.GeoDebugOverlay = void 0;
49
+ const react_1 = __importStar(require("react"));
50
+ const index_1 = __importDefault(require("./index"));
51
+ const index_2 = require("./index");
52
+ const GeoDebugPanel_1 = require("./GeoDebugPanel");
53
+ /**
54
+ * Drop this anywhere in your component tree once.
55
+ * It renders the GeoDebugPanel automatically when:
56
+ * - debug: true was passed to configure()
57
+ * - location tracking is active (implies permission was granted)
58
+ * Nothing is rendered if debug is false or tracking hasn't started.
59
+ */
60
+ const GeoDebugOverlay = () => {
61
+ const [visible, setVisible] = (0, react_1.useState)(false);
62
+ (0, react_1.useEffect)(() => {
63
+ let cancelled = false;
64
+ const check = () => __awaiter(void 0, void 0, void 0, function* () {
65
+ try {
66
+ const tracking = yield index_1.default.isTracking();
67
+ if (!cancelled)
68
+ setVisible((0, index_2._isDebugMode)() && tracking);
69
+ }
70
+ catch (_) {
71
+ if (!cancelled)
72
+ setVisible(false);
73
+ }
74
+ });
75
+ check();
76
+ const id = setInterval(check, 3000);
77
+ return () => {
78
+ cancelled = true;
79
+ clearInterval(id);
80
+ };
81
+ }, []);
82
+ if (!visible)
83
+ return null;
84
+ return <GeoDebugPanel_1.GeoDebugPanel />;
85
+ };
86
+ exports.GeoDebugOverlay = GeoDebugOverlay;
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ interface Props {
3
+ /** Refresh interval in ms. Default: 30000 */
4
+ pollInterval?: number;
5
+ }
6
+ export declare const GeoDebugPanel: React.FC<Props>;
7
+ export {};
@@ -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="Geopoints" value={`${updates}`} desc="total locations 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
+ });
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
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.
@@ -60,6 +61,16 @@ declare function onError(callback: ErrorCallback): GeoSubscription;
60
61
  * @platform android
61
62
  */
62
63
  declare function registerHeadlessTask(handler: (location: Location) => Promise<void>): void;
64
+ /**
65
+ * Returns battery information including current level and drain since tracking started.
66
+ * Only meaningful after start() has been called.
67
+ */
68
+ declare function getBatteryInfo(): Promise<BatteryInfo>;
69
+ /**
70
+ * Show or hide the status bar location indicator at runtime (iOS only).
71
+ * On Android this is a no-op — the foreground notification handles visibility.
72
+ */
73
+ declare function setLocationIndicator(show: boolean): Promise<void>;
63
74
  declare const RNGeoService: {
64
75
  configure: typeof configure;
65
76
  start: typeof start;
@@ -69,5 +80,9 @@ declare const RNGeoService: {
69
80
  onLocation: typeof onLocation;
70
81
  onError: typeof onError;
71
82
  registerHeadlessTask: typeof registerHeadlessTask;
83
+ getBatteryInfo: typeof getBatteryInfo;
84
+ setLocationIndicator: typeof setLocationIndicator;
72
85
  };
73
86
  export default RNGeoService;
87
+ export { GeoDebugPanel } from './GeoDebugPanel';
88
+ 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,12 +54,18 @@ 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.
58
64
  */
59
65
  function configure(config) {
60
66
  return __awaiter(this, void 0, void 0, function* () {
67
+ var _a;
68
+ _debugMode = (_a = config.debug) !== null && _a !== void 0 ? _a : false;
61
69
  const merged = Object.assign(Object.assign({}, DEFAULT_CONFIG), config);
62
70
  return nativeModule.configure(merged);
63
71
  });
@@ -142,6 +150,24 @@ function registerHeadlessTask(handler) {
142
150
  const taskName = DEFAULT_CONFIG.backgroundTaskName;
143
151
  react_native_1.AppRegistry.registerHeadlessTask(taskName, () => handler);
144
152
  }
153
+ /**
154
+ * Returns battery information including current level and drain since tracking started.
155
+ * Only meaningful after start() has been called.
156
+ */
157
+ function getBatteryInfo() {
158
+ return __awaiter(this, void 0, void 0, function* () {
159
+ return nativeModule.getBatteryInfo();
160
+ });
161
+ }
162
+ /**
163
+ * Show or hide the status bar location indicator at runtime (iOS only).
164
+ * On Android this is a no-op — the foreground notification handles visibility.
165
+ */
166
+ function setLocationIndicator(show) {
167
+ return __awaiter(this, void 0, void 0, function* () {
168
+ return nativeModule.setLocationIndicator(show);
169
+ });
170
+ }
145
171
  const RNGeoService = {
146
172
  configure,
147
173
  start,
@@ -151,5 +177,11 @@ const RNGeoService = {
151
177
  onLocation,
152
178
  onError,
153
179
  registerHeadlessTask,
180
+ getBatteryInfo,
181
+ setLocationIndicator,
154
182
  };
155
183
  exports.default = RNGeoService;
184
+ var GeoDebugPanel_1 = require("./GeoDebugPanel");
185
+ Object.defineProperty(exports, "GeoDebugPanel", { enumerable: true, get: function () { return GeoDebugPanel_1.GeoDebugPanel; } });
186
+ var GeoDebugOverlay_1 = require("./GeoDebugOverlay");
187
+ Object.defineProperty(exports, "GeoDebugOverlay", { enumerable: true, get: function () { return GeoDebugOverlay_1.GeoDebugOverlay; } });
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 total location 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.1",
3
+ "version": "1.0.3",
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",
@@ -28,6 +28,7 @@
28
28
  "react-native": ">=0.60.0"
29
29
  },
30
30
  "devDependencies": {
31
+ "@types/react": "^18.3.28",
31
32
  "@types/react-native": "0.73.0",
32
33
  "typescript": "5.9.3"
33
34
  },