react-native-background-geolocation 5.2.0 → 5.4.0

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.
@@ -26,7 +26,7 @@ Pod::Spec.new do |s|
26
26
  s.source_files = 'ios/RNBackgroundGeolocation/*.{h,m,mm}'
27
27
  s.preserve_paths = 'docs', 'CHANGELOG.md', 'LICENSE', 'package.json', 'RNBackgroundGeolocation.ios.js'
28
28
 
29
- tslm_version = ENV['TSLOCATIONMANAGER_VERSION'] || '~> 4.2.0'
29
+ tslm_version = ENV['TSLOCATIONMANAGER_VERSION'] || '~> 4.4.0'
30
30
  s.dependency 'TSLocationManager', tslm_version
31
31
 
32
32
  s.libraries = 'sqlite3', 'z', 'stdc++'
@@ -9,7 +9,7 @@ def DEFAULT_TARGET_SDK_VERSION = 35
9
9
 
10
10
  // Plugin dependencies
11
11
  def DEFAULT_PLAY_SERVICES_LOCATION_VERSION = "21.3.0"
12
- def DEFAULT_TSLOCATIONMANAGER_VERSION = "4.2.+"
12
+ def DEFAULT_TSLOCATIONMANAGER_VERSION = "4.4.+"
13
13
  def DEFAULT_OK_HTTP_VERSION = "4.12.0"
14
14
  def DEFAULT_ANDROID_PERMISSIONS_VERSION = "2.1.6"
15
15
  def DEFAULT_EVENTBUS_VERSION = "3.3.1"
@@ -25,6 +25,38 @@ def safeExtGet(prop, fallback) {
25
25
  rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
26
26
  }
27
27
 
28
+ // Guards against the missing-symbol crash that occurs when an app pins an `ext.tslocationmanagerVersion`
29
+ // older than the SDK this plugin's bridge adapter was built against. Returns `minVersion` (with a
30
+ // warning) when the pinned version's entire range is below it; otherwise returns the pin unchanged.
31
+ // Dynamic pins are handled by comparing the pin's ceiling to the minimum's floor, so "4.2.+" is
32
+ // overridden while "4.+"/"+" are respected. Drop-in identical across the four plugin SDKs.
33
+ def ensureTSLocationManagerVersion(String configured, String minVersion) {
34
+ def parse = { String v, boolean asCeiling ->
35
+ def parts = (v ?: "").trim().tokenize(".")
36
+ def out = [0, 0, 0]
37
+ boolean wild = false
38
+ for (int i = 0; i < 3; i++) {
39
+ if (wild) { out[i] = asCeiling ? Integer.MAX_VALUE : 0; continue }
40
+ def p = (i < parts.size()) ? parts[i] : null
41
+ if (p != null && p.isInteger()) {
42
+ out[i] = p as int
43
+ } else if (p != null) { // "+" wildcard or qualifier
44
+ wild = true
45
+ out[i] = asCeiling ? Integer.MAX_VALUE : 0
46
+ } // missing trailing component stays 0
47
+ }
48
+ return out
49
+ }
50
+ def c = parse(configured, true) // ceiling of the app's pin
51
+ def f = parse(minVersion, false) // floor of the required minimum
52
+ def cmp = (c[0] <=> f[0]) ?: (c[1] <=> f[1]) ?: (c[2] <=> f[2])
53
+ if (cmp < 0) {
54
+ println("[tslocationmanager] ⚠️ ext.tslocationmanagerVersion '${configured}' is older than the '${minVersion}' required by this plugin release and has been overridden to '${minVersion}'. The native bridge adapter references symbols added in '${minVersion}'; an older SDK causes missing-symbol build failures / runtime crashes. Update or remove ext.tslocationmanagerVersion in your root build.gradle to silence this warning.")
55
+ return minVersion
56
+ }
57
+ return configured
58
+ }
59
+
28
60
  react {
29
61
  libraryName = "react-native-background-geolocation"
30
62
  codegenJavaPackageName = "com.transistorsoft.rnbackgroundgeolocation"
@@ -51,6 +83,11 @@ android {
51
83
  java.srcDir("$buildDir/generated/source/codegen/java")
52
84
  }
53
85
  }
86
+ testOptions {
87
+ unitTests {
88
+ includeAndroidResources = true
89
+ }
90
+ }
54
91
  }
