@umituz/react-native-location 1.0.17 → 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 +1 -1
- package/src/infrastructure/services/LocationService.ts +26 -47
- package/src/infrastructure/services/LocationWatcher.ts +18 -22
- package/src/infrastructure/utils/LocationUtils.ts +104 -5
- package/src/presentation/hooks/useLocation.ts +18 -12
- package/src/presentation/hooks/useLocationWatch.ts +6 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
67
|
+
if (__DEV__) console.log("[LocationService] Using cached location");
|
|
82
68
|
return cached.location;
|
|
83
69
|
} catch (error) {
|
|
84
|
-
|
|
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
|
|
80
|
+
await storageRepository.setItem(this.getCacheKey(), data);
|
|
95
81
|
} catch (error) {
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 <
|
|
148
|
+
if (accuracy < LocationUtils.ACCURACY_EXCELLENT_THRESHOLD) {
|
|
60
149
|
return "Excellent (±" + Math.round(accuracy) + "m)";
|
|
61
150
|
}
|
|
62
|
-
if (accuracy <
|
|
151
|
+
if (accuracy < LocationUtils.ACCURACY_GOOD_THRESHOLD) {
|
|
63
152
|
return "Good (±" + Math.round(accuracy) + "m)";
|
|
64
153
|
}
|
|
65
|
-
if (accuracy <
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
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
|
-
}, [
|
|
53
|
+
}, [optionsStringRef, stopWatching]); // Use string ref for stable dependency
|
|
49
54
|
|
|
50
55
|
useEffect(() => {
|
|
51
56
|
return () => {
|