@umituz/react-native-location 1.0.7 → 1.0.9

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.7",
3
+ "version": "1.0.9",
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",
package/src/index.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from "./domain/entities/Location";
2
2
  export * from "./types/location.types";
3
3
  export * from "./infrastructure/services/LocationService";
4
+ export * from "./infrastructure/services/LocationWatcher";
5
+ export * from "./infrastructure/utils/LocationUtils";
4
6
  export * from "./presentation/hooks/useLocation";
7
+ export * from "./presentation/hooks/useLocationWatch";
@@ -180,6 +180,55 @@ export class LocationService {
180
180
  return undefined;
181
181
  }
182
182
  }
183
+
184
+ async isLocationEnabled(): Promise<boolean> {
185
+ try {
186
+ this.log("Checking if location is enabled...");
187
+ const enabled = await Location.hasServicesEnabledAsync();
188
+ this.log("Location enabled:", enabled);
189
+ return enabled;
190
+ } catch (error) {
191
+ this.logError("Error checking location enabled:", error);
192
+ return false;
193
+ }
194
+ }
195
+
196
+ async getPermissionStatus(): Promise<Location.PermissionStatus> {
197
+ try {
198
+ this.log("Getting permission status...");
199
+ const status = await Location.getForegroundPermissionsAsync();
200
+ this.log("Permission status:", status.status);
201
+ return status.status;
202
+ } catch (error) {
203
+ this.logError("Error getting permission status:", error);
204
+ return "undetermined";
205
+ }
206
+ }
207
+
208
+ async getLastKnownPosition(): Promise<LocationData | null> {
209
+ try {
210
+ this.log("Getting last known position...");
211
+ 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);
219
+
220
+ return {
221
+ coords: {
222
+ latitude: location.coords.latitude,
223
+ longitude: location.coords.longitude,
224
+ },
225
+ timestamp: location.timestamp,
226
+ };
227
+ } catch (error) {
228
+ this.logError("Error getting last known position:", error);
229
+ return null;
230
+ }
231
+ }
183
232
  }
184
233
 
185
234
  export function createLocationService(config?: LocationConfig): LocationService {
@@ -0,0 +1,116 @@
1
+ import * as Location from "expo-location";
2
+ import {
3
+ LocationData,
4
+ LocationError,
5
+ LocationCallback,
6
+ LocationErrorCallback,
7
+ LocationWatcherOptions,
8
+ LocationErrorImpl,
9
+ LocationErrorCode,
10
+ } from "../../types/location.types";
11
+
12
+ declare const __DEV__: boolean;
13
+
14
+ export class LocationWatcher {
15
+ private subscription: Location.LocationSubscription | null = null;
16
+ private options: LocationWatcherOptions;
17
+
18
+ constructor(options: LocationWatcherOptions = {}) {
19
+ this.options = options;
20
+ }
21
+
22
+ private log(message: string, ...args: unknown[]): void {
23
+ if (__DEV__) {
24
+ console.log(`[LocationWatcher] ${message}`, ...args);
25
+ }
26
+ }
27
+
28
+ private logError(message: string, error: unknown): void {
29
+ if (__DEV__) {
30
+ console.error(`[LocationWatcher] ${message}`, error);
31
+ }
32
+ }
33
+
34
+ async watchPosition(
35
+ onSuccess: LocationCallback,
36
+ onError?: LocationErrorCallback
37
+ ): Promise<string> {
38
+ try {
39
+ this.log("Requesting permissions...");
40
+ const { status } = await Location.requestForegroundPermissionsAsync();
41
+
42
+ if (status !== "granted") {
43
+ const error: LocationError = {
44
+ code: "PERMISSION_DENIED",
45
+ message: "Location permission not granted",
46
+ };
47
+ if (onError) {
48
+ onError(error);
49
+ }
50
+ throw new LocationErrorImpl("PERMISSION_DENIED", "Location permission not granted");
51
+ }
52
+
53
+ this.log("Starting location watch...");
54
+
55
+ this.subscription = await Location.watchPositionAsync(
56
+ {
57
+ accuracy: this.options.accuracy || Location.Accuracy.Balanced,
58
+ },
59
+ (location) => {
60
+ this.log("Location update received", location);
61
+
62
+ const locationData: LocationData = {
63
+ coords: {
64
+ latitude: location.coords.latitude,
65
+ longitude: location.coords.longitude,
66
+ },
67
+ timestamp: location.timestamp,
68
+ };
69
+
70
+ onSuccess(locationData);
71
+ }
72
+ );
73
+
74
+ return "watching";
75
+ } catch (error) {
76
+ this.logError("Error watching position:", error);
77
+
78
+ let errorCode: LocationErrorCode = "UNKNOWN_ERROR";
79
+ let errorMessage = "Unknown error watching location";
80
+
81
+ if (error instanceof LocationErrorImpl) {
82
+ errorCode = error.code;
83
+ errorMessage = error.message;
84
+ } else if (error instanceof Error) {
85
+ errorMessage = error.message;
86
+ }
87
+
88
+ const locationError: LocationError = {
89
+ code: errorCode,
90
+ message: errorMessage,
91
+ };
92
+
93
+ if (onError) {
94
+ onError(locationError);
95
+ }
96
+
97
+ throw new LocationErrorImpl(errorCode, errorMessage);
98
+ }
99
+ }
100
+
101
+ clearWatch(): void {
102
+ if (this.subscription) {
103
+ this.log("Clearing location watch");
104
+ this.subscription.remove();
105
+ this.subscription = null;
106
+ }
107
+ }
108
+
109
+ isWatching(): boolean {
110
+ return this.subscription !== null;
111
+ }
112
+ }
113
+
114
+ export function createLocationWatcher(options?: LocationWatcherOptions): LocationWatcher {
115
+ return new LocationWatcher(options);
116
+ }
@@ -0,0 +1,83 @@
1
+ import { Coordinates, DistanceUnit } from "../../types/location.types";
2
+
3
+ export class LocationUtils {
4
+ private static readonly EARTH_RADIUS_KM = 6371;
5
+ private static readonly EARTH_RADIUS_MILES = 3959;
6
+ private static readonly EARTH_RADIUS_METERS = 6371000;
7
+
8
+ static calculateDistance(
9
+ from: Coordinates,
10
+ to: Coordinates,
11
+ unit: DistanceUnit = "km"
12
+ ): number {
13
+ const lat1Rad = (from.latitude * Math.PI) / 180;
14
+ const lat2Rad = (to.latitude * Math.PI) / 180;
15
+ const deltaLatRad = ((to.latitude - from.latitude) * Math.PI) / 180;
16
+ const deltaLonRad = ((to.longitude - from.longitude) * Math.PI) / 180;
17
+
18
+ const a =
19
+ Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) +
20
+ Math.cos(lat1Rad) *
21
+ Math.cos(lat2Rad) *
22
+ Math.sin(deltaLonRad / 2) *
23
+ Math.sin(deltaLonRad / 2);
24
+
25
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
26
+
27
+ switch (unit) {
28
+ case "miles":
29
+ return this.EARTH_RADIUS_MILES * c;
30
+ case "meters":
31
+ return this.EARTH_RADIUS_METERS * c;
32
+ case "km":
33
+ default:
34
+ return this.EARTH_RADIUS_KM * c;
35
+ }
36
+ }
37
+
38
+ static isValidCoordinate(latitude: number, longitude: number): boolean {
39
+ return (
40
+ latitude >= -90 &&
41
+ latitude <= 90 &&
42
+ longitude >= -180 &&
43
+ longitude <= 180
44
+ );
45
+ }
46
+
47
+ static isValidAccuracy(accuracy: number | null | undefined, maxAccuracy = 100): boolean {
48
+ if (accuracy === null || accuracy === undefined) {
49
+ return false;
50
+ }
51
+ return accuracy > 0 && accuracy <= maxAccuracy;
52
+ }
53
+
54
+ static formatAccuracy(accuracy: number | null | undefined): string {
55
+ if (accuracy === null || accuracy === undefined) {
56
+ return "Unknown";
57
+ }
58
+
59
+ if (accuracy < 10) {
60
+ return "Excellent (±" + Math.round(accuracy) + "m)";
61
+ }
62
+ if (accuracy < 50) {
63
+ return "Good (±" + Math.round(accuracy) + "m)";
64
+ }
65
+ if (accuracy < 100) {
66
+ return "Fair (±" + Math.round(accuracy) + "m)";
67
+ }
68
+ return "Poor (±" + Math.round(accuracy) + "m)";
69
+ }
70
+
71
+ static coordinatesAreEqual(
72
+ coord1: Coordinates,
73
+ coord2: Coordinates,
74
+ precision = 6
75
+ ): boolean {
76
+ const lat1 = coord1.latitude.toFixed(precision);
77
+ const lat2 = coord2.latitude.toFixed(precision);
78
+ const lon1 = coord1.longitude.toFixed(precision);
79
+ const lon2 = coord2.longitude.toFixed(precision);
80
+
81
+ return lat1 === lat2 && lon1 === lon2;
82
+ }
83
+ }
@@ -0,0 +1,75 @@
1
+ import { useEffect, useRef, useState, useCallback } from "react";
2
+ import { createLocationWatcher } from "../../infrastructure/services/LocationWatcher";
3
+ import { LocationData, LocationError, LocationWatcherOptions } from "../../types/location.types";
4
+
5
+ export interface UseLocationWatchResult {
6
+ location: LocationData | null;
7
+ error: LocationError | null;
8
+ isWatching: boolean;
9
+ startWatching: () => Promise<void>;
10
+ stopWatching: () => void;
11
+ }
12
+
13
+ export function useLocationWatch(options?: LocationWatcherOptions): UseLocationWatchResult {
14
+ const watcherRef = useRef<ReturnType<typeof createLocationWatcher> | null>(null);
15
+ const [location, setLocation] = useState<LocationData | null>(null);
16
+ const [error, setError] = useState<LocationError | null>(null);
17
+ const [isWatching, setIsWatching] = useState(false);
18
+
19
+ const stopWatching = useCallback(() => {
20
+ if (watcherRef.current) {
21
+ watcherRef.current.clearWatch();
22
+ watcherRef.current = null;
23
+ setIsWatching(false);
24
+ }
25
+ }, []);
26
+
27
+ const startWatching = useCallback(async () => {
28
+ stopWatching();
29
+
30
+ const watcher = createLocationWatcher(options);
31
+ watcherRef.current = watcher;
32
+
33
+ try {
34
+ await watcher.watchPosition(
35
+ (data) => {
36
+ setLocation(data);
37
+ setError(null);
38
+ },
39
+ (err) => {
40
+ setError(err);
41
+ }
42
+ );
43
+ 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
+ }
60
+ }, [options, stopWatching]);
61
+
62
+ useEffect(() => {
63
+ return () => {
64
+ stopWatching();
65
+ };
66
+ }, [stopWatching]);
67
+
68
+ return {
69
+ location,
70
+ error,
71
+ isWatching,
72
+ startWatching,
73
+ stopWatching,
74
+ };
75
+ }
@@ -29,6 +29,15 @@ export interface CachedLocationData {
29
29
  timestamp: number;
30
30
  }
31
31
 
32
+ export type DistanceUnit = "km" | "miles" | "meters";
33
+
34
+ export type LocationErrorCode =
35
+ | "PERMISSION_DENIED"
36
+ | "LOCATION_UNAVAILABLE"
37
+ | "TIMEOUT"
38
+ | "CACHE_ERROR"
39
+ | "UNKNOWN_ERROR";
40
+
32
41
  export interface LocationConfig {
33
42
  accuracy?: Location.Accuracy;
34
43
  timeout?: number;
@@ -36,6 +45,7 @@ export interface LocationConfig {
36
45
  cacheKey?: string;
37
46
  cacheDuration?: number;
38
47
  withAddress?: boolean;
48
+ distanceFilter?: number;
39
49
  }
40
50
 
41
51
  export const DEFAULT_LOCATION_CONFIG: LocationConfig = {
@@ -45,14 +55,17 @@ export const DEFAULT_LOCATION_CONFIG: LocationConfig = {
45
55
  cacheKey: "default",
46
56
  cacheDuration: 300000,
47
57
  withAddress: true,
58
+ distanceFilter: 10,
48
59
  };
49
60
 
50
- export type LocationErrorCode =
51
- | "PERMISSION_DENIED"
52
- | "LOCATION_UNAVAILABLE"
53
- | "TIMEOUT"
54
- | "CACHE_ERROR"
55
- | "UNKNOWN_ERROR";
61
+ export type LocationCallback = (location: LocationData) => void;
62
+ export type LocationErrorCallback = (error: LocationError) => void;
63
+
64
+ export interface LocationWatcherOptions {
65
+ accuracy?: Location.Accuracy;
66
+ distanceFilter?: number;
67
+ timeout?: number;
68
+ }
56
69
 
57
70
  export class LocationErrorImpl extends Error implements LocationError {
58
71
  code: LocationErrorCode;