@umituz/react-native-location 1.0.26 → 1.0.28
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/application/ports/ICachePort.ts +35 -0
- package/src/application/ports/ILocationPort.ts +58 -0
- package/src/application/ports/ILoggerPort.ts +30 -0
- package/src/domain/errors/LocationErrors.ts +76 -0
- package/src/domain/services/PermissionService.ts +59 -0
- package/src/domain/value-objects/CoordinatesVO.ts +66 -0
- package/src/domain/value-objects/DistanceVO.ts +64 -0
- package/src/index.ts +33 -4
- package/src/infrastructure/repositories/LocationCache.repository.ts +68 -0
- package/src/infrastructure/services/LocationLoggerService.ts +58 -0
- package/src/infrastructure/services/LocationService.ts +98 -118
- package/src/infrastructure/services/LocationWatcherService.ts +95 -0
- package/src/infrastructure/utils/Accuracy.utils.ts +51 -0
- package/src/infrastructure/utils/ObjectComparison.utils.ts +22 -0
- package/src/presentation/hooks/useLocationHook.ts +99 -0
- package/src/presentation/hooks/useLocationWatchHook.ts +98 -0
- package/src/infrastructure/services/LocationWatcher.ts +0 -96
- package/src/infrastructure/utils/LocationUtils.ts +0 -181
- package/src/presentation/hooks/useLocation.ts +0 -74
- package/src/presentation/hooks/useLocationWatch.ts +0 -62
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import * as Location from "expo-location";
|
|
2
|
-
import {
|
|
3
|
-
LocationData,
|
|
4
|
-
LocationError,
|
|
5
|
-
LocationErrorCode,
|
|
6
|
-
LocationWatcherOptions,
|
|
7
|
-
} from "../../types/location.types";
|
|
8
|
-
|
|
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
|
-
|
|
16
|
-
private subscription: Location.LocationSubscription | null = null;
|
|
17
|
-
private options: LocationWatcherOptions;
|
|
18
|
-
|
|
19
|
-
constructor(options: LocationWatcherOptions = {}) {
|
|
20
|
-
this.options = options;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async watchPosition(
|
|
24
|
-
onSuccess: (location: LocationData) => void,
|
|
25
|
-
onError?: (error: LocationError) => void,
|
|
26
|
-
): Promise<void> {
|
|
27
|
-
this.clearWatch();
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const granted = await this.ensurePermission();
|
|
31
|
-
if (!granted) {
|
|
32
|
-
onError?.({ code: "PERMISSION_DENIED", message: "Location permission not granted" });
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
this.subscription = await Location.watchPositionAsync(
|
|
37
|
-
{
|
|
38
|
-
accuracy: this.options.accuracy ?? Location.Accuracy.Balanced,
|
|
39
|
-
distanceInterval: this.options.distanceInterval,
|
|
40
|
-
timeInterval: this.options.timeInterval,
|
|
41
|
-
},
|
|
42
|
-
(location) => {
|
|
43
|
-
onSuccess({
|
|
44
|
-
coords: {
|
|
45
|
-
latitude: location.coords.latitude,
|
|
46
|
-
longitude: location.coords.longitude,
|
|
47
|
-
},
|
|
48
|
-
timestamp: location.timestamp,
|
|
49
|
-
});
|
|
50
|
-
},
|
|
51
|
-
);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
if (__DEV__) console.error("[LocationWatcher] Error watching position:", error);
|
|
54
|
-
|
|
55
|
-
let code: LocationErrorCode = "UNKNOWN_ERROR";
|
|
56
|
-
let message = "Unknown error watching location";
|
|
57
|
-
|
|
58
|
-
if (error instanceof Error) {
|
|
59
|
-
message = error.message;
|
|
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
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
onError?.({ code, message });
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
clearWatch(): void {
|
|
72
|
-
if (this.subscription) {
|
|
73
|
-
if (__DEV__) console.log("[LocationWatcher] Clearing location watch");
|
|
74
|
-
this.subscription.remove();
|
|
75
|
-
this.subscription = null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
isWatching(): boolean {
|
|
80
|
-
return this.subscription !== null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private async ensurePermission(): Promise<boolean> {
|
|
84
|
-
try {
|
|
85
|
-
const { status: current } = await Location.getForegroundPermissionsAsync();
|
|
86
|
-
if (current === "granted") return true;
|
|
87
|
-
|
|
88
|
-
if (__DEV__) console.log("[LocationWatcher] Requesting permissions...");
|
|
89
|
-
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
90
|
-
return status === "granted";
|
|
91
|
-
} catch (error) {
|
|
92
|
-
if (__DEV__) console.error("[LocationWatcher] Error requesting permissions:", error);
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { Coordinates, DistanceUnit, LocationAddress } from "../../types/location.types";
|
|
2
|
-
import * as Location from "expo-location";
|
|
3
|
-
|
|
4
|
-
export class LocationUtils {
|
|
5
|
-
private static readonly EARTH_RADIUS_KM = 6371;
|
|
6
|
-
private static readonly EARTH_RADIUS_MILES = 3959;
|
|
7
|
-
private static readonly EARTH_RADIUS_METERS = 6371000;
|
|
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
|
-
|
|
88
|
-
static calculateDistance(
|
|
89
|
-
from: Coordinates,
|
|
90
|
-
to: Coordinates,
|
|
91
|
-
unit: DistanceUnit = "km"
|
|
92
|
-
): number {
|
|
93
|
-
const lat1Rad = (from.latitude * Math.PI) / 180;
|
|
94
|
-
const lat2Rad = (to.latitude * Math.PI) / 180;
|
|
95
|
-
const deltaLatRad = ((to.latitude - from.latitude) * Math.PI) / 180;
|
|
96
|
-
const deltaLonRad = ((to.longitude - from.longitude) * Math.PI) / 180;
|
|
97
|
-
|
|
98
|
-
const a =
|
|
99
|
-
Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) +
|
|
100
|
-
Math.cos(lat1Rad) *
|
|
101
|
-
Math.cos(lat2Rad) *
|
|
102
|
-
Math.sin(deltaLonRad / 2) *
|
|
103
|
-
Math.sin(deltaLonRad / 2);
|
|
104
|
-
|
|
105
|
-
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
106
|
-
|
|
107
|
-
switch (unit) {
|
|
108
|
-
case "miles":
|
|
109
|
-
return this.EARTH_RADIUS_MILES * c;
|
|
110
|
-
case "meters":
|
|
111
|
-
return this.EARTH_RADIUS_METERS * c;
|
|
112
|
-
case "km":
|
|
113
|
-
default:
|
|
114
|
-
return this.EARTH_RADIUS_KM * c;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
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
|
-
|
|
124
|
-
return (
|
|
125
|
-
latitude >= -90 &&
|
|
126
|
-
latitude <= 90 &&
|
|
127
|
-
longitude >= -180 &&
|
|
128
|
-
longitude <= 180
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
static isValidAccuracy(accuracy: number | null | undefined, maxAccuracy = LocationUtils.DEFAULT_MAX_ACCURACY): boolean {
|
|
133
|
-
if (accuracy === null || accuracy === undefined) {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
// Check for NaN and Infinity
|
|
137
|
-
if (!Number.isFinite(accuracy)) {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
return accuracy > 0 && accuracy <= maxAccuracy;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
static formatAccuracy(accuracy: number | null | undefined): string {
|
|
144
|
-
if (accuracy === null || accuracy === undefined) {
|
|
145
|
-
return "Unknown";
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (accuracy < LocationUtils.ACCURACY_EXCELLENT_THRESHOLD) {
|
|
149
|
-
return "Excellent (±" + Math.round(accuracy) + "m)";
|
|
150
|
-
}
|
|
151
|
-
if (accuracy < LocationUtils.ACCURACY_GOOD_THRESHOLD) {
|
|
152
|
-
return "Good (±" + Math.round(accuracy) + "m)";
|
|
153
|
-
}
|
|
154
|
-
if (accuracy < LocationUtils.ACCURACY_FAIR_THRESHOLD) {
|
|
155
|
-
return "Fair (±" + Math.round(accuracy) + "m)";
|
|
156
|
-
}
|
|
157
|
-
return "Poor (±" + Math.round(accuracy) + "m)";
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
static coordinatesAreEqual(
|
|
161
|
-
coord1: Coordinates,
|
|
162
|
-
coord2: Coordinates,
|
|
163
|
-
precision = 6
|
|
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
|
-
|
|
175
|
-
const epsilon = Math.pow(10, -precision);
|
|
176
|
-
return (
|
|
177
|
-
Math.abs(coord1.latitude - coord2.latitude) < epsilon &&
|
|
178
|
-
Math.abs(coord1.longitude - coord2.longitude) < epsilon
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
-
import { LocationService } from "../../infrastructure/services/LocationService";
|
|
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;
|
|
7
|
-
|
|
8
|
-
export interface UseLocationResult {
|
|
9
|
-
location: LocationData | null;
|
|
10
|
-
isLoading: boolean;
|
|
11
|
-
error: LocationError | null;
|
|
12
|
-
getCurrentLocation: () => Promise<LocationData | null>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function useLocation(config?: LocationConfig): UseLocationResult {
|
|
16
|
-
// Use stable reference for deep comparison of config object
|
|
17
|
-
const configStringRef = useRef<string>(LocationUtils.getStableReference(config));
|
|
18
|
-
const serviceRef = useRef(new LocationService(config));
|
|
19
|
-
|
|
20
|
-
const currentConfigString = LocationUtils.getStableReference(config);
|
|
21
|
-
if (configStringRef.current !== currentConfigString) {
|
|
22
|
-
configStringRef.current = currentConfigString;
|
|
23
|
-
serviceRef.current = new LocationService(config);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const [location, setLocation] = useState<LocationData | null>(null);
|
|
27
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
28
|
-
const [error, setError] = useState<LocationError | null>(null);
|
|
29
|
-
const mountedRef = useRef(true);
|
|
30
|
-
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
mountedRef.current = true;
|
|
33
|
-
return () => {
|
|
34
|
-
mountedRef.current = false;
|
|
35
|
-
};
|
|
36
|
-
}, []);
|
|
37
|
-
|
|
38
|
-
const getCurrentLocation = useCallback(async () => {
|
|
39
|
-
setIsLoading(true);
|
|
40
|
-
setError(null);
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const data = await serviceRef.current.getCurrentPosition();
|
|
44
|
-
if (mountedRef.current) {
|
|
45
|
-
setLocation(data);
|
|
46
|
-
}
|
|
47
|
-
return data;
|
|
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
|
-
|
|
57
|
-
const errorObj: LocationError = {
|
|
58
|
-
code,
|
|
59
|
-
message: err instanceof Error ? err.message : "An unknown error occurred",
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
if (mountedRef.current) {
|
|
63
|
-
setError(errorObj);
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
} finally {
|
|
67
|
-
if (mountedRef.current) {
|
|
68
|
-
setIsLoading(false);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}, [configStringRef]);
|
|
72
|
-
|
|
73
|
-
return { location, isLoading, error, getCurrentLocation };
|
|
74
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, useCallback } from "react";
|
|
2
|
-
import { LocationWatcher } from "../../infrastructure/services/LocationWatcher";
|
|
3
|
-
import { LocationData, LocationError, LocationWatcherOptions } from "../../types/location.types";
|
|
4
|
-
import { LocationUtils } from "../../infrastructure/utils/LocationUtils";
|
|
5
|
-
|
|
6
|
-
export interface UseLocationWatchResult {
|
|
7
|
-
location: LocationData | null;
|
|
8
|
-
error: LocationError | null;
|
|
9
|
-
isWatching: boolean;
|
|
10
|
-
startWatching: () => Promise<void>;
|
|
11
|
-
stopWatching: () => void;
|
|
12
|
-
}
|
|
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));
|
|
17
|
-
const watcherRef = useRef<LocationWatcher | null>(null);
|
|
18
|
-
const [location, setLocation] = useState<LocationData | null>(null);
|
|
19
|
-
const [error, setError] = useState<LocationError | null>(null);
|
|
20
|
-
const [isWatching, setIsWatching] = useState(false);
|
|
21
|
-
|
|
22
|
-
const stopWatching = useCallback(() => {
|
|
23
|
-
if (watcherRef.current) {
|
|
24
|
-
watcherRef.current.clearWatch();
|
|
25
|
-
watcherRef.current = null;
|
|
26
|
-
setIsWatching(false);
|
|
27
|
-
}
|
|
28
|
-
}, []);
|
|
29
|
-
|
|
30
|
-
const startWatching = useCallback(async () => {
|
|
31
|
-
stopWatching();
|
|
32
|
-
setError(null);
|
|
33
|
-
|
|
34
|
-
const watcher = new LocationWatcher(options);
|
|
35
|
-
watcherRef.current = watcher;
|
|
36
|
-
|
|
37
|
-
await watcher.watchPosition(
|
|
38
|
-
(data) => {
|
|
39
|
-
setLocation(data);
|
|
40
|
-
setError(null);
|
|
41
|
-
},
|
|
42
|
-
(err) => {
|
|
43
|
-
// Clear watcher reference on error to maintain consistent state
|
|
44
|
-
watcherRef.current = null;
|
|
45
|
-
setError(err);
|
|
46
|
-
setIsWatching(false);
|
|
47
|
-
},
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
if (watcher.isWatching()) {
|
|
51
|
-
setIsWatching(true);
|
|
52
|
-
}
|
|
53
|
-
}, [optionsStringRef, stopWatching]); // Use string ref for stable dependency
|
|
54
|
-
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
return () => {
|
|
57
|
-
stopWatching();
|
|
58
|
-
};
|
|
59
|
-
}, [stopWatching]);
|
|
60
|
-
|
|
61
|
-
return { location, error, isWatching, startWatching, stopWatching };
|
|
62
|
-
}
|