@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.
- package/README.md +270 -92
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/geoservice/GeoServiceModule.kt +64 -0
- package/android/src/main/java/com/geoservice/LocationService.kt +111 -9
- package/android/src/main/java/com/geoservice/WatchdogWorker.kt +87 -0
- package/debug-panel.d.ts +1 -0
- package/debug-panel.js +1 -0
- package/ios/RNGeoService.m +223 -30
- package/lib/GeoDebugOverlay.d.ts +9 -0
- package/lib/GeoDebugOverlay.js +86 -0
- package/lib/GeoDebugPanel.d.ts +7 -0
- package/lib/GeoDebugPanel.js +319 -0
- package/lib/autoDebug.d.ts +2 -0
- package/lib/autoDebug.js +22 -0
- package/lib/index.d.ts +19 -1
- package/lib/index.js +60 -3
- package/lib/setup.d.ts +1 -0
- package/lib/setup.js +17 -0
- package/lib/types.d.ts +20 -0
- package/package.json +11 -5
package/ios/RNGeoService.m
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#import "RNGeoService.h"
|
|
2
2
|
#import <React/RCTLog.h>
|
|
3
|
+
#import <UIKit/UIKit.h>
|
|
3
4
|
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// CLActivityType helper
|
|
@@ -33,6 +34,9 @@ static CLLocationAccuracy accuracyFromString(NSString *accuracy) {
|
|
|
33
34
|
@property (nonatomic, strong) CLLocationManager *locationManager;
|
|
34
35
|
@property (nonatomic, strong) NSDictionary *config;
|
|
35
36
|
|
|
37
|
+
// Locations buffered while JS listeners are not yet attached (background relaunch)
|
|
38
|
+
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *pendingLocations;
|
|
39
|
+
|
|
36
40
|
@property (nonatomic, assign) BOOL isTracking;
|
|
37
41
|
@property (nonatomic, assign) BOOL hasListeners;
|
|
38
42
|
@property (nonatomic, assign) BOOL coarseTracking;
|
|
@@ -45,6 +49,15 @@ static CLLocationAccuracy accuracyFromString(NSString *accuracy) {
|
|
|
45
49
|
@property (nonatomic, assign) NSInteger slowReadingCount;
|
|
46
50
|
@property (nonatomic, assign) BOOL isIdle;
|
|
47
51
|
|
|
52
|
+
// Battery tracking
|
|
53
|
+
@property (nonatomic, assign) float batteryLevelAtStart;
|
|
54
|
+
|
|
55
|
+
// Session tracking metrics
|
|
56
|
+
@property (nonatomic, assign) NSInteger updateCount;
|
|
57
|
+
@property (nonatomic, strong) NSDate *trackingStartTime;
|
|
58
|
+
@property (nonatomic, assign) NSTimeInterval gpsActiveSeconds; // accumulated GPS-on time
|
|
59
|
+
@property (nonatomic, strong) NSDate *gpsActiveStart; // when current GPS-on window started
|
|
60
|
+
|
|
48
61
|
@end
|
|
49
62
|
|
|
50
63
|
@implementation RNGeoService
|
|
@@ -59,6 +72,56 @@ RCT_EXPORT_MODULE();
|
|
|
59
72
|
return dispatch_get_main_queue();
|
|
60
73
|
}
|
|
61
74
|
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Init — auto-resume tracking if app was relaunched from terminated state
|
|
77
|
+
//
|
|
78
|
+
// iOS can relaunch a terminated app when startMonitoringSignificantLocationChanges
|
|
79
|
+
// is active. When this happens, React Native creates a fresh module instance.
|
|
80
|
+
// We detect this via NSUserDefaults and immediately resume tracking so that
|
|
81
|
+
// location updates are not lost during the relaunch window.
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
- (instancetype)init {
|
|
84
|
+
if (self = [super init]) {
|
|
85
|
+
self.pendingLocations = [NSMutableArray array];
|
|
86
|
+
|
|
87
|
+
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
88
|
+
BOOL wasTracking = [defaults boolForKey:@"GeoServiceIsTracking"];
|
|
89
|
+
|
|
90
|
+
if (wasTracking) {
|
|
91
|
+
// Restore persisted config
|
|
92
|
+
NSData *configData = [defaults objectForKey:@"GeoServiceConfig"];
|
|
93
|
+
if (configData) {
|
|
94
|
+
NSDictionary *restoredConfig = [NSPropertyListSerialization
|
|
95
|
+
propertyListWithData:configData options:0 format:nil error:nil];
|
|
96
|
+
if (restoredConfig) {
|
|
97
|
+
self.config = restoredConfig;
|
|
98
|
+
self.coarseTracking = [restoredConfig[@"coarseTracking"] boolValue];
|
|
99
|
+
self.debugMode = [restoredConfig[@"debug"] boolValue];
|
|
100
|
+
self.adaptiveAccuracy = restoredConfig[@"adaptiveAccuracy"]
|
|
101
|
+
? [restoredConfig[@"adaptiveAccuracy"] boolValue] : YES;
|
|
102
|
+
self.idleSpeedThreshold = restoredConfig[@"idleSpeedThreshold"]
|
|
103
|
+
? [restoredConfig[@"idleSpeedThreshold"] floatValue] : 0.5f;
|
|
104
|
+
self.idleSampleCount = restoredConfig[@"idleSampleCount"]
|
|
105
|
+
? [restoredConfig[@"idleSampleCount"] integerValue] : 3;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
[self applyConfigToLocationManager];
|
|
110
|
+
|
|
111
|
+
// Significant changes is always running alongside standard updates —
|
|
112
|
+
// it is the only mechanism that can wake a terminated app and costs
|
|
113
|
+
// almost nothing in battery (cell towers, not GPS).
|
|
114
|
+
[self.locationManager startMonitoringSignificantLocationChanges];
|
|
115
|
+
if (!self.coarseTracking) {
|
|
116
|
+
[self.locationManager startUpdatingLocation];
|
|
117
|
+
}
|
|
118
|
+
self.isTracking = YES;
|
|
119
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Auto-resumed tracking after app relaunch");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return self;
|
|
123
|
+
}
|
|
124
|
+
|
|
62
125
|
// ---------------------------------------------------------------------------
|
|
63
126
|
// Supported events
|
|
64
127
|
// ---------------------------------------------------------------------------
|
|
@@ -66,8 +129,24 @@ RCT_EXPORT_MODULE();
|
|
|
66
129
|
return @[@"onLocation", @"onError"];
|
|
67
130
|
}
|
|
68
131
|
|
|
69
|
-
|
|
70
|
-
|
|
132
|
+
// Drain any locations that arrived before JS listeners were attached.
|
|
133
|
+
// This is the normal case during a background relaunch from terminated state:
|
|
134
|
+
// CLLocationManager fires before the React component tree has mounted.
|
|
135
|
+
- (void)startObserving {
|
|
136
|
+
self.hasListeners = YES;
|
|
137
|
+
if (self.pendingLocations.count > 0) {
|
|
138
|
+
if (self.debugMode) {
|
|
139
|
+
RCTLogInfo(@"[RNGeoService] Draining %lu buffered location(s) to JS",
|
|
140
|
+
(unsigned long)self.pendingLocations.count);
|
|
141
|
+
}
|
|
142
|
+
for (NSDictionary *loc in self.pendingLocations) {
|
|
143
|
+
[self sendEventWithName:@"onLocation" body:loc];
|
|
144
|
+
}
|
|
145
|
+
[self.pendingLocations removeAllObjects];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
- (void)stopObserving { self.hasListeners = NO; }
|
|
71
150
|
|
|
72
151
|
// ---------------------------------------------------------------------------
|
|
73
152
|
// Lazy CLLocationManager
|
|
@@ -86,7 +165,7 @@ RCT_EXPORT_MODULE();
|
|
|
86
165
|
RCT_EXPORT_METHOD(configure:(NSDictionary *)options
|
|
87
166
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
88
167
|
reject:(RCTPromiseRejectBlock)reject) {
|
|
89
|
-
self.config
|
|
168
|
+
self.config = options;
|
|
90
169
|
self.coarseTracking = [options[@"coarseTracking"] boolValue];
|
|
91
170
|
self.debugMode = [options[@"debug"] boolValue];
|
|
92
171
|
self.adaptiveAccuracy = options[@"adaptiveAccuracy"] ? [options[@"adaptiveAccuracy"] boolValue] : YES;
|
|
@@ -95,6 +174,19 @@ RCT_EXPORT_METHOD(configure:(NSDictionary *)options
|
|
|
95
174
|
self.slowReadingCount = 0;
|
|
96
175
|
self.isIdle = NO;
|
|
97
176
|
|
|
177
|
+
// Persist config so it survives app termination and can be restored on
|
|
178
|
+
// background relaunch triggered by significant location changes.
|
|
179
|
+
NSError *serializeError = nil;
|
|
180
|
+
NSData *configData = [NSPropertyListSerialization
|
|
181
|
+
dataWithPropertyList:options
|
|
182
|
+
format:NSPropertyListBinaryFormat_v1_0
|
|
183
|
+
options:0
|
|
184
|
+
error:&serializeError];
|
|
185
|
+
if (configData && !serializeError) {
|
|
186
|
+
[[NSUserDefaults standardUserDefaults] setObject:configData forKey:@"GeoServiceConfig"];
|
|
187
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
188
|
+
}
|
|
189
|
+
|
|
98
190
|
[self applyConfigToLocationManager];
|
|
99
191
|
|
|
100
192
|
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Config applied: %@", options);
|
|
@@ -117,7 +209,7 @@ RCT_EXPORT_METHOD(configure:(NSDictionary *)options
|
|
|
117
209
|
self.locationManager.pausesLocationUpdatesAutomatically = autoPause;
|
|
118
210
|
|
|
119
211
|
if (@available(iOS 11.0, *)) {
|
|
120
|
-
BOOL bgIndicator = [cfg[@"showBackgroundIndicator"] boolValue];
|
|
212
|
+
BOOL bgIndicator = self.debugMode ? YES : [cfg[@"showBackgroundIndicator"] boolValue];
|
|
121
213
|
self.locationManager.showsBackgroundLocationIndicator = bgIndicator;
|
|
122
214
|
}
|
|
123
215
|
|
|
@@ -131,30 +223,44 @@ RCT_EXPORT_METHOD(start:(RCTPromiseResolveBlock)resolve
|
|
|
131
223
|
reject:(RCTPromiseRejectBlock)reject) {
|
|
132
224
|
CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
|
|
133
225
|
|
|
226
|
+
// If permission is denied or restricted, resolve without starting.
|
|
227
|
+
// The app is responsible for requesting OS permission (via react-native-permissions)
|
|
228
|
+
// before calling start(). If denied, the didChangeAuthorizationStatus delegate
|
|
229
|
+
// will handle cleanup.
|
|
134
230
|
if (status == kCLAuthorizationStatusDenied ||
|
|
135
231
|
status == kCLAuthorizationStatusRestricted) {
|
|
136
|
-
|
|
232
|
+
resolve(nil);
|
|
137
233
|
return;
|
|
138
234
|
}
|
|
139
235
|
|
|
140
|
-
if (status == kCLAuthorizationStatusNotDetermined) {
|
|
141
|
-
[self.locationManager requestAlwaysAuthorization];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
236
|
[self applyConfigToLocationManager];
|
|
145
237
|
|
|
238
|
+
// Significant changes MUST always run alongside standard updates.
|
|
239
|
+
// It is the only iOS mechanism that can relaunch a terminated app —
|
|
240
|
+
// and it uses cell towers (not GPS), so battery cost is negligible.
|
|
241
|
+
[self.locationManager startMonitoringSignificantLocationChanges];
|
|
242
|
+
|
|
146
243
|
if (self.coarseTracking) {
|
|
147
|
-
|
|
148
|
-
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Coarse (significant-change) tracking started");
|
|
244
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Coarse (significant-change only) tracking started");
|
|
149
245
|
} else {
|
|
150
246
|
[self.locationManager startUpdatingLocation];
|
|
151
|
-
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Standard tracking started");
|
|
247
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Standard tracking started (+ significant changes for background wake)");
|
|
152
248
|
}
|
|
153
249
|
|
|
154
250
|
self.isTracking = YES;
|
|
155
251
|
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"GeoServiceIsTracking"];
|
|
156
252
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
157
253
|
|
|
254
|
+
// Record battery level at tracking start for drain calculation
|
|
255
|
+
[UIDevice currentDevice].batteryMonitoringEnabled = YES;
|
|
256
|
+
self.batteryLevelAtStart = [UIDevice currentDevice].batteryLevel;
|
|
257
|
+
|
|
258
|
+
// Reset session metrics
|
|
259
|
+
self.updateCount = 0;
|
|
260
|
+
self.gpsActiveSeconds = 0;
|
|
261
|
+
self.trackingStartTime = [NSDate date];
|
|
262
|
+
self.gpsActiveStart = [NSDate date]; // GPS starts active
|
|
263
|
+
|
|
158
264
|
resolve(nil);
|
|
159
265
|
}
|
|
160
266
|
|
|
@@ -163,14 +269,12 @@ RCT_EXPORT_METHOD(start:(RCTPromiseResolveBlock)resolve
|
|
|
163
269
|
// ---------------------------------------------------------------------------
|
|
164
270
|
RCT_EXPORT_METHOD(stop:(RCTPromiseResolveBlock)resolve
|
|
165
271
|
reject:(RCTPromiseRejectBlock)reject) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
} else {
|
|
169
|
-
[self.locationManager stopUpdatingLocation];
|
|
170
|
-
}
|
|
272
|
+
[self.locationManager stopUpdatingLocation];
|
|
273
|
+
[self.locationManager stopMonitoringSignificantLocationChanges];
|
|
171
274
|
|
|
172
275
|
self.isTracking = NO;
|
|
173
276
|
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"GeoServiceIsTracking"];
|
|
277
|
+
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"GeoServiceConfig"];
|
|
174
278
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
175
279
|
|
|
176
280
|
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Tracking stopped");
|
|
@@ -195,6 +299,55 @@ RCT_EXPORT_METHOD(getCurrentLocation:(RCTPromiseResolveBlock)resolve
|
|
|
195
299
|
}
|
|
196
300
|
}
|
|
197
301
|
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// getBatteryInfo() / setLocationIndicator()
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
RCT_EXPORT_METHOD(getBatteryInfo:(RCTPromiseResolveBlock)resolve
|
|
306
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
307
|
+
[UIDevice currentDevice].batteryMonitoringEnabled = YES;
|
|
308
|
+
float current = [UIDevice currentDevice].batteryLevel;
|
|
309
|
+
UIDeviceBatteryState state = [UIDevice currentDevice].batteryState;
|
|
310
|
+
BOOL isCharging = state == UIDeviceBatteryStateCharging || state == UIDeviceBatteryStateFull;
|
|
311
|
+
float drain = (self.batteryLevelAtStart > 0 && current > 0)
|
|
312
|
+
? (self.batteryLevelAtStart - current) * 100.0f : 0.0f;
|
|
313
|
+
|
|
314
|
+
// Elapsed session time
|
|
315
|
+
NSTimeInterval elapsed = self.trackingStartTime
|
|
316
|
+
? [[NSDate date] timeIntervalSinceDate:self.trackingStartTime] : 0;
|
|
317
|
+
|
|
318
|
+
// GPS active = accumulated time + current window (if GPS is on right now)
|
|
319
|
+
NSTimeInterval gpsActive = self.gpsActiveSeconds;
|
|
320
|
+
if (!self.isIdle && self.gpsActiveStart) {
|
|
321
|
+
gpsActive += [[NSDate date] timeIntervalSinceDate:self.gpsActiveStart];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
double updatesPerMinute = (elapsed > 0)
|
|
325
|
+
? (self.updateCount / (elapsed / 60.0)) : 0;
|
|
326
|
+
double drainRatePerHour = (elapsed > 0 && drain > 0)
|
|
327
|
+
? (drain / (elapsed / 3600.0)) : 0;
|
|
328
|
+
|
|
329
|
+
resolve(@{
|
|
330
|
+
@"level": @(current * 100.0f),
|
|
331
|
+
@"isCharging": @(isCharging),
|
|
332
|
+
@"levelAtStart": @(self.batteryLevelAtStart * 100.0f),
|
|
333
|
+
@"drainSinceStart": @(MAX(drain, 0.0f)),
|
|
334
|
+
@"updateCount": @(self.updateCount),
|
|
335
|
+
@"trackingElapsedSeconds": @(elapsed),
|
|
336
|
+
@"gpsActiveSeconds": @(gpsActive),
|
|
337
|
+
@"updatesPerMinute": @(updatesPerMinute),
|
|
338
|
+
@"drainRatePerHour": @(MAX(drainRatePerHour, 0.0))
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
RCT_EXPORT_METHOD(setLocationIndicator:(BOOL)show
|
|
343
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
344
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
345
|
+
if (@available(iOS 11.0, *)) {
|
|
346
|
+
self.locationManager.showsBackgroundLocationIndicator = show;
|
|
347
|
+
}
|
|
348
|
+
resolve(nil);
|
|
349
|
+
}
|
|
350
|
+
|
|
198
351
|
// ---------------------------------------------------------------------------
|
|
199
352
|
// CLLocationManagerDelegate
|
|
200
353
|
// ---------------------------------------------------------------------------
|
|
@@ -203,8 +356,11 @@ RCT_EXPORT_METHOD(getCurrentLocation:(RCTPromiseResolveBlock)resolve
|
|
|
203
356
|
CLLocation *location = [locations lastObject];
|
|
204
357
|
if (!location) return;
|
|
205
358
|
|
|
359
|
+
self.updateCount++;
|
|
360
|
+
|
|
206
361
|
if (self.debugMode) {
|
|
207
|
-
RCTLogInfo(@"[RNGeoService] Location: %f, %f (±%.0fm) speed=%.1fm/s",
|
|
362
|
+
RCTLogInfo(@"[RNGeoService] Location #%ld: %f, %f (±%.0fm) speed=%.1fm/s",
|
|
363
|
+
(long)self.updateCount,
|
|
208
364
|
location.coordinate.latitude,
|
|
209
365
|
location.coordinate.longitude,
|
|
210
366
|
location.horizontalAccuracy,
|
|
@@ -215,8 +371,18 @@ RCT_EXPORT_METHOD(getCurrentLocation:(RCTPromiseResolveBlock)resolve
|
|
|
215
371
|
[self evaluateMotionState:location];
|
|
216
372
|
}
|
|
217
373
|
|
|
374
|
+
NSDictionary *locationDict = [self locationToDictionary:location];
|
|
375
|
+
|
|
218
376
|
if (self.hasListeners) {
|
|
219
|
-
[self sendEventWithName:@"onLocation" body:
|
|
377
|
+
[self sendEventWithName:@"onLocation" body:locationDict];
|
|
378
|
+
} else {
|
|
379
|
+
// Buffer the location — JS listeners haven't attached yet.
|
|
380
|
+
// This is normal during background relaunch: CLLocationManager fires
|
|
381
|
+
// before the React component tree has had time to mount.
|
|
382
|
+
// Events are drained in startObserving() once a listener attaches.
|
|
383
|
+
if (self.pendingLocations.count < 10) {
|
|
384
|
+
[self.pendingLocations addObject:locationDict];
|
|
385
|
+
}
|
|
220
386
|
}
|
|
221
387
|
}
|
|
222
388
|
|
|
@@ -229,6 +395,11 @@ RCT_EXPORT_METHOD(getCurrentLocation:(RCTPromiseResolveBlock)resolve
|
|
|
229
395
|
if (!self.isIdle && self.slowReadingCount >= self.idleSampleCount) {
|
|
230
396
|
self.isIdle = YES;
|
|
231
397
|
self.slowReadingCount = 0;
|
|
398
|
+
// Accumulate GPS-on time before going idle
|
|
399
|
+
if (self.gpsActiveStart) {
|
|
400
|
+
self.gpsActiveSeconds += [[NSDate date] timeIntervalSinceDate:self.gpsActiveStart];
|
|
401
|
+
self.gpsActiveStart = nil;
|
|
402
|
+
}
|
|
232
403
|
// Reduce accuracy — CoreLocation stops requesting GPS
|
|
233
404
|
self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
|
|
234
405
|
self.locationManager.distanceFilter = 50.0;
|
|
@@ -238,6 +409,7 @@ RCT_EXPORT_METHOD(getCurrentLocation:(RCTPromiseResolveBlock)resolve
|
|
|
238
409
|
if (self.isIdle) {
|
|
239
410
|
self.isIdle = NO;
|
|
240
411
|
self.slowReadingCount = 0;
|
|
412
|
+
self.gpsActiveStart = [NSDate date]; // GPS back on
|
|
241
413
|
[self applyConfigToLocationManager];
|
|
242
414
|
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Movement detected — accuracy restored");
|
|
243
415
|
} else {
|
|
@@ -248,20 +420,21 @@ RCT_EXPORT_METHOD(getCurrentLocation:(RCTPromiseResolveBlock)resolve
|
|
|
248
420
|
|
|
249
421
|
- (void)locationManager:(CLLocationManager *)manager
|
|
250
422
|
didFailWithError:(NSError *)error {
|
|
251
|
-
// kCLErrorLocationUnknown (code 0) is transient — CoreLocation
|
|
252
|
-
// fix yet but will keep trying automatically. Silently ignore it
|
|
253
|
-
// surface a noisy error to the app before the GPS has warmed up.
|
|
423
|
+
// kCLErrorLocationUnknown (code 0) is transient — CoreLocation hasn't acquired
|
|
424
|
+
// a fix yet but will keep trying automatically. Silently ignore it.
|
|
254
425
|
if ([error.domain isEqualToString:kCLErrorDomain] && error.code == kCLErrorLocationUnknown) {
|
|
255
426
|
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Location unknown (transient) — waiting for GPS fix");
|
|
256
427
|
return;
|
|
257
428
|
}
|
|
258
429
|
|
|
259
|
-
// kCLErrorDenied
|
|
260
|
-
// error worth surfacing, and we should stop tracking to avoid repeated failures.
|
|
430
|
+
// kCLErrorDenied: user revoked permission. Stop everything and clear state.
|
|
261
431
|
if ([error.domain isEqualToString:kCLErrorDomain] && error.code == kCLErrorDenied) {
|
|
262
432
|
RCTLogWarn(@"[RNGeoService] Location permission denied — stopping tracking");
|
|
263
433
|
[self.locationManager stopUpdatingLocation];
|
|
264
434
|
[self.locationManager stopMonitoringSignificantLocationChanges];
|
|
435
|
+
self.isTracking = NO;
|
|
436
|
+
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"GeoServiceIsTracking"];
|
|
437
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
265
438
|
} else {
|
|
266
439
|
RCTLogError(@"[RNGeoService] Location error: %@", error.localizedDescription);
|
|
267
440
|
}
|
|
@@ -276,18 +449,38 @@ RCT_EXPORT_METHOD(getCurrentLocation:(RCTPromiseResolveBlock)resolve
|
|
|
276
449
|
|
|
277
450
|
- (void)locationManager:(CLLocationManager *)manager
|
|
278
451
|
didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
|
|
279
|
-
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Auth status: %d", status);
|
|
452
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Auth status changed: %d", status);
|
|
453
|
+
|
|
454
|
+
// Permission was revoked while tracking — stop and notify JS
|
|
455
|
+
if (status == kCLAuthorizationStatusDenied || status == kCLAuthorizationStatusRestricted) {
|
|
456
|
+
if (self.isTracking) {
|
|
457
|
+
[self.locationManager stopUpdatingLocation];
|
|
458
|
+
[self.locationManager stopMonitoringSignificantLocationChanges];
|
|
459
|
+
self.isTracking = NO;
|
|
460
|
+
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"GeoServiceIsTracking"];
|
|
461
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
462
|
+
if (self.hasListeners) {
|
|
463
|
+
[self sendEventWithName:@"onError" body:@{
|
|
464
|
+
@"code": @(kCLErrorDenied),
|
|
465
|
+
@"message": @"Location permission was revoked. Please re-enable in Settings."
|
|
466
|
+
}];
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
280
471
|
|
|
281
|
-
//
|
|
282
|
-
if (status == kCLAuthorizationStatusAuthorizedAlways
|
|
472
|
+
// Permission granted after background relaunch — resume if we were tracking before
|
|
473
|
+
if ((status == kCLAuthorizationStatusAuthorizedAlways ||
|
|
474
|
+
status == kCLAuthorizationStatusAuthorizedWhenInUse) &&
|
|
475
|
+
!self.isTracking &&
|
|
283
476
|
[[NSUserDefaults standardUserDefaults] boolForKey:@"GeoServiceIsTracking"]) {
|
|
284
477
|
[self applyConfigToLocationManager];
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
} else {
|
|
478
|
+
[self.locationManager startMonitoringSignificantLocationChanges];
|
|
479
|
+
if (!self.coarseTracking) {
|
|
288
480
|
[self.locationManager startUpdatingLocation];
|
|
289
481
|
}
|
|
290
482
|
self.isTracking = YES;
|
|
483
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Tracking resumed after auth grant");
|
|
291
484
|
}
|
|
292
485
|
}
|
|
293
486
|
|
|
@@ -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;
|