@umituz/react-native-location 1.0.15 → 1.0.16
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/index.ts +0 -1
- package/src/infrastructure/services/LocationService.ts +95 -94
- package/src/infrastructure/services/LocationWatcher.ts +37 -53
- package/src/infrastructure/utils/LocationUtils.ts +5 -6
- package/src/presentation/hooks/useLocation.ts +38 -22
- package/src/presentation/hooks/useLocationWatch.ts +17 -35
- package/src/types/env.d.ts +1 -0
- package/src/types/location.types.ts +13 -44
- package/src/domain/entities/Location.ts +0 -1
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -2,21 +2,33 @@ import * as Location from "expo-location";
|
|
|
2
2
|
import { storageRepository, unwrap } from "@umituz/react-native-design-system/storage";
|
|
3
3
|
import {
|
|
4
4
|
LocationData,
|
|
5
|
+
LocationAddress,
|
|
5
6
|
LocationConfig,
|
|
6
|
-
|
|
7
|
-
CachedLocationData,
|
|
8
|
-
LocationErrorImpl,
|
|
7
|
+
LocationError,
|
|
9
8
|
LocationErrorCode,
|
|
10
9
|
} from "../../types/location.types";
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
interface CachedLocationData {
|
|
12
|
+
location: LocationData;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_CONFIG: Required<LocationConfig> = {
|
|
17
|
+
accuracy: Location.Accuracy.Balanced,
|
|
18
|
+
timeout: 10000,
|
|
19
|
+
enableCache: true,
|
|
20
|
+
cacheKey: "default",
|
|
21
|
+
cacheDuration: 300000,
|
|
22
|
+
withAddress: true,
|
|
23
|
+
};
|
|
13
24
|
|
|
14
25
|
export class LocationService {
|
|
15
|
-
private config: LocationConfig
|
|
26
|
+
private config: Required<LocationConfig>;
|
|
16
27
|
private storage = storageRepository;
|
|
28
|
+
private inFlightRequest: Promise<LocationData> | null = null;
|
|
17
29
|
|
|
18
30
|
constructor(config: LocationConfig = {}) {
|
|
19
|
-
this.config = { ...
|
|
31
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
private log(message: string, ...args: unknown[]): void {
|
|
@@ -31,17 +43,13 @@ export class LocationService {
|
|
|
31
43
|
}
|
|
32
44
|
}
|
|
33
45
|
|
|
34
|
-
private logWarn(message: string, ...args: unknown[]): void {
|
|
35
|
-
if (__DEV__) {
|
|
36
|
-
console.warn(`[LocationService] ${message}`, ...args);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
46
|
async requestPermissions(): Promise<boolean> {
|
|
41
47
|
try {
|
|
48
|
+
const { status: current } = await Location.getForegroundPermissionsAsync();
|
|
49
|
+
if (current === "granted") return true;
|
|
50
|
+
|
|
42
51
|
this.log("Requesting permissions...");
|
|
43
52
|
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
44
|
-
this.log("Permission status:", status);
|
|
45
53
|
return status === "granted";
|
|
46
54
|
} catch (error) {
|
|
47
55
|
this.logError("Error requesting permissions:", error);
|
|
@@ -49,27 +57,23 @@ export class LocationService {
|
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
private getCacheKey(): string {
|
|
61
|
+
const suffix = this.config.withAddress ? "_addr" : "";
|
|
62
|
+
return `location_cache_${this.config.cacheKey}${suffix}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
private async getCachedLocation(): Promise<LocationData | null> {
|
|
53
|
-
if (!this.config.enableCache)
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
66
|
+
if (!this.config.enableCache) return null;
|
|
56
67
|
|
|
57
68
|
try {
|
|
58
|
-
const cacheKey =
|
|
69
|
+
const cacheKey = this.getCacheKey();
|
|
59
70
|
const result = await this.storage.getItem<CachedLocationData | null>(cacheKey, null);
|
|
60
71
|
const cached = unwrap(result, null);
|
|
61
72
|
|
|
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;
|
|
73
|
+
if (!cached) return null;
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
const cacheAge = Date.now() - cached.timestamp;
|
|
76
|
+
if (cacheAge > this.config.cacheDuration) {
|
|
73
77
|
await this.storage.removeItem(cacheKey);
|
|
74
78
|
return null;
|
|
75
79
|
}
|
|
@@ -83,55 +87,47 @@ export class LocationService {
|
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
private async cacheLocation(location: LocationData): Promise<void> {
|
|
86
|
-
if (!this.config.enableCache)
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
90
|
+
if (!this.config.enableCache) return;
|
|
89
91
|
|
|
90
92
|
try {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
location,
|
|
94
|
-
timestamp: Date.now(),
|
|
95
|
-
};
|
|
96
|
-
await this.storage.setItem(cacheKey, cachedData);
|
|
97
|
-
this.log("Location cached successfully");
|
|
93
|
+
const data: CachedLocationData = { location, timestamp: Date.now() };
|
|
94
|
+
await this.storage.setItem(this.getCacheKey(), data);
|
|
98
95
|
} catch (error) {
|
|
99
96
|
this.logError("Cache write error:", error);
|
|
100
97
|
}
|
|
101
98
|
}
|
|
102
99
|
|
|
103
100
|
async getCurrentPosition(): Promise<LocationData> {
|
|
104
|
-
|
|
101
|
+
if (this.inFlightRequest) {
|
|
102
|
+
return this.inFlightRequest;
|
|
103
|
+
}
|
|
105
104
|
|
|
106
|
-
this.
|
|
105
|
+
this.inFlightRequest = this.fetchPosition();
|
|
106
|
+
try {
|
|
107
|
+
return await this.inFlightRequest;
|
|
108
|
+
} finally {
|
|
109
|
+
this.inFlightRequest = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
107
112
|
|
|
113
|
+
private async fetchPosition(): Promise<LocationData> {
|
|
108
114
|
const cached = await this.getCachedLocation();
|
|
109
|
-
if (cached)
|
|
110
|
-
this.log("Returning cached location");
|
|
111
|
-
return cached;
|
|
112
|
-
}
|
|
115
|
+
if (cached) return cached;
|
|
113
116
|
|
|
114
117
|
const hasPermission = await this.requestPermissions();
|
|
115
118
|
if (!hasPermission) {
|
|
116
|
-
this.
|
|
117
|
-
throw new LocationErrorImpl("PERMISSION_DENIED", "Location permission not granted");
|
|
119
|
+
throw this.createError("PERMISSION_DENIED", "Location permission not granted");
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
try {
|
|
121
|
-
this.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
let addressData;
|
|
128
|
-
if (withAddress) {
|
|
129
|
-
this.log("Reverse geocoding...");
|
|
130
|
-
addressData = await this.reverseGeocode(
|
|
123
|
+
const location = await this.getPositionWithTimeout();
|
|
124
|
+
|
|
125
|
+
let address: LocationAddress | undefined;
|
|
126
|
+
if (this.config.withAddress) {
|
|
127
|
+
address = await this.reverseGeocode(
|
|
131
128
|
location.coords.latitude,
|
|
132
|
-
location.coords.longitude
|
|
129
|
+
location.coords.longitude,
|
|
133
130
|
);
|
|
134
|
-
this.log("Address obtained", addressData);
|
|
135
131
|
}
|
|
136
132
|
|
|
137
133
|
const locationData: LocationData = {
|
|
@@ -140,53 +136,64 @@ export class LocationService {
|
|
|
140
136
|
longitude: location.coords.longitude,
|
|
141
137
|
},
|
|
142
138
|
timestamp: location.timestamp,
|
|
143
|
-
address
|
|
139
|
+
address,
|
|
144
140
|
};
|
|
145
141
|
|
|
146
142
|
await this.cacheLocation(locationData);
|
|
147
|
-
|
|
148
143
|
return locationData;
|
|
149
144
|
} catch (error) {
|
|
150
145
|
this.logError("Error getting location:", error);
|
|
151
146
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (error instanceof LocationErrorImpl) {
|
|
156
|
-
errorCode = error.code;
|
|
157
|
-
errorMessage = error.message;
|
|
158
|
-
} else if (error instanceof Error) {
|
|
159
|
-
errorMessage = error.message;
|
|
147
|
+
if (error instanceof Error && "code" in error) {
|
|
148
|
+
throw error;
|
|
160
149
|
}
|
|
161
150
|
|
|
162
|
-
|
|
151
|
+
const message = error instanceof Error ? error.message : "Unknown error getting location";
|
|
152
|
+
throw this.createError("UNKNOWN_ERROR", message);
|
|
163
153
|
}
|
|
164
154
|
}
|
|
165
155
|
|
|
166
|
-
async
|
|
156
|
+
private async getPositionWithTimeout(): Promise<Location.LocationObject> {
|
|
157
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
158
|
+
|
|
159
|
+
const locationPromise = Location.getCurrentPositionAsync({
|
|
160
|
+
accuracy: this.config.accuracy,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
164
|
+
timeoutId = setTimeout(() => {
|
|
165
|
+
reject(this.createError("TIMEOUT", `Location request timed out after ${this.config.timeout}ms`));
|
|
166
|
+
}, this.config.timeout);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const result = await Promise.race([locationPromise, timeoutPromise]);
|
|
170
|
+
clearTimeout(timeoutId!);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async reverseGeocode(latitude: number, longitude: number): Promise<LocationAddress | undefined> {
|
|
167
175
|
try {
|
|
168
176
|
const [address] = await Location.reverseGeocodeAsync({ latitude, longitude });
|
|
169
177
|
if (!address) return undefined;
|
|
170
178
|
|
|
171
179
|
return {
|
|
172
|
-
city: address.city,
|
|
173
|
-
region: address.region,
|
|
174
|
-
country: address.country,
|
|
175
|
-
street: address.street,
|
|
176
|
-
formattedAddress: [address.city, address.country]
|
|
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,
|
|
177
187
|
};
|
|
178
188
|
} catch (error) {
|
|
179
|
-
this.
|
|
189
|
+
this.logError("Reverse geocode failed:", error);
|
|
180
190
|
return undefined;
|
|
181
191
|
}
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
async isLocationEnabled(): Promise<boolean> {
|
|
185
195
|
try {
|
|
186
|
-
|
|
187
|
-
const enabled = await Location.hasServicesEnabledAsync();
|
|
188
|
-
this.log("Location enabled:", enabled);
|
|
189
|
-
return enabled;
|
|
196
|
+
return await Location.hasServicesEnabledAsync();
|
|
190
197
|
} catch (error) {
|
|
191
198
|
this.logError("Error checking location enabled:", error);
|
|
192
199
|
return false;
|
|
@@ -195,10 +202,8 @@ export class LocationService {
|
|
|
195
202
|
|
|
196
203
|
async getPermissionStatus(): Promise<Location.PermissionStatus> {
|
|
197
204
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
this.log("Permission status:", status.status);
|
|
201
|
-
return status.status;
|
|
205
|
+
const { status } = await Location.getForegroundPermissionsAsync();
|
|
206
|
+
return status;
|
|
202
207
|
} catch (error) {
|
|
203
208
|
this.logError("Error getting permission status:", error);
|
|
204
209
|
return Location.PermissionStatus.UNDETERMINED;
|
|
@@ -207,15 +212,8 @@ export class LocationService {
|
|
|
207
212
|
|
|
208
213
|
async getLastKnownPosition(): Promise<LocationData | null> {
|
|
209
214
|
try {
|
|
210
|
-
this.log("Getting last known position...");
|
|
211
215
|
const location = await Location.getLastKnownPositionAsync();
|
|
212
|
-
|
|
213
|
-
if (!location) {
|
|
214
|
-
this.log("No last known position available");
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
this.log("Last known position obtained", location);
|
|
216
|
+
if (!location) return null;
|
|
219
217
|
|
|
220
218
|
return {
|
|
221
219
|
coords: {
|
|
@@ -229,8 +227,11 @@ export class LocationService {
|
|
|
229
227
|
return null;
|
|
230
228
|
}
|
|
231
229
|
}
|
|
232
|
-
}
|
|
233
230
|
|
|
234
|
-
|
|
235
|
-
|
|
231
|
+
private createError(code: LocationErrorCode, message: string): LocationError & Error {
|
|
232
|
+
const error = new Error(message) as Error & LocationError;
|
|
233
|
+
error.name = "LocationError";
|
|
234
|
+
error.code = code;
|
|
235
|
+
return error;
|
|
236
|
+
}
|
|
236
237
|
}
|
|
@@ -2,14 +2,12 @@ import * as Location from "expo-location";
|
|
|
2
2
|
import {
|
|
3
3
|
LocationData,
|
|
4
4
|
LocationError,
|
|
5
|
-
LocationCallback,
|
|
6
|
-
LocationErrorCallback,
|
|
7
|
-
LocationWatcherOptions,
|
|
8
|
-
LocationErrorImpl,
|
|
9
5
|
LocationErrorCode,
|
|
6
|
+
LocationWatcherOptions,
|
|
10
7
|
} from "../../types/location.types";
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
type LocationCallback = (location: LocationData) => void;
|
|
10
|
+
type ErrorCallback = (error: LocationError) => void;
|
|
13
11
|
|
|
14
12
|
export class LocationWatcher {
|
|
15
13
|
private subscription: Location.LocationSubscription | null = null;
|
|
@@ -31,70 +29,46 @@ export class LocationWatcher {
|
|
|
31
29
|
}
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
async watchPosition(
|
|
35
|
-
|
|
36
|
-
onError?: LocationErrorCallback
|
|
37
|
-
): Promise<string> {
|
|
38
|
-
try {
|
|
39
|
-
this.log("Requesting permissions...");
|
|
40
|
-
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
32
|
+
async watchPosition(onSuccess: LocationCallback, onError?: ErrorCallback): Promise<void> {
|
|
33
|
+
this.clearWatch();
|
|
41
34
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (onError) {
|
|
48
|
-
onError(error);
|
|
49
|
-
}
|
|
50
|
-
throw new LocationErrorImpl("PERMISSION_DENIED", "Location permission not granted");
|
|
35
|
+
try {
|
|
36
|
+
const granted = await this.ensurePermission();
|
|
37
|
+
if (!granted) {
|
|
38
|
+
onError?.({ code: "PERMISSION_DENIED", message: "Location permission not granted" });
|
|
39
|
+
return;
|
|
51
40
|
}
|
|
52
41
|
|
|
53
|
-
this.log("Starting location watch...");
|
|
54
|
-
|
|
55
42
|
this.subscription = await Location.watchPositionAsync(
|
|
56
43
|
{
|
|
57
|
-
accuracy: this.options.accuracy
|
|
44
|
+
accuracy: this.options.accuracy ?? Location.Accuracy.Balanced,
|
|
45
|
+
distanceInterval: this.options.distanceInterval,
|
|
46
|
+
timeInterval: this.options.timeInterval,
|
|
58
47
|
},
|
|
59
48
|
(location) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const locationData: LocationData = {
|
|
49
|
+
onSuccess({
|
|
63
50
|
coords: {
|
|
64
51
|
latitude: location.coords.latitude,
|
|
65
52
|
longitude: location.coords.longitude,
|
|
66
53
|
},
|
|
67
54
|
timestamp: location.timestamp,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
onSuccess(locationData);
|
|
71
|
-
}
|
|
55
|
+
});
|
|
56
|
+
},
|
|
72
57
|
);
|
|
73
|
-
|
|
74
|
-
return "watching";
|
|
75
58
|
} catch (error) {
|
|
76
59
|
this.logError("Error watching position:", error);
|
|
77
60
|
|
|
78
|
-
let
|
|
79
|
-
let
|
|
61
|
+
let code: LocationErrorCode = "UNKNOWN_ERROR";
|
|
62
|
+
let message = "Unknown error watching location";
|
|
80
63
|
|
|
81
|
-
if (error instanceof
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const locationError: LocationError = {
|
|
89
|
-
code: errorCode,
|
|
90
|
-
message: errorMessage,
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
if (onError) {
|
|
94
|
-
onError(locationError);
|
|
64
|
+
if (error instanceof Error) {
|
|
65
|
+
message = error.message;
|
|
66
|
+
if ("code" in error) {
|
|
67
|
+
code = (error as { code: string }).code as LocationErrorCode;
|
|
68
|
+
}
|
|
95
69
|
}
|
|
96
70
|
|
|
97
|
-
|
|
71
|
+
onError?.({ code, message });
|
|
98
72
|
}
|
|
99
73
|
}
|
|
100
74
|
|
|
@@ -109,8 +83,18 @@ export class LocationWatcher {
|
|
|
109
83
|
isWatching(): boolean {
|
|
110
84
|
return this.subscription !== null;
|
|
111
85
|
}
|
|
112
|
-
}
|
|
113
86
|
|
|
114
|
-
|
|
115
|
-
|
|
87
|
+
private async ensurePermission(): Promise<boolean> {
|
|
88
|
+
try {
|
|
89
|
+
const { status: current } = await Location.getForegroundPermissionsAsync();
|
|
90
|
+
if (current === "granted") return true;
|
|
91
|
+
|
|
92
|
+
this.log("Requesting permissions...");
|
|
93
|
+
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
94
|
+
return status === "granted";
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.logError("Error requesting permissions:", error);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
116
100
|
}
|
|
@@ -73,11 +73,10 @@ export class LocationUtils {
|
|
|
73
73
|
coord2: Coordinates,
|
|
74
74
|
precision = 6
|
|
75
75
|
): boolean {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return lat1 === lat2 && lon1 === lon2;
|
|
76
|
+
const epsilon = Math.pow(10, -precision);
|
|
77
|
+
return (
|
|
78
|
+
Math.abs(coord1.latitude - coord2.latitude) < epsilon &&
|
|
79
|
+
Math.abs(coord1.longitude - coord2.longitude) < epsilon
|
|
80
|
+
);
|
|
82
81
|
}
|
|
83
82
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useState, useCallback, useRef } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import { LocationData, LocationError, LocationConfig } from "../../types/location.types";
|
|
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
4
|
|
|
5
5
|
export interface UseLocationResult {
|
|
6
6
|
location: LocationData | null;
|
|
@@ -10,10 +10,25 @@ export interface UseLocationResult {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function useLocation(config?: LocationConfig): UseLocationResult {
|
|
13
|
-
const
|
|
13
|
+
const configRef = useRef(config);
|
|
14
|
+
const serviceRef = useRef(new LocationService(config));
|
|
15
|
+
|
|
16
|
+
if (configRef.current !== config) {
|
|
17
|
+
configRef.current = config;
|
|
18
|
+
serviceRef.current = new LocationService(config);
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
const [location, setLocation] = useState<LocationData | null>(null);
|
|
15
22
|
const [isLoading, setIsLoading] = useState(false);
|
|
16
23
|
const [error, setError] = useState<LocationError | null>(null);
|
|
24
|
+
const mountedRef = useRef(true);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
mountedRef.current = true;
|
|
28
|
+
return () => {
|
|
29
|
+
mountedRef.current = false;
|
|
30
|
+
};
|
|
31
|
+
}, []);
|
|
17
32
|
|
|
18
33
|
const getCurrentLocation = useCallback(async () => {
|
|
19
34
|
setIsLoading(true);
|
|
@@ -21,32 +36,33 @@ export function useLocation(config?: LocationConfig): UseLocationResult {
|
|
|
21
36
|
|
|
22
37
|
try {
|
|
23
38
|
const data = await serviceRef.current.getCurrentPosition();
|
|
24
|
-
|
|
39
|
+
if (mountedRef.current) {
|
|
40
|
+
setLocation(data);
|
|
41
|
+
}
|
|
25
42
|
return data;
|
|
26
43
|
} catch (err) {
|
|
27
|
-
|
|
28
|
-
code:
|
|
29
|
-
message: "An unknown error occurred",
|
|
44
|
+
const errorObj: LocationError = {
|
|
45
|
+
code: extractErrorCode(err),
|
|
46
|
+
message: err instanceof Error ? err.message : "An unknown error occurred",
|
|
30
47
|
};
|
|
31
48
|
|
|
32
|
-
if (
|
|
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
|
-
};
|
|
49
|
+
if (mountedRef.current) {
|
|
50
|
+
setError(errorObj);
|
|
37
51
|
}
|
|
38
|
-
|
|
39
|
-
setError(errorObj);
|
|
40
52
|
return null;
|
|
41
53
|
} finally {
|
|
42
|
-
|
|
54
|
+
if (mountedRef.current) {
|
|
55
|
+
setIsLoading(false);
|
|
56
|
+
}
|
|
43
57
|
}
|
|
44
58
|
}, []);
|
|
45
59
|
|
|
46
|
-
return {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
return { location, isLoading, error, getCurrentLocation };
|
|
61
|
+
}
|
|
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";
|
|
52
68
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, useCallback } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { LocationWatcher } from "../../infrastructure/services/LocationWatcher";
|
|
3
3
|
import { LocationData, LocationError, LocationWatcherOptions } from "../../types/location.types";
|
|
4
4
|
|
|
5
5
|
export interface UseLocationWatchResult {
|
|
@@ -11,7 +11,7 @@ export interface UseLocationWatchResult {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function useLocationWatch(options?: LocationWatcherOptions): UseLocationWatchResult {
|
|
14
|
-
const watcherRef = useRef<
|
|
14
|
+
const watcherRef = useRef<LocationWatcher | null>(null);
|
|
15
15
|
const [location, setLocation] = useState<LocationData | null>(null);
|
|
16
16
|
const [error, setError] = useState<LocationError | null>(null);
|
|
17
17
|
const [isWatching, setIsWatching] = useState(false);
|
|
@@ -26,36 +26,24 @@ export function useLocationWatch(options?: LocationWatcherOptions): UseLocationW
|
|
|
26
26
|
|
|
27
27
|
const startWatching = useCallback(async () => {
|
|
28
28
|
stopWatching();
|
|
29
|
+
setError(null);
|
|
29
30
|
|
|
30
|
-
const watcher =
|
|
31
|
+
const watcher = new LocationWatcher(options);
|
|
31
32
|
watcherRef.current = watcher;
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
(data)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
(err)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
await watcher.watchPosition(
|
|
35
|
+
(data) => {
|
|
36
|
+
setLocation(data);
|
|
37
|
+
setError(null);
|
|
38
|
+
},
|
|
39
|
+
(err) => {
|
|
40
|
+
setError(err);
|
|
41
|
+
setIsWatching(false);
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (watcher.isWatching()) {
|
|
43
46
|
setIsWatching(true);
|
|
44
|
-
} catch (err) {
|
|
45
|
-
let errorObj: LocationError = {
|
|
46
|
-
code: "UNKNOWN_ERROR",
|
|
47
|
-
message: "An unknown error occurred",
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
if (err && typeof err === "object" && "code" in err && "message" in err) {
|
|
51
|
-
errorObj = {
|
|
52
|
-
code: typeof err.code === "string" ? err.code : "UNKNOWN_ERROR",
|
|
53
|
-
message: typeof err.message === "string" ? err.message : "An unknown error occurred",
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
setError(errorObj);
|
|
58
|
-
setIsWatching(false);
|
|
59
47
|
}
|
|
60
48
|
}, [options, stopWatching]);
|
|
61
49
|
|
|
@@ -65,11 +53,5 @@ export function useLocationWatch(options?: LocationWatcherOptions): UseLocationW
|
|
|
65
53
|
};
|
|
66
54
|
}, [stopWatching]);
|
|
67
55
|
|
|
68
|
-
return {
|
|
69
|
-
location,
|
|
70
|
-
error,
|
|
71
|
-
isWatching,
|
|
72
|
-
startWatching,
|
|
73
|
-
stopWatching,
|
|
74
|
-
};
|
|
56
|
+
return { location, error, isWatching, startWatching, stopWatching };
|
|
75
57
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare const __DEV__: boolean;
|
|
@@ -6,11 +6,11 @@ export interface Coordinates {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export interface LocationAddress {
|
|
9
|
-
city
|
|
10
|
-
region
|
|
11
|
-
country
|
|
12
|
-
street
|
|
13
|
-
formattedAddress
|
|
9
|
+
city: string | null;
|
|
10
|
+
region: string | null;
|
|
11
|
+
country: string | null;
|
|
12
|
+
street: string | null;
|
|
13
|
+
formattedAddress: string | null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface LocationData {
|
|
@@ -19,25 +19,18 @@ export interface LocationData {
|
|
|
19
19
|
address?: LocationAddress;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export type LocationErrorCode =
|
|
23
|
+
| "PERMISSION_DENIED"
|
|
24
|
+
| "TIMEOUT"
|
|
25
|
+
| "UNKNOWN_ERROR";
|
|
26
|
+
|
|
22
27
|
export interface LocationError {
|
|
23
|
-
code:
|
|
28
|
+
code: LocationErrorCode;
|
|
24
29
|
message: string;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
export interface CachedLocationData {
|
|
28
|
-
location: LocationData;
|
|
29
|
-
timestamp: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
32
|
export type DistanceUnit = "km" | "miles" | "meters";
|
|
33
33
|
|
|
34
|
-
export type LocationErrorCode =
|
|
35
|
-
| "PERMISSION_DENIED"
|
|
36
|
-
| "LOCATION_UNAVAILABLE"
|
|
37
|
-
| "TIMEOUT"
|
|
38
|
-
| "CACHE_ERROR"
|
|
39
|
-
| "UNKNOWN_ERROR";
|
|
40
|
-
|
|
41
34
|
export interface LocationConfig {
|
|
42
35
|
accuracy?: Location.Accuracy;
|
|
43
36
|
timeout?: number;
|
|
@@ -45,34 +38,10 @@ export interface LocationConfig {
|
|
|
45
38
|
cacheKey?: string;
|
|
46
39
|
cacheDuration?: number;
|
|
47
40
|
withAddress?: boolean;
|
|
48
|
-
distanceFilter?: number;
|
|
49
41
|
}
|
|
50
42
|
|
|
51
|
-
export const DEFAULT_LOCATION_CONFIG: LocationConfig = {
|
|
52
|
-
accuracy: Location.Accuracy.Balanced,
|
|
53
|
-
timeout: 10000,
|
|
54
|
-
enableCache: true,
|
|
55
|
-
cacheKey: "default",
|
|
56
|
-
cacheDuration: 300000,
|
|
57
|
-
withAddress: true,
|
|
58
|
-
distanceFilter: 10,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export type LocationCallback = (location: LocationData) => void;
|
|
62
|
-
export type LocationErrorCallback = (error: LocationError) => void;
|
|
63
|
-
|
|
64
43
|
export interface LocationWatcherOptions {
|
|
65
44
|
accuracy?: Location.Accuracy;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export class LocationErrorImpl extends Error implements LocationError {
|
|
71
|
-
code: LocationErrorCode;
|
|
72
|
-
|
|
73
|
-
constructor(code: LocationErrorCode, message: string) {
|
|
74
|
-
super(message);
|
|
75
|
-
this.name = "LocationError";
|
|
76
|
-
this.code = code;
|
|
77
|
-
}
|
|
45
|
+
distanceInterval?: number;
|
|
46
|
+
timeInterval?: number;
|
|
78
47
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "../../types/location.types";
|