@umituz/react-native-location 1.0.16 → 1.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-location",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Device location services for React Native with GPS, permissions, caching, and reverse geocoding",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -7,6 +7,7 @@ import {
7
7
  LocationError,
8
8
  LocationErrorCode,
9
9
  } from "../../types/location.types";
10
+ import { LocationUtils } from "../utils/LocationUtils";
10
11
 
11
12
  interface CachedLocationData {
12
13
  location: LocationData;
@@ -24,42 +25,28 @@ const DEFAULT_CONFIG: Required<LocationConfig> = {
24
25
 
25
26
  export class LocationService {
26
27
  private config: Required<LocationConfig>;
27
- private storage = storageRepository;
28
28
  private inFlightRequest: Promise<LocationData> | null = null;
29
29
 
30
30
  constructor(config: LocationConfig = {}) {
31
31
  this.config = { ...DEFAULT_CONFIG, ...config };
32
32
  }
33
33
 
34
- private log(message: string, ...args: unknown[]): void {
35
- if (__DEV__) {
36
- console.log(`[LocationService] ${message}`, ...args);
37
- }
38
- }
39
-
40
- private logError(message: string, error: unknown): void {
41
- if (__DEV__) {
42
- console.error(`[LocationService] ${message}`, error);
43
- }
44
- }
45
-
46
34
  async requestPermissions(): Promise<boolean> {
47
35
  try {
48
36
  const { status: current } = await Location.getForegroundPermissionsAsync();
49
37
  if (current === "granted") return true;
50
38
 
51
- this.log("Requesting permissions...");
39
+ if (__DEV__) console.log("[LocationService] Requesting permissions...");
52
40
  const { status } = await Location.requestForegroundPermissionsAsync();
53
41
  return status === "granted";
54
42
  } catch (error) {
55
- this.logError("Error requesting permissions:", error);
43
+ if (__DEV__) console.error("[LocationService] Error requesting permissions:", error);
56
44
  return false;
57
45
  }
58
46
  }
59
47
 
60
48
  private getCacheKey(): string {
61
- const suffix = this.config.withAddress ? "_addr" : "";
62
- return `location_cache_${this.config.cacheKey}${suffix}`;
49
+ return LocationUtils.generateCacheKey(this.config.cacheKey, this.config.withAddress);
63
50
  }
64
51
 
65
52
  private async getCachedLocation(): Promise<LocationData | null> {
@@ -67,21 +54,20 @@ export class LocationService {
67
54
 
68
55
  try {
69
56
  const cacheKey = this.getCacheKey();
70
- const result = await this.storage.getItem<CachedLocationData | null>(cacheKey, null);
57
+ const result = await storageRepository.getItem<CachedLocationData | null>(cacheKey, null);
71
58
  const cached = unwrap(result, null);
72
59
 
73
60
  if (!cached) return null;
74
61
 
75
- const cacheAge = Date.now() - cached.timestamp;
76
- if (cacheAge > this.config.cacheDuration) {
77
- await this.storage.removeItem(cacheKey);
62
+ if (LocationUtils.isCacheExpired(cached.timestamp, this.config.cacheDuration)) {
63
+ await storageRepository.removeItem(cacheKey);
78
64
  return null;
79
65
  }
80
66
 
81
- this.log("Using cached location");
67
+ if (__DEV__) console.log("[LocationService] Using cached location");
82
68
  return cached.location;
83
69
  } catch (error) {
84
- this.logError("Cache read error:", error);
70
+ if (__DEV__) console.error("[LocationService] Cache read error:", error);
85
71
  return null;
86
72
  }
87
73
  }
@@ -91,9 +77,9 @@ export class LocationService {
91
77
 
92
78
  try {
93
79
  const data: CachedLocationData = { location, timestamp: Date.now() };
94
- await this.storage.setItem(this.getCacheKey(), data);
80
+ await storageRepository.setItem(this.getCacheKey(), data);
95
81
  } catch (error) {
96
- this.logError("Cache write error:", error);
82
+ if (__DEV__) console.error("[LocationService] Cache write error:", error);
97
83
  }
98
84
  }
99
85
 
@@ -142,11 +128,7 @@ export class LocationService {
142
128
  await this.cacheLocation(locationData);
143
129
  return locationData;
144
130
  } catch (error) {
145
- this.logError("Error getting location:", error);
146
-
147
- if (error instanceof Error && "code" in error) {
148
- throw error;
149
- }
131
+ if (__DEV__) console.error("[LocationService] Error getting location:", error);
150
132
 
151
133
  const message = error instanceof Error ? error.message : "Unknown error getting location";
152
134
  throw this.createError("UNKNOWN_ERROR", message);
@@ -154,7 +136,7 @@ export class LocationService {
154
136
  }
155
137
 
156
138
  private async getPositionWithTimeout(): Promise<Location.LocationObject> {
157
- let timeoutId: ReturnType<typeof setTimeout>;
139
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
158
140
 
159
141
  const locationPromise = Location.getCurrentPositionAsync({
160
142
  accuracy: this.config.accuracy,
@@ -166,9 +148,14 @@ export class LocationService {
166
148
  }, this.config.timeout);
167
149
  });
168
150
 
169
- const result = await Promise.race([locationPromise, timeoutPromise]);
170
- clearTimeout(timeoutId!);
171
- return result;
151
+ try {
152
+ const result = await Promise.race([locationPromise, timeoutPromise]);
153
+ return result;
154
+ } finally {
155
+ if (timeoutId !== null) {
156
+ clearTimeout(timeoutId);
157
+ }
158
+ }
172
159
  }
173
160
 
174
161
  async reverseGeocode(latitude: number, longitude: number): Promise<LocationAddress | undefined> {
@@ -176,17 +163,9 @@ export class LocationService {
176
163
  const [address] = await Location.reverseGeocodeAsync({ latitude, longitude });
177
164
  if (!address) return undefined;
178
165
 
179
- return {
180
- city: address.city ?? null,
181
- region: address.region ?? null,
182
- country: address.country ?? null,
183
- street: address.street ?? null,
184
- formattedAddress: [address.street, address.city, address.region, address.country]
185
- .filter(Boolean)
186
- .join(", ") || null,
187
- };
166
+ return LocationUtils.buildFormattedAddress(address);
188
167
  } catch (error) {
189
- this.logError("Reverse geocode failed:", error);
168
+ if (__DEV__) console.error("[LocationService] Reverse geocode failed:", error);
190
169
  return undefined;
191
170
  }
192
171
  }
@@ -195,7 +174,7 @@ export class LocationService {
195
174
  try {
196
175
  return await Location.hasServicesEnabledAsync();
197
176
  } catch (error) {
198
- this.logError("Error checking location enabled:", error);
177
+ if (__DEV__) console.error("[LocationService] Error checking location enabled:", error);
199
178
  return false;
200
179
  }
201
180
  }
@@ -205,7 +184,7 @@ export class LocationService {
205
184
  const { status } = await Location.getForegroundPermissionsAsync();
206
185
  return status;
207
186
  } catch (error) {
208
- this.logError("Error getting permission status:", error);
187
+ if (__DEV__) console.error("[LocationService] Error getting permission status:", error);
209
188
  return Location.PermissionStatus.UNDETERMINED;
210
189
  }
211
190
  }
@@ -223,7 +202,7 @@ export class LocationService {
223
202
  timestamp: location.timestamp,
224
203
  };
225
204
  } catch (error) {
226
- this.logError("Error getting last known position:", error);
205
+ if (__DEV__) console.error("[LocationService] Error getting last known position:", error);
227
206
  return null;
228
207
  }
229
208
  }
@@ -6,10 +6,13 @@ import {
6
6
  LocationWatcherOptions,
7
7
  } from "../../types/location.types";
8
8
 
9
- type LocationCallback = (location: LocationData) => void;
10
- type ErrorCallback = (error: LocationError) => void;
11
-
12
9
  export class LocationWatcher {
10
+ private static readonly VALID_ERROR_CODES: readonly LocationErrorCode[] = [
11
+ "PERMISSION_DENIED",
12
+ "TIMEOUT",
13
+ "UNKNOWN_ERROR",
14
+ ] as const;
15
+
13
16
  private subscription: Location.LocationSubscription | null = null;
14
17
  private options: LocationWatcherOptions;
15
18
 
@@ -17,19 +20,10 @@ export class LocationWatcher {
17
20
  this.options = options;
18
21
  }
19
22
 
20
- private log(message: string, ...args: unknown[]): void {
21
- if (__DEV__) {
22
- console.log(`[LocationWatcher] ${message}`, ...args);
23
- }
24
- }
25
-
26
- private logError(message: string, error: unknown): void {
27
- if (__DEV__) {
28
- console.error(`[LocationWatcher] ${message}`, error);
29
- }
30
- }
31
-
32
- async watchPosition(onSuccess: LocationCallback, onError?: ErrorCallback): Promise<void> {
23
+ async watchPosition(
24
+ onSuccess: (location: LocationData) => void,
25
+ onError?: (error: LocationError) => void,
26
+ ): Promise<void> {
33
27
  this.clearWatch();
34
28
 
35
29
  try {
@@ -56,15 +50,17 @@ export class LocationWatcher {
56
50
  },
57
51
  );
58
52
  } catch (error) {
59
- this.logError("Error watching position:", error);
53
+ if (__DEV__) console.error("[LocationWatcher] Error watching position:", error);
60
54
 
61
55
  let code: LocationErrorCode = "UNKNOWN_ERROR";
62
56
  let message = "Unknown error watching location";
63
57
 
64
58
  if (error instanceof Error) {
65
59
  message = error.message;
66
- if ("code" in error) {
67
- code = (error as { code: string }).code as LocationErrorCode;
60
+ if ("code" in error && typeof error.code === "string") {
61
+ if (LocationWatcher.VALID_ERROR_CODES.includes(error.code as LocationErrorCode)) {
62
+ code = error.code as LocationErrorCode;
63
+ }
68
64
  }
69
65
  }
70
66
 
@@ -74,7 +70,7 @@ export class LocationWatcher {
74
70
 
75
71
  clearWatch(): void {
76
72
  if (this.subscription) {
77
- this.log("Clearing location watch");
73
+ if (__DEV__) console.log("[LocationWatcher] Clearing location watch");
78
74
  this.subscription.remove();
79
75
  this.subscription = null;
80
76
  }
@@ -89,11 +85,11 @@ export class LocationWatcher {
89
85
  const { status: current } = await Location.getForegroundPermissionsAsync();
90
86
  if (current === "granted") return true;
91
87
 
92
- this.log("Requesting permissions...");
88
+ if (__DEV__) console.log("[LocationWatcher] Requesting permissions...");
93
89
  const { status } = await Location.requestForegroundPermissionsAsync();
94
90
  return status === "granted";
95
91
  } catch (error) {
96
- this.logError("Error requesting permissions:", error);
92
+ if (__DEV__) console.error("[LocationWatcher] Error requesting permissions:", error);
97
93
  return false;
98
94
  }
99
95
  }
@@ -1,10 +1,90 @@
1
- import { Coordinates, DistanceUnit } from "../../types/location.types";
1
+ import { Coordinates, DistanceUnit, LocationAddress } from "../../types/location.types";
2
+ import * as Location from "expo-location";
2
3
 
3
4
  export class LocationUtils {
4
5
  private static readonly EARTH_RADIUS_KM = 6371;
5
6
  private static readonly EARTH_RADIUS_MILES = 3959;
6
7
  private static readonly EARTH_RADIUS_METERS = 6371000;
7
8
 
9
+ // Accuracy thresholds in meters for classification
10
+ private static readonly ACCURACY_EXCELLENT_THRESHOLD = 10;
11
+ private static readonly ACCURACY_GOOD_THRESHOLD = 50;
12
+ private static readonly ACCURACY_FAIR_THRESHOLD = 100;
13
+ private static readonly DEFAULT_MAX_ACCURACY = 100;
14
+
15
+ /**
16
+ * Generates a cache key for location data
17
+ * @param cacheKey Base cache key
18
+ * @param withAddress Whether address is included
19
+ * @returns Formatted cache key
20
+ */
21
+ static generateCacheKey(cacheKey: string, withAddress: boolean): string {
22
+ const suffix = withAddress ? "_addr" : "";
23
+ return `location_cache_${cacheKey}${suffix}`;
24
+ }
25
+
26
+ /**
27
+ * Calculates the age of a cached location
28
+ * @param cachedTimestamp Timestamp when location was cached
29
+ * @returns Age in milliseconds
30
+ */
31
+ static getCacheAge(cachedTimestamp: number): number {
32
+ return Date.now() - cachedTimestamp;
33
+ }
34
+
35
+ /**
36
+ * Checks if cached location is expired
37
+ * @param cachedTimestamp Timestamp when location was cached
38
+ * @param cacheDuration Maximum cache duration in milliseconds
39
+ * @returns True if cache is expired
40
+ */
41
+ static isCacheExpired(cachedTimestamp: number, cacheDuration: number): boolean {
42
+ return LocationUtils.getCacheAge(cachedTimestamp) > cacheDuration;
43
+ }
44
+
45
+ /**
46
+ * Builds a formatted address from Expo Location address object
47
+ * @param address Expo Location address object (from reverseGeocodeAsync)
48
+ * @returns Formatted LocationAddress
49
+ */
50
+ static buildFormattedAddress(
51
+ address: Pick<Location.LocationGeocodedAddress, "street" | "city" | "region" | "country">
52
+ ): LocationAddress {
53
+ const addressParts = [
54
+ address.street,
55
+ address.city,
56
+ address.region,
57
+ address.country,
58
+ ].filter((part): part is string => Boolean(part));
59
+
60
+ return {
61
+ city: address.city ?? null,
62
+ region: address.region ?? null,
63
+ country: address.country ?? null,
64
+ street: address.street ?? null,
65
+ formattedAddress: addressParts.length > 0 ? addressParts.join(", ") : null,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Deep compares two objects by JSON stringification
71
+ * @param obj1 First object
72
+ * @param obj2 Second object
73
+ * @returns True if objects are deeply equal
74
+ */
75
+ static deepEqual(obj1: unknown, obj2: unknown): boolean {
76
+ return JSON.stringify(obj1) === JSON.stringify(obj2);
77
+ }
78
+
79
+ /**
80
+ * Generates a stable string reference for deep comparison
81
+ * @param obj Object to stringify
82
+ * @returns JSON string representation
83
+ */
84
+ static getStableReference(obj: unknown): string {
85
+ return JSON.stringify(obj ?? {});
86
+ }
87
+
8
88
  static calculateDistance(
9
89
  from: Coordinates,
10
90
  to: Coordinates,
@@ -36,6 +116,11 @@ export class LocationUtils {
36
116
  }
37
117
 
38
118
  static isValidCoordinate(latitude: number, longitude: number): boolean {
119
+ // Check for NaN and Infinity first
120
+ if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
121
+ return false;
122
+ }
123
+
39
124
  return (
40
125
  latitude >= -90 &&
41
126
  latitude <= 90 &&
@@ -44,10 +129,14 @@ export class LocationUtils {
44
129
  );
45
130
  }
46
131
 
47
- static isValidAccuracy(accuracy: number | null | undefined, maxAccuracy = 100): boolean {
132
+ static isValidAccuracy(accuracy: number | null | undefined, maxAccuracy = LocationUtils.DEFAULT_MAX_ACCURACY): boolean {
48
133
  if (accuracy === null || accuracy === undefined) {
49
134
  return false;
50
135
  }
136
+ // Check for NaN and Infinity
137
+ if (!Number.isFinite(accuracy)) {
138
+ return false;
139
+ }
51
140
  return accuracy > 0 && accuracy <= maxAccuracy;
52
141
  }
53
142
 
@@ -56,13 +145,13 @@ export class LocationUtils {
56
145
  return "Unknown";
57
146
  }
58
147
 
59
- if (accuracy < 10) {
148
+ if (accuracy < LocationUtils.ACCURACY_EXCELLENT_THRESHOLD) {
60
149
  return "Excellent (±" + Math.round(accuracy) + "m)";
61
150
  }
62
- if (accuracy < 50) {
151
+ if (accuracy < LocationUtils.ACCURACY_GOOD_THRESHOLD) {
63
152
  return "Good (±" + Math.round(accuracy) + "m)";
64
153
  }
65
- if (accuracy < 100) {
154
+ if (accuracy < LocationUtils.ACCURACY_FAIR_THRESHOLD) {
66
155
  return "Fair (±" + Math.round(accuracy) + "m)";
67
156
  }
68
157
  return "Poor (±" + Math.round(accuracy) + "m)";
@@ -73,6 +162,16 @@ export class LocationUtils {
73
162
  coord2: Coordinates,
74
163
  precision = 6
75
164
  ): boolean {
165
+ // Check for NaN and Infinity first
166
+ if (
167
+ !Number.isFinite(coord1.latitude) ||
168
+ !Number.isFinite(coord1.longitude) ||
169
+ !Number.isFinite(coord2.latitude) ||
170
+ !Number.isFinite(coord2.longitude)
171
+ ) {
172
+ return false;
173
+ }
174
+
76
175
  const epsilon = Math.pow(10, -precision);
77
176
  return (
78
177
  Math.abs(coord1.latitude - coord2.latitude) < epsilon &&
@@ -1,6 +1,9 @@
1
1
  import { useState, useCallback, useRef, useEffect } from "react";
2
2
  import { LocationService } from "../../infrastructure/services/LocationService";
3
3
  import { LocationData, LocationError, LocationConfig, LocationErrorCode } from "../../types/location.types";
4
+ import { LocationUtils } from "../../infrastructure/utils/LocationUtils";
5
+
6
+ const VALID_ERROR_CODES: readonly LocationErrorCode[] = ["PERMISSION_DENIED", "TIMEOUT", "UNKNOWN_ERROR"] as const;
4
7
 
5
8
  export interface UseLocationResult {
6
9
  location: LocationData | null;
@@ -10,11 +13,13 @@ export interface UseLocationResult {
10
13
  }
11
14
 
12
15
  export function useLocation(config?: LocationConfig): UseLocationResult {
13
- const configRef = useRef(config);
16
+ // Use stable reference for deep comparison of config object
17
+ const configStringRef = useRef<string>(LocationUtils.getStableReference(config));
14
18
  const serviceRef = useRef(new LocationService(config));
15
19
 
16
- if (configRef.current !== config) {
17
- configRef.current = config;
20
+ const currentConfigString = LocationUtils.getStableReference(config);
21
+ if (configStringRef.current !== currentConfigString) {
22
+ configStringRef.current = currentConfigString;
18
23
  serviceRef.current = new LocationService(config);
19
24
  }
20
25
 
@@ -41,8 +46,16 @@ export function useLocation(config?: LocationConfig): UseLocationResult {
41
46
  }
42
47
  return data;
43
48
  } catch (err) {
49
+ // Extract and validate error code inline
50
+ let code: LocationErrorCode = "UNKNOWN_ERROR";
51
+ if (err instanceof Error && "code" in err && typeof err.code === "string") {
52
+ if (VALID_ERROR_CODES.includes(err.code as LocationErrorCode)) {
53
+ code = err.code as LocationErrorCode;
54
+ }
55
+ }
56
+
44
57
  const errorObj: LocationError = {
45
- code: extractErrorCode(err),
58
+ code,
46
59
  message: err instanceof Error ? err.message : "An unknown error occurred",
47
60
  };
48
61
 
@@ -55,14 +68,7 @@ export function useLocation(config?: LocationConfig): UseLocationResult {
55
68
  setIsLoading(false);
56
69
  }
57
70
  }
58
- }, []);
71
+ }, [configStringRef]);
59
72
 
60
73
  return { location, isLoading, error, getCurrentLocation };
61
74
  }
62
-
63
- function extractErrorCode(err: unknown): LocationErrorCode {
64
- if (err instanceof Error && "code" in err && typeof (err as { code: unknown }).code === "string") {
65
- return (err as { code: string }).code as LocationErrorCode;
66
- }
67
- return "UNKNOWN_ERROR";
68
- }
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useRef, useState, useCallback } from "react";
2
2
  import { LocationWatcher } from "../../infrastructure/services/LocationWatcher";
3
3
  import { LocationData, LocationError, LocationWatcherOptions } from "../../types/location.types";
4
+ import { LocationUtils } from "../../infrastructure/utils/LocationUtils";
4
5
 
5
6
  export interface UseLocationWatchResult {
6
7
  location: LocationData | null;
@@ -11,6 +12,8 @@ export interface UseLocationWatchResult {
11
12
  }
12
13
 
13
14
  export function useLocationWatch(options?: LocationWatcherOptions): UseLocationWatchResult {
15
+ // Use stable reference for deep comparison of options object
16
+ const optionsStringRef = useRef<string>(LocationUtils.getStableReference(options));
14
17
  const watcherRef = useRef<LocationWatcher | null>(null);
15
18
  const [location, setLocation] = useState<LocationData | null>(null);
16
19
  const [error, setError] = useState<LocationError | null>(null);
@@ -37,6 +40,8 @@ export function useLocationWatch(options?: LocationWatcherOptions): UseLocationW
37
40
  setError(null);
38
41
  },
39
42
  (err) => {
43
+ // Clear watcher reference on error to maintain consistent state
44
+ watcherRef.current = null;
40
45
  setError(err);
41
46
  setIsWatching(false);
42
47
  },
@@ -45,7 +50,7 @@ export function useLocationWatch(options?: LocationWatcherOptions): UseLocationW
45
50
  if (watcher.isWatching()) {
46
51
  setIsWatching(true);
47
52
  }
48
- }, [options, stopWatching]);
53
+ }, [optionsStringRef, stopWatching]); // Use string ref for stable dependency
49
54
 
50
55
  useEffect(() => {
51
56
  return () => {