55
92
 
56
93
  repositories{
@@ -61,7 +98,7 @@ repositories{
61
98
 
62
99
  dependencies {
63
100
  def playServicesLocationVersion = safeExtGet('playServicesLocationVersion', safeExtGet('googlePlayServicesLocationVersion', DEFAULT_PLAY_SERVICES_LOCATION_VERSION))
64
- def tslocationmanagerVersion = safeExtGet('tslocationmanagerVersion', DEFAULT_TSLOCATIONMANAGER_VERSION)
101
+ def tslocationmanagerVersion = ensureTSLocationManagerVersion(safeExtGet('tslocationmanagerVersion', DEFAULT_TSLOCATIONMANAGER_VERSION), DEFAULT_TSLOCATIONMANAGER_VERSION)
65
102
  implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
66
103
 
67
104
  def locationMajorVersion = playServicesLocationVersion.split('\\.')[0] as int
@@ -71,6 +108,10 @@ dependencies {
71
108
  api "com.transistorsoft:tslocationmanager-gms20:$tslocationmanagerVersion"
72
109
  }
73
110
 
74
- implementation "com.google.android.gms:play-services-location:$playServicesLocationVersion"
111
+ implementation "com.google.android.gms:play-services-location:$playServicesLocationVersion"
75
112
  implementation 'org.greenrobot:eventbus:3.3.1'
113
+
114
+ // Bridge-translation unit tests (JVM/Robolectric) — not shipped to consumers.
115
+ testImplementation "junit:junit:4.13.2"
116
+ testImplementation "org.robolectric:robolectric:4.14.1"
76
117
  }
@@ -43,6 +43,7 @@ import com.transistorsoft.locationmanager.adapter.BackgroundGeolocation;
43
43
  import com.transistorsoft.locationmanager.adapter.callback.*;
44
44
 
45
45
  import com.transistorsoft.locationmanager.data.LocationModel;
46
+ import com.transistorsoft.locationmanager.data.LocationQuery;
46
47
 
47
48
  import com.transistorsoft.locationmanager.data.SQLQuery;
48
49
  import com.transistorsoft.locationmanager.device.DeviceInfo;
@@ -489,8 +490,9 @@ public class RNBackgroundGeolocationModule
489
490
  }
490
491
 
491
492
  @ReactMethod
492
- public void getLocations(final Promise response) {
493
- getAdapter().getLocations(new TSGetLocationsCallback() {
493
+ public void getLocations(ReadableMap params, final Promise response) {
494
+ LocationQuery query = LocationQuery.fromMap(params != null ? params.toHashMap() : null);
495
+ getAdapter().getLocations(query, new TSGetLocationsCallback() {
494
496
  @Override public void onSuccess(List<LocationModel> records) {
495
497
  try {
496
498
  JSONArray data = new JSONArray();
@@ -519,7 +521,7 @@ public class RNBackgroundGeolocationModule
519
521
  response.resolve(uuid);
520
522
  }
521
523
  @Override public void onFailure(String error) {
522
- response.reject(error);
524
+ response.reject("insert_location_error", error);
523
525
  }
524
526
  });
525
527
  }
@@ -0,0 +1,70 @@
1
+ package com.transistorsoft.rnbackgroundgeolocation;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+ import static org.junit.Assert.assertTrue;
5
+
6
+ import com.facebook.react.bridge.JavaOnlyMap;
7
+
8
+ import org.json.JSONObject;
9
+ import org.junit.Test;
10
+ import org.junit.runner.RunWith;
11
+ import org.robolectric.RobolectricTestRunner;
12
+ import org.robolectric.annotation.Config;
13
+
14
+ /**
15
+ * Bridge-translation tests for the React Native Android module — the JS→native marshaling seam.
16
+ *
17
+ * These run on the JVM (Robolectric only for a real {@code org.json} implementation) with no device
18
+ * and no SDK: they exercise the exact {@code ReadableMap → org.json.JSONObject} conversion the real
19
+ * {@code insertLocation} path hands to the native SDK. This is the layer where the bridge-audit
20
+ * findings live (a numeric timestamp must stay a number; nested {@code coords}/{@code extras} must
21
+ * survive), and it is un-catchable by either a JS-only Jest test (mocked native) or a native SDK
22
+ * test (no bridge).
23
+ *
24
+ * The complementary half — the native {@code onSuccess(uuid) → promise.resolve(uuid)} settlement —
25
+ * is trivial glue that needs a live React runtime (RN 0.81's {@code ReactApplicationContext} is
26
+ * abstract and the module constructor initialises the SDK), so it belongs to the on-device E2E
27
+ * layer rather than a JVM unit test.
28
+ */
29
+ @RunWith(RobolectricTestRunner.class)
30
+ @Config(sdk = 34)
31
+ public class RNBackgroundGeolocationModuleTest {
32
+
33
+ /** A bridge-shaped insertLocation params map: {coords:{lat,lng}, timestamp, extras:{...}}. */
34
+ private JavaOnlyMap makeInsertParams(double timestampMillis) {
35
+ JavaOnlyMap coords = new JavaOnlyMap();
36
+ coords.putDouble("latitude", 45.5152);
37
+ coords.putDouble("longitude", -73.6104);
38
+
39
+ JavaOnlyMap extras = new JavaOnlyMap();
40
+ extras.putString("source", "import");
41
+
42
+ JavaOnlyMap params = new JavaOnlyMap();
43
+ params.putMap("coords", coords);
44
+ params.putDouble("timestamp", timestampMillis);
45
+ params.putMap("extras", extras);
46
+ return params;
47
+ }
48
+
49
+ @Test
50
+ public void mapToJson_preservesNestedCoordsAndExtras() throws Exception {
51
+ JSONObject json = RNBackgroundGeolocationModule.mapToJson(makeInsertParams(1_705_314_600_000d));
52
+
53
+ assertTrue("coords must survive as a nested object", json.has("coords"));
54
+ assertEquals(45.5152, json.getJSONObject("coords").getDouble("latitude"), 0.0);
55
+ assertEquals(-73.6104, json.getJSONObject("coords").getDouble("longitude"), 0.0);
56
+ assertEquals("import", json.getJSONObject("extras").getString("source"));
57
+ }
58
+
59
+ @Test
60
+ public void mapToJson_keepsNumericTimestampAsNumber_notString() throws Exception {
61
+ // Audit finding: a numeric epoch timestamp must reach the SDK as a NUMBER, so the SDK's
62
+ // epoch branch (TSLocation.normalizeTimestamp `raw instanceof Number`) fires — never a String.
63
+ JSONObject json = RNBackgroundGeolocationModule.mapToJson(makeInsertParams(1_705_314_600_000d));
64
+
65
+ Object ts = json.get("timestamp");
66
+ assertTrue("timestamp must be a JSON number, not a String (got " + ts.getClass().getSimpleName() + ")",
67
+ ts instanceof Number);
68
+ assertEquals(1_705_314_600_000L, ((Number) ts).longValue());
69
+ }
70
+ }
@@ -389,9 +389,10 @@ RCT_EXPORT_METHOD(stopWatchPosition:(NSInteger)watchId resolve:(RCTPromiseResolv
389
389
  resolve(@(YES));
390
390
  }
391
391
 
392
- RCT_EXPORT_METHOD(getLocations:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
392
+ RCT_EXPORT_METHOD(getLocations:(NSDictionary*)params resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
393
393
  {
394
- [locationManager getLocations:^(NSArray* records) {
394
+ LocationQuery *query = [[LocationQuery alloc] initWithDictionary:params];
395
+ [locationManager getLocations:query success:^(NSArray* records) {
395
396
  resolve(records);
396
397
  } failure:^(NSString* error) {
397
398
  reject(@"get_locations_error", error, nil);
@@ -9,38 +9,41 @@ class NativeEventEmitter {
9
9
  removeAllListeners() { listeners.clear(); }
10
10
  }
11
11
 
12
+ // The native module is a TurboModule: every data method returns a Promise (not the pre-migration
13
+ // callback convention). NativeModule.js returns these calls directly, so the mocks must resolve a
14
+ // Promise. `log`/`playSound` are fire-and-forget (void).
12
15
  const RNBackgroundGeolocation = {
13
16
  // Minimal surface your JS calls in tests:
14
- ready: (cfg, ok) => ok?.({ enabled: false }),
15
- configure: (cfg, ok) => ok?.({}),
16
- setConfig: (cfg, ok) => ok?.({}),
17
- reset: (cfg, ok) => ok?.({ enabled: false }),
18
- getState: (ok) => ok?.({ enabled: false }),
19
- beginBackgroundTask: (ok) => ok?.(1),
20
- finish: (id, ok) => ok?.(id),
21
- addGeofence: (_cfg, ok) => ok?.(),
22
- addGeofences: (_list, ok) => ok?.(),
23
- removeGeofence: (_id, ok) => ok?.(),
24
- removeGeofences: (ok) => ok?.(),
25
- getGeofences: (ok) => ok?.([]),
26
- getGeofence: (_id, ok) => ok?.(null),
27
- geofenceExists: (_id, cb) => cb(false),
28
- changePace: (_b, ok) => ok?.(),
29
- getLog: (ok) => ok?.(''),
30
- destroyLog: (ok) => ok?.(),
31
- emailLog: (_email, ok) => ok?.(),
32
- getOdometer: (ok) => ok?.(0),
33
- setOdometer: (_v, ok) => ok?.({}),
34
- getLocations: (ok) => ok?.([]),
35
- getCount: (ok) => ok?.(0),
36
- destroyLocations: (ok) => ok?.(),
37
- insertLocation: (_p, ok) => ok?.({}),
38
- sync: (ok) => ok?.({ success: true }),
39
- getProviderState: (ok) => ok?.({ enabled: true }),
40
- requestPermission: (ok) => ok?.(1),
41
- requestTemporaryFullAccuracy: (_p, ok) => ok?.(1),
17
+ ready: () => Promise.resolve({ enabled: false }),
18
+ configure: () => Promise.resolve({}),
19
+ setConfig: () => Promise.resolve({}),
20
+ reset: () => Promise.resolve({ enabled: false }),
21
+ getState: () => Promise.resolve({ enabled: false }),
22
+ beginBackgroundTask: () => Promise.resolve(1),
23
+ finish: (id) => Promise.resolve(id),
24
+ addGeofence: () => Promise.resolve(),
25
+ addGeofences: () => Promise.resolve(),
26
+ removeGeofence: () => Promise.resolve(),
27
+ removeGeofences: () => Promise.resolve(),
28
+ getGeofences: () => Promise.resolve([]),
29
+ getGeofence: () => Promise.resolve(null),
30
+ geofenceExists: () => Promise.resolve(false),
31
+ changePace: () => Promise.resolve(),
32
+ getLog: () => Promise.resolve(''),
33
+ destroyLog: () => Promise.resolve(),
34
+ emailLog: () => Promise.resolve(),
35
+ getOdometer: () => Promise.resolve(0),
36
+ setOdometer: () => Promise.resolve({}),
37
+ getLocations: () => Promise.resolve([]),
38
+ getCount: () => Promise.resolve(0),
39
+ destroyLocations: () => Promise.resolve(),
40
+ insertLocation: () => Promise.resolve('00000000-0000-0000-0000-000000000000'),
41
+ sync: () => Promise.resolve({ success: true }),
42
+ getProviderState: () => Promise.resolve({ enabled: true }),
43
+ requestPermission: () => Promise.resolve(1),
44
+ requestTemporaryFullAccuracy: () => Promise.resolve(1),
42
45
  log: () => {},
43
- getDeviceInfo: (ok) => ok?.({ model: 'mock' }),
46
+ getDeviceInfo: () => Promise.resolve({ model: 'mock' }),
44
47
  playSound: () => {},
45
48
  };
46
49
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
 
3
3
  "name": "react-native-background-geolocation",
4
- "version": "5.2.0",
4
+ "version": "5.4.0",
5
5
  "description": "The most sophisticated cross-platform background location-tracking & geofencing module with battery-conscious motion-detection intelligence",
6
6
  "scripts": {
7
7
  "build": "yarn run build:expo",
@@ -41,7 +41,7 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "@babel/runtime": "^7.26.0",
44
- "@transistorsoft/background-geolocation-types": "^5.2.0",
44
+ "@transistorsoft/background-geolocation-types": "^5.2.2",
45
45
  "tslib": "^2.6.3"
46
46
  },
47
47
  "author": "Chris Scott <chris@transistorsoft.com>",
@@ -330,8 +330,8 @@ export default class NativeModule {
330
330
  * HTTP & Persistence Methods
331
331
  */
332
332
 
333
- static getLocations() {
334
- return RNBackgroundGeolocation.getLocations();
333
+ static getLocations(query) {
334
+ return RNBackgroundGeolocation.getLocations(query || {});
335
335
  }
336
336
 
337
337
  static getCount() {
package/src/index.js CHANGED
@@ -439,8 +439,8 @@ export default class BackgroundGeolocation {
439
439
  * HTTP & Persistence
440
440
  *
441
441
  */
442
- static getLocations() {
443
- return NativeModule.getLocations();
442
+ static getLocations(query) {
443
+ return NativeModule.getLocations(query);
444
444
  }
445
445
 
446
446
  /**
@@ -21,7 +21,7 @@ export interface Spec extends TurboModule {
21
21
  setOdometer(value: number): Promise<Object>;
22
22
 
23
23
  // HTTP & DB
24
- getLocations(): Promise<Array<Object>>;
24
+ getLocations(query: Object): Promise<Array<Object>>;
25
25
  getCount(): Promise<number>;
26
26
  destroyLocations(): Promise<void>;
27
27
  sync(): Promise<Array<Object>>;
@@ -28,7 +28,7 @@ export interface Spec extends TurboModule {
28
28
  +getState: () => Promise<Object>;
29
29
 
30
30
  // Locations / persistence
31
- +getLocations: () => Promise<Array<Object>>;
31
+ +getLocations: (query: Object) => Promise<Array<Object>>;
32
32
  +getCount: () => Promise<Int32>;
33
33
  +insertLocation: (params: Object) => Promise<string>;
34
34
 
@@ -0,0 +1,51 @@
1
+ // JS-side marshaling test for insertLocation: does the JavaScript API layer forward the caller's
2
+ // location to the native bridge boundary *unchanged*, and pass the returned uuid back — with no
3
+ // real SDK and no device? The native module is a jest spy standing in for the bridge; we assert on
4
+ // what it receives (the args that crossed JS -> native) and what the caller ultimately gets back.
5
+ //
6
+ // This is the JS half of the two-halves bridge-marshaling strategy. The native half (ReadableMap ->
7
+ // JSONObject) is covered by the Android Robolectric test `RNBackgroundGeolocationModuleTest`.
8
+
9
+ const { NativeModules } = require('react-native'); // mapped to mocks/react-native.js via jest config
10
+ const BG = require('../src/index.js').default;
11
+
12
+ const native = NativeModules.RNBackgroundGeolocation;
13
+
14
+ describe('insertLocation — JS -> native bridge marshaling', () => {
15
+ afterEach(() => {
16
+ jest.restoreAllMocks();
17
+ });
18
+
19
+ test('forwards the location to the native module unchanged and resolves its uuid', async () => {
20
+ const insert = jest.spyOn(native, 'insertLocation').mockResolvedValue('uuid-abc');
21
+
22
+ const location = {
23
+ coords: { latitude: 45.5152, longitude: -73.6104 },
24
+ timestamp: 1705314600000, // numeric epoch
25
+ extras: { source: 'import' },
26
+ };
27
+
28
+ const uuid = await BG.insertLocation(location);
29
+
30
+ // The exact object reaches the native boundary — no mangling by index.js / NativeModule.
31
+ expect(insert).toHaveBeenCalledTimes(1);
32
+ expect(insert).toHaveBeenCalledWith(location);
33
+
34
+ // Type/shape preserved across the JS layers: numeric timestamp stays a number, coords/extras nested.
35
+ const [passed] = insert.mock.calls[0];
36
+ expect(typeof passed.timestamp).toBe('number');
37
+ expect(passed.coords).toEqual({ latitude: 45.5152, longitude: -73.6104 });
38
+ expect(passed.extras).toEqual({ source: 'import' });
39
+
40
+ // The uuid string flows back out through NativeModule + index.js.
41
+ expect(uuid).toBe('uuid-abc');
42
+ });
43
+
44
+ test('propagates a native rejection to the caller', async () => {
45
+ jest.spyOn(native, 'insertLocation').mockRejectedValue(new Error('insert_location_error'));
46
+
47
+ await expect(
48
+ BG.insertLocation({ coords: { latitude: 0, longitude: 0 }, timestamp: 0 })
49
+ ).rejects.toThrow('insert_location_error');
50
+ });
51
+ });