@umituz/react-native-location 1.0.27 → 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.
@@ -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
- }