react-native-nitro-geolocation 1.3.3 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -112,12 +112,15 @@ for the full compatibility matrix and option notes.
112
112
  ### 3. Background API
113
113
 
114
114
  Native background tracking, geofencing, activity events, Android Headless JS,
115
- HTTP sync, and stored event recovery should use the explicit background subpath.
115
+ HTTP sync, stored event recovery, and silent-delivery diagnosis should use the
116
+ explicit background subpath.
116
117
 
117
118
  Background location is native-only. Browser builds expose unsupported stubs so
118
119
  web bundles can still import shared code safely. Start with the
119
120
  [Background Location guide](https://react-native-nitro-geolocation.pages.dev/background/overview)
120
121
  for permissions, start/stop, geofencing, storage recovery, and native sync.
122
+ Use `diagnoseBackgroundLocation()` from the same subpath to turn the raw
123
+ background status into actionable issues when delivery is silent.
121
124
 
122
125
  ---
123
126
 
@@ -218,7 +221,7 @@ Use the docs site for the detailed flows:
218
221
  - [Quick Start](https://react-native-nitro-geolocation.pages.dev/guide/quick-start) - install, set native permissions, and read your first location.
219
222
  - [Modern API](https://react-native-nitro-geolocation.pages.dev/guide/modern-api) - accuracy presets, watches, Android settings, cached reads, geocoding, heading, and iOS accuracy authorization.
220
223
  - [Compat API](https://react-native-nitro-geolocation.pages.dev/guide/compat-api) - callback compatibility and web behavior.
221
- - [Background Location](https://react-native-nitro-geolocation.pages.dev/background/overview) - native background tracking, geofencing, storage recovery, Headless JS, and HTTP sync.
224
+ - [Background Location](https://react-native-nitro-geolocation.pages.dev/background/overview) - native background tracking, geofencing, storage recovery, Headless JS, HTTP sync, and delivery diagnosis.
222
225
  - [Migration Assistance](https://react-native-nitro-geolocation.pages.dev/guide/migration-assistance) - choose the community or service migration path.
223
226
  - [Expo Development Builds](https://react-native-nitro-geolocation.pages.dev/guide/expo-development-build) - use the package in Expo custom native builds.
224
227
  - [DevTools Plugin](https://react-native-nitro-geolocation.pages.dev/guide/devtools) - mock locations during development.
@@ -50,3 +50,12 @@ internal fun resolveMaxStored(configured: Int?, default: Int): Int {
50
50
  else -> configured
51
51
  }
52
52
  }
53
+
54
+ /**
55
+ * Headless JS is the fallback path when no in-process JS listener received the native event. If a
56
+ * live listener already handled the event, starting Headless JS duplicates delivery and React
57
+ * Native warns when the app did not register NitroBackgroundLocationTask.
58
+ */
59
+ internal fun shouldDispatchHeadlessTask(deliveredToInProcessListener: Boolean): Boolean {
60
+ return !deliveredToInProcessListener
61
+ }
@@ -45,22 +45,27 @@ class NitroBackgroundEventHub {
45
45
  errorListeners.remove(token)
46
46
  }
47
47
 
48
- fun emit(event: BackgroundEventEnvelope) {
48
+ fun emit(event: BackgroundEventEnvelope): Boolean {
49
+ var delivered = eventListeners.isNotEmpty()
49
50
  eventListeners.values.forEach { listener -> dispatch { listener(event) } }
50
51
 
51
52
  when (event.type) {
52
53
  BackgroundEventType.LOCATION -> {
53
54
  event.location?.let { location ->
55
+ delivered = delivered || locationListeners.isNotEmpty()
54
56
  locationListeners.values.forEach { listener -> dispatch { listener(location) } }
55
57
  }
56
58
  }
57
59
  BackgroundEventType.ERROR -> {
58
60
  event.error?.let { error ->
61
+ delivered = delivered || errorListeners.isNotEmpty()
59
62
  errorListeners.values.forEach { listener -> dispatch { listener(error) } }
60
63
  }
61
64
  }
62
65
  else -> Unit
63
66
  }
67
+
68
+ return delivered
64
69
  }
65
70
 
66
71
  // Listeners run inline on the caller's thread (often the broadcast receiver thread). Isolate
@@ -604,7 +604,8 @@ class NitroBackgroundLocationController private constructor(
604
604
  }
605
605
 
606
606
  private fun dispatchEvent(event: BackgroundEventEnvelope) {
607
- eventHub.emit(event)
607
+ val deliveredToInProcessListener = eventHub.emit(event)
608
+ if (!shouldDispatchHeadlessTask(deliveredToInProcessListener)) return
608
609
  // The in-process eventHub already delivered to any live JS listeners; the headless task is
609
610
  // the fallback for when JS is dead. Guard its start: startService from the background can be
610
611
  // rejected (ForegroundServiceStartNotAllowed / IllegalState), which must not crash the
@@ -64,4 +64,14 @@ class BackgroundDecisionsTest {
64
64
  assertEquals(0, resolveMaxStored(0, 10_000))
65
65
  assertEquals(0, resolveMaxStored(-5, 10_000))
66
66
  }
67
+
68
+ @Test
69
+ fun headlessTaskIsSkippedWhenInProcessListenerReceivedEvent() {
70
+ assertFalse(shouldDispatchHeadlessTask(true))
71
+ }
72
+
73
+ @Test
74
+ fun headlessTaskRunsWhenNoInProcessListenerReceivedEvent() {
75
+ assertTrue(shouldDispatchHeadlessTask(false))
76
+ }
67
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-geolocation",
3
- "version": "1.3.3",
3
+ "version": "1.4.1",
4
4
  "description": "Nitro-powered native geolocation for modern React Native apps",
5
5
  "main": "src/index",
6
6
  "source": "src/index",
@@ -8,6 +8,7 @@ import type {
8
8
  BackgroundGeofenceEventEnvelope,
9
9
  BackgroundHttpSyncEvent,
10
10
  BackgroundLocation,
11
+ BackgroundLocationStatus,
11
12
  BackgroundSubscription,
12
13
  StoredBackgroundEvent,
13
14
  StoredBackgroundEventEnvelope
@@ -65,10 +66,7 @@ export const getBackgroundConfiguration =
65
66
  NativeBackgroundLocation
66
67
  );
67
68
  export const startBackgroundLocation: NitroBackgroundLocation["startBackgroundLocation"] =
68
- (options) =>
69
- NativeBackgroundLocation.startBackgroundLocation(
70
- options ?? (undefined as any)
71
- );
69
+ (options) => NativeBackgroundLocation.startBackgroundLocation(options);
72
70
  export const stopBackgroundLocation =
73
71
  NativeBackgroundLocation.stopBackgroundLocation.bind(
74
72
  NativeBackgroundLocation
@@ -82,15 +80,9 @@ export const getBackgroundLocationStatus =
82
80
  NativeBackgroundLocation
83
81
  );
84
82
  export const getStoredBackgroundLocations: NitroBackgroundLocation["getStoredBackgroundLocations"] =
85
- (options) =>
86
- NativeBackgroundLocation.getStoredBackgroundLocations(
87
- options ?? (undefined as any)
88
- );
83
+ (options) => NativeBackgroundLocation.getStoredBackgroundLocations(options);
89
84
  export const clearStoredBackgroundLocations: NitroBackgroundLocation["clearStoredBackgroundLocations"] =
90
- (ids) =>
91
- NativeBackgroundLocation.clearStoredBackgroundLocations(
92
- ids ?? (undefined as any)
93
- );
85
+ (ids) => NativeBackgroundLocation.clearStoredBackgroundLocations(ids);
94
86
  export const markStoredBackgroundLocationsDelivered =
95
87
  NativeBackgroundLocation.markStoredBackgroundLocationsDelivered.bind(
96
88
  NativeBackgroundLocation
@@ -98,19 +90,15 @@ export const markStoredBackgroundLocationsDelivered =
98
90
  export async function getStoredBackgroundEvents(
99
91
  options?: Parameters<NitroBackgroundLocation["getStoredBackgroundEvents"]>[0]
100
92
  ): Promise<StoredBackgroundEvent[]> {
101
- const events = await NativeBackgroundLocation.getStoredBackgroundEvents(
102
- options ?? (undefined as any)
103
- );
93
+ const events =
94
+ await NativeBackgroundLocation.getStoredBackgroundEvents(options);
104
95
  return events.map((event: StoredBackgroundEventEnvelope) => ({
105
96
  ...event,
106
97
  event: narrowBackgroundEvent(event.event)
107
98
  }));
108
99
  }
109
100
  export const clearStoredBackgroundEvents: NitroBackgroundLocation["clearStoredBackgroundEvents"] =
110
- (ids) =>
111
- NativeBackgroundLocation.clearStoredBackgroundEvents(
112
- ids ?? (undefined as any)
113
- );
101
+ (ids) => NativeBackgroundLocation.clearStoredBackgroundEvents(ids);
114
102
  export const markStoredBackgroundEventsDelivered =
115
103
  NativeBackgroundLocation.markStoredBackgroundEventsDelivered.bind(
116
104
  NativeBackgroundLocation
@@ -120,17 +108,13 @@ export const addGeofences = NativeBackgroundLocation.addGeofences.bind(
120
108
  );
121
109
  export const removeGeofences: NitroBackgroundLocation["removeGeofences"] = (
122
110
  identifiers
123
- ) =>
124
- NativeBackgroundLocation.removeGeofences(identifiers ?? (undefined as any));
111
+ ) => NativeBackgroundLocation.removeGeofences(identifiers);
125
112
  export const getRegisteredGeofences =
126
113
  NativeBackgroundLocation.getRegisteredGeofences.bind(
127
114
  NativeBackgroundLocation
128
115
  );
129
116
  export const startActivityRecognition: NitroBackgroundLocation["startActivityRecognition"] =
130
- (options) =>
131
- NativeBackgroundLocation.startActivityRecognition(
132
- options ?? (undefined as any)
133
- );
117
+ (options) => NativeBackgroundLocation.startActivityRecognition(options);
134
118
  export const stopActivityRecognition =
135
119
  NativeBackgroundLocation.stopActivityRecognition.bind(
136
120
  NativeBackgroundLocation
@@ -138,6 +122,79 @@ export const stopActivityRecognition =
138
122
  export const syncStoredLocations =
139
123
  NativeBackgroundLocation.syncStoredLocations.bind(NativeBackgroundLocation);
140
124
 
125
+ export interface BackgroundLocationDiagnosis {
126
+ /** True when no blocking issues were found. */
127
+ healthy: boolean;
128
+ /** The raw status the diagnosis was derived from. */
129
+ status: BackgroundLocationStatus;
130
+ /** Human-readable, actionable reasons delivery may not be working. Empty when healthy. */
131
+ issues: string[];
132
+ }
133
+
134
+ /**
135
+ * Inspects the current background-location status and returns actionable reasons why location
136
+ * delivery may not be working. Useful when the pipeline is silent: it surfaces the common causes
137
+ * (a recorded native error, missing permissions, disabled location services, or a service that is
138
+ * configured but not yet delivering) without the caller having to interpret the raw status object.
139
+ */
140
+ export async function diagnoseBackgroundLocation(): Promise<BackgroundLocationDiagnosis> {
141
+ const status = await getBackgroundLocationStatus();
142
+ const issues: string[] = [];
143
+
144
+ if (status.lastError) {
145
+ issues.push(
146
+ `Last native error: ${status.lastError.message} (code ${status.lastError.code}).`
147
+ );
148
+ }
149
+ if (status.foregroundPermission !== "granted") {
150
+ issues.push(
151
+ `Foreground location permission is "${status.foregroundPermission}", not "granted".`
152
+ );
153
+ }
154
+ if (status.backgroundPermission !== "granted") {
155
+ issues.push(
156
+ `Background location permission is "${status.backgroundPermission}", not "granted".`
157
+ );
158
+ }
159
+ if (!status.locationServicesEnabled) {
160
+ issues.push("Device location services are turned off.");
161
+ }
162
+ if (status.isConfigured && !status.isRunning) {
163
+ issues.push(
164
+ "Tracking is configured but not running — call startBackgroundLocation()."
165
+ );
166
+ }
167
+ if (status.isRunning && status.state !== "running") {
168
+ issues.push(
169
+ `Service is started but not yet delivering (state: "${status.state}").`
170
+ );
171
+ }
172
+ if (
173
+ status.isRunning &&
174
+ status.state === "running" &&
175
+ status.storedLocationCount === 0
176
+ ) {
177
+ issues.push(
178
+ "Running with no stored locations yet — the provider may have no fix, or distanceFilter/interval is filtering updates."
179
+ );
180
+ }
181
+ if (status.android) {
182
+ if (status.isRunning && !status.android.isForegroundServiceRunning) {
183
+ issues.push("Android foreground service is not running.");
184
+ }
185
+ if (
186
+ status.android.notificationPermission &&
187
+ status.android.notificationPermission !== "granted"
188
+ ) {
189
+ issues.push(
190
+ `Android notification permission is "${status.android.notificationPermission}" — the foreground-service notification may be suppressed.`
191
+ );
192
+ }
193
+ }
194
+
195
+ return { healthy: issues.length === 0, status, issues };
196
+ }
197
+
141
198
  export function onBackgroundEvent(
142
199
  listener: (event: BackgroundEvent) => void
143
200
  ): BackgroundSubscription {