@umituz/react-native-location 1.0.5 → 1.0.7
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.
|
|
3
|
+
"version": "1.0.7",
|
|
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",
|
|
@@ -33,10 +33,11 @@
|
|
|
33
33
|
"react-native": ">=0.74.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
+
"@types/react": "~19.1.10",
|
|
36
37
|
"@umituz/react-native-design-system": "latest",
|
|
37
38
|
"@umituz/react-native-storage": "latest",
|
|
38
39
|
"expo-location": "~18.0.0",
|
|
39
|
-
"
|
|
40
|
+
"firebase": "^12.7.0",
|
|
40
41
|
"react": "19.1.0",
|
|
41
42
|
"react-native": "0.81.5",
|
|
42
43
|
"typescript": "~5.9.2"
|
|
@@ -49,4 +50,4 @@
|
|
|
49
50
|
"README.md",
|
|
50
51
|
"LICENSE"
|
|
51
52
|
]
|
|
52
|
-
}
|
|
53
|
+
}
|
|
@@ -1,23 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
latitude: number;
|
|
3
|
-
longitude: number;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface LocationAddress {
|
|
7
|
-
city?: string | null;
|
|
8
|
-
region?: string | null;
|
|
9
|
-
country?: string | null;
|
|
10
|
-
street?: string | null;
|
|
11
|
-
formattedAddress?: string | null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface LocationData {
|
|
15
|
-
coords: Coordinates;
|
|
16
|
-
timestamp: number;
|
|
17
|
-
address?: LocationAddress;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface LocationError {
|
|
21
|
-
code: string;
|
|
22
|
-
message: string;
|
|
23
|
-
}
|
|
1
|
+
export * from "../../types/location.types";
|
package/src/index.ts
CHANGED
|
@@ -1,53 +1,140 @@
|
|
|
1
1
|
import * as Location from "expo-location";
|
|
2
|
-
import {
|
|
2
|
+
import { storageRepository, unwrap } from "@umituz/react-native-storage";
|
|
3
|
+
import {
|
|
4
|
+
LocationData,
|
|
5
|
+
LocationConfig,
|
|
6
|
+
DEFAULT_LOCATION_CONFIG,
|
|
7
|
+
CachedLocationData,
|
|
8
|
+
LocationErrorImpl,
|
|
9
|
+
LocationErrorCode,
|
|
10
|
+
} from "../../types/location.types";
|
|
11
|
+
|
|
12
|
+
declare const __DEV__: boolean;
|
|
3
13
|
|
|
4
14
|
export class LocationService {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
private config: LocationConfig;
|
|
16
|
+
private storage = storageRepository;
|
|
17
|
+
|
|
18
|
+
constructor(config: LocationConfig = {}) {
|
|
19
|
+
this.config = { ...DEFAULT_LOCATION_CONFIG, ...config };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private log(message: string, ...args: unknown[]): void {
|
|
23
|
+
if (__DEV__) {
|
|
24
|
+
console.log(`[LocationService] ${message}`, ...args);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private logError(message: string, error: unknown): void {
|
|
29
|
+
if (__DEV__) {
|
|
30
|
+
console.error(`[LocationService] ${message}`, error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private logWarn(message: string, ...args: unknown[]): void {
|
|
35
|
+
if (__DEV__) {
|
|
36
|
+
console.warn(`[LocationService] ${message}`, ...args);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
9
40
|
async requestPermissions(): Promise<boolean> {
|
|
10
41
|
try {
|
|
11
|
-
|
|
42
|
+
this.log("Requesting permissions...");
|
|
12
43
|
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
13
|
-
|
|
44
|
+
this.log("Permission status:", status);
|
|
14
45
|
return status === "granted";
|
|
15
46
|
} catch (error) {
|
|
16
|
-
|
|
47
|
+
this.logError("Error requesting permissions:", error);
|
|
17
48
|
return false;
|
|
18
49
|
}
|
|
19
50
|
}
|
|
20
51
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
52
|
+
private async getCachedLocation(): Promise<LocationData | null> {
|
|
53
|
+
if (!this.config.enableCache) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const cacheKey = `location_cache_${this.config.cacheKey}`;
|
|
59
|
+
const result = await this.storage.getItem<CachedLocationData | null>(cacheKey, null);
|
|
60
|
+
const cached = unwrap(result, null);
|
|
61
|
+
|
|
62
|
+
if (!cached) {
|
|
63
|
+
this.log("No cached location found");
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const cacheAge = now - cached.timestamp;
|
|
69
|
+
const cacheDuration = this.config.cacheDuration || 300000;
|
|
70
|
+
|
|
71
|
+
if (cacheAge > cacheDuration) {
|
|
72
|
+
this.log("Cache expired");
|
|
73
|
+
await this.storage.removeItem(cacheKey);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.log("Using cached location");
|
|
78
|
+
return cached.location;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.logError("Cache read error:", error);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async cacheLocation(location: LocationData): Promise<void> {
|
|
86
|
+
if (!this.config.enableCache) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const cacheKey = `location_cache_${this.config.cacheKey}`;
|
|
92
|
+
const cachedData: CachedLocationData = {
|
|
93
|
+
location,
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
};
|
|
96
|
+
await this.storage.setItem(cacheKey, cachedData);
|
|
97
|
+
this.log("Location cached successfully");
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this.logError("Cache write error:", error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getCurrentPosition(): Promise<LocationData> {
|
|
104
|
+
const withAddress = this.config.withAddress ?? true;
|
|
105
|
+
|
|
106
|
+
this.log("getCurrentPosition called");
|
|
107
|
+
|
|
108
|
+
const cached = await this.getCachedLocation();
|
|
109
|
+
if (cached) {
|
|
110
|
+
this.log("Returning cached location");
|
|
111
|
+
return cached;
|
|
112
|
+
}
|
|
113
|
+
|
|
27
114
|
const hasPermission = await this.requestPermissions();
|
|
28
115
|
if (!hasPermission) {
|
|
29
|
-
|
|
30
|
-
throw
|
|
116
|
+
this.logWarn("Permission denied");
|
|
117
|
+
throw new LocationErrorImpl("PERMISSION_DENIED", "Location permission not granted");
|
|
31
118
|
}
|
|
32
119
|
|
|
33
120
|
try {
|
|
34
|
-
|
|
121
|
+
this.log("Getting position...");
|
|
35
122
|
const location = await Location.getCurrentPositionAsync({
|
|
36
|
-
accuracy:
|
|
123
|
+
accuracy: this.config.accuracy,
|
|
37
124
|
});
|
|
38
|
-
|
|
125
|
+
this.log("Position obtained", location);
|
|
39
126
|
|
|
40
127
|
let addressData;
|
|
41
128
|
if (withAddress) {
|
|
42
|
-
|
|
129
|
+
this.log("Reverse geocoding...");
|
|
43
130
|
addressData = await this.reverseGeocode(
|
|
44
131
|
location.coords.latitude,
|
|
45
132
|
location.coords.longitude
|
|
46
133
|
);
|
|
47
|
-
|
|
134
|
+
this.log("Address obtained", addressData);
|
|
48
135
|
}
|
|
49
136
|
|
|
50
|
-
|
|
137
|
+
const locationData: LocationData = {
|
|
51
138
|
coords: {
|
|
52
139
|
latitude: location.coords.latitude,
|
|
53
140
|
longitude: location.coords.longitude,
|
|
@@ -55,17 +142,27 @@ export class LocationService {
|
|
|
55
142
|
timestamp: location.timestamp,
|
|
56
143
|
address: addressData,
|
|
57
144
|
};
|
|
145
|
+
|
|
146
|
+
await this.cacheLocation(locationData);
|
|
147
|
+
|
|
148
|
+
return locationData;
|
|
58
149
|
} catch (error) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
150
|
+
this.logError("Error getting location:", error);
|
|
151
|
+
|
|
152
|
+
let errorCode: LocationErrorCode = "UNKNOWN_ERROR";
|
|
153
|
+
let errorMessage = "Unknown error getting location";
|
|
154
|
+
|
|
155
|
+
if (error instanceof LocationErrorImpl) {
|
|
156
|
+
errorCode = error.code;
|
|
157
|
+
errorMessage = error.message;
|
|
158
|
+
} else if (error instanceof Error) {
|
|
159
|
+
errorMessage = error.message;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
throw new LocationErrorImpl(errorCode, errorMessage);
|
|
63
163
|
}
|
|
64
164
|
}
|
|
65
165
|
|
|
66
|
-
/**
|
|
67
|
-
* Reverse geocodes coordinates to an address.
|
|
68
|
-
*/
|
|
69
166
|
async reverseGeocode(latitude: number, longitude: number) {
|
|
70
167
|
try {
|
|
71
168
|
const [address] = await Location.reverseGeocodeAsync({ latitude, longitude });
|
|
@@ -79,10 +176,12 @@ export class LocationService {
|
|
|
79
176
|
formattedAddress: [address.city, address.country].filter(Boolean).join(", "),
|
|
80
177
|
};
|
|
81
178
|
} catch (error) {
|
|
82
|
-
|
|
179
|
+
this.logWarn("Reverse geocode failed:", error);
|
|
83
180
|
return undefined;
|
|
84
181
|
}
|
|
85
182
|
}
|
|
86
183
|
}
|
|
87
184
|
|
|
88
|
-
export
|
|
185
|
+
export function createLocationService(config?: LocationConfig): LocationService {
|
|
186
|
+
return new LocationService(config);
|
|
187
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useState, useCallback } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import { LocationData, LocationError } from "../../
|
|
1
|
+
import { useState, useCallback, useRef } from "react";
|
|
2
|
+
import { createLocationService } from "../../infrastructure/services/LocationService";
|
|
3
|
+
import { LocationData, LocationError, LocationConfig } from "../../types/location.types";
|
|
4
4
|
|
|
5
5
|
export interface UseLocationResult {
|
|
6
6
|
location: LocationData | null;
|
|
@@ -9,7 +9,8 @@ export interface UseLocationResult {
|
|
|
9
9
|
getCurrentLocation: () => Promise<LocationData | null>;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function useLocation(): UseLocationResult {
|
|
12
|
+
export function useLocation(config?: LocationConfig): UseLocationResult {
|
|
13
|
+
const serviceRef = useRef(createLocationService(config));
|
|
13
14
|
const [location, setLocation] = useState<LocationData | null>(null);
|
|
14
15
|
const [isLoading, setIsLoading] = useState(false);
|
|
15
16
|
const [error, setError] = useState<LocationError | null>(null);
|
|
@@ -17,15 +18,24 @@ export function useLocation(): UseLocationResult {
|
|
|
17
18
|
const getCurrentLocation = useCallback(async () => {
|
|
18
19
|
setIsLoading(true);
|
|
19
20
|
setError(null);
|
|
21
|
+
|
|
20
22
|
try {
|
|
21
|
-
const data = await
|
|
23
|
+
const data = await serviceRef.current.getCurrentPosition();
|
|
22
24
|
setLocation(data);
|
|
23
25
|
return data;
|
|
24
|
-
} catch (err
|
|
25
|
-
|
|
26
|
-
code:
|
|
27
|
-
message:
|
|
26
|
+
} catch (err) {
|
|
27
|
+
let errorObj: LocationError = {
|
|
28
|
+
code: "UNKNOWN_ERROR",
|
|
29
|
+
message: "An unknown error occurred",
|
|
28
30
|
};
|
|
31
|
+
|
|
32
|
+
if (err && typeof err === "object" && "code" in err && "message" in err) {
|
|
33
|
+
errorObj = {
|
|
34
|
+
code: typeof err.code === "string" ? err.code : "UNKNOWN_ERROR",
|
|
35
|
+
message: typeof err.message === "string" ? err.message : "An unknown error occurred",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
setError(errorObj);
|
|
30
40
|
return null;
|
|
31
41
|
} finally {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as Location from "expo-location";
|
|
2
|
+
|
|
3
|
+
export interface Coordinates {
|
|
4
|
+
latitude: number;
|
|
5
|
+
longitude: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface LocationAddress {
|
|
9
|
+
city?: string | null;
|
|
10
|
+
region?: string | null;
|
|
11
|
+
country?: string | null;
|
|
12
|
+
street?: string | null;
|
|
13
|
+
formattedAddress?: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LocationData {
|
|
17
|
+
coords: Coordinates;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
address?: LocationAddress;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LocationError {
|
|
23
|
+
code: string;
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CachedLocationData {
|
|
28
|
+
location: LocationData;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LocationConfig {
|
|
33
|
+
accuracy?: Location.Accuracy;
|
|
34
|
+
timeout?: number;
|
|
35
|
+
enableCache?: boolean;
|
|
36
|
+
cacheKey?: string;
|
|
37
|
+
cacheDuration?: number;
|
|
38
|
+
withAddress?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const DEFAULT_LOCATION_CONFIG: LocationConfig = {
|
|
42
|
+
accuracy: Location.Accuracy.Balanced,
|
|
43
|
+
timeout: 10000,
|
|
44
|
+
enableCache: true,
|
|
45
|
+
cacheKey: "default",
|
|
46
|
+
cacheDuration: 300000,
|
|
47
|
+
withAddress: true,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type LocationErrorCode =
|
|
51
|
+
| "PERMISSION_DENIED"
|
|
52
|
+
| "LOCATION_UNAVAILABLE"
|
|
53
|
+
| "TIMEOUT"
|
|
54
|
+
| "CACHE_ERROR"
|
|
55
|
+
| "UNKNOWN_ERROR";
|
|
56
|
+
|
|
57
|
+
export class LocationErrorImpl extends Error implements LocationError {
|
|
58
|
+
code: LocationErrorCode;
|
|
59
|
+
|
|
60
|
+
constructor(code: LocationErrorCode, message: string) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = "LocationError";
|
|
63
|
+
this.code = code;
|
|
64
|
+
}
|
|
65
|
+
}
|