@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,13 +1,18 @@
1
+ /**
2
+ * Infrastructure Layer - Location Service
3
+ *
4
+ * Refactored location service using domain services and ports.
5
+ * DRY principle - no code duplication.
6
+ */
7
+
1
8
  import * as Location from "expo-location";
2
- import { storageRepository, unwrap } from "@umituz/react-native-design-system/storage";
3
- import {
4
- LocationData,
5
- LocationAddress,
6
- LocationConfig,
7
- LocationError,
8
- LocationErrorCode,
9
- } from "../../types/location.types";
10
- import { LocationUtils } from "../utils/LocationUtils";
9
+ import { LocationData, LocationAddress, LocationConfig } from "../../types/location.types";
10
+ import { ILocationPort } from "../../application/ports/ILocationPort";
11
+ import { ICachePort } from "../../application/ports/ICachePort";
12
+ import { ILoggerPort } from "../../application/ports/ILoggerPort";
13
+ import { PermissionService } from "../../domain/services/PermissionService";
14
+ import { LocationErrors } from "../../domain/errors/LocationErrors";
15
+ import { CacheUtils } from "../repositories/LocationCache.repository";
11
16
 
12
17
  interface CachedLocationData {
13
18
  location: LocationData;
@@ -23,67 +28,20 @@ const DEFAULT_CONFIG: Required<LocationConfig> = {
23
28
  withAddress: true,
24
29
  };
25
30
 
26
- export class LocationService {
31
+ export class LocationService implements ILocationPort {
27
32
  private config: Required<LocationConfig>;
28
33
  private inFlightRequest: Promise<LocationData> | null = null;
29
34
 
30
- constructor(config: LocationConfig = {}) {
35
+ constructor(
36
+ private cache: ICachePort,
37
+ private logger: ILoggerPort,
38
+ config: LocationConfig = {}
39
+ ) {
31
40
  this.config = { ...DEFAULT_CONFIG, ...config };
32
41
  }
33
42
 
34
- async requestPermissions(): Promise<boolean> {
35
- try {
36
- const { status: current } = await Location.getForegroundPermissionsAsync();
37
- if (current === "granted") return true;
38
-
39
- if (__DEV__) console.log("[LocationService] Requesting permissions...");
40
- const { status } = await Location.requestForegroundPermissionsAsync();
41
- return status === "granted";
42
- } catch (error) {
43
- if (__DEV__) console.error("[LocationService] Error requesting permissions:", error);
44
- return false;
45
- }
46
- }
47
-
48
- private getCacheKey(): string {
49
- return LocationUtils.generateCacheKey(this.config.cacheKey, this.config.withAddress);
50
- }
51
-
52
- private async getCachedLocation(): Promise<LocationData | null> {
53
- if (!this.config.enableCache) return null;
54
-
55
- try {
56
- const cacheKey = this.getCacheKey();
57
- const result = await storageRepository.getItem<CachedLocationData | null>(cacheKey, null);
58
- const cached = unwrap(result, null);
59
-
60
- if (!cached) return null;
61
-
62
- if (LocationUtils.isCacheExpired(cached.timestamp, this.config.cacheDuration)) {
63
- await storageRepository.removeItem(cacheKey);
64
- return null;
65
- }
66
-
67
- if (__DEV__) console.log("[LocationService] Using cached location");
68
- return cached.location;
69
- } catch (error) {
70
- if (__DEV__) console.error("[LocationService] Cache read error:", error);
71
- return null;
72
- }
73
- }
74
-
75
- private async cacheLocation(location: LocationData): Promise<void> {
76
- if (!this.config.enableCache) return;
77
-
78
- try {
79
- const data: CachedLocationData = { location, timestamp: Date.now() };
80
- await storageRepository.setItem(this.getCacheKey(), data);
81
- } catch (error) {
82
- if (__DEV__) console.error("[LocationService] Cache write error:", error);
83
- }
84
- }
85
-
86
43
  async getCurrentPosition(): Promise<LocationData> {
44
+ // Request deduplication
87
45
  if (this.inFlightRequest) {
88
46
  return this.inFlightRequest;
89
47
  }
@@ -97,42 +55,65 @@ export class LocationService {
97
55
  }
98
56
 
99
57
  private async fetchPosition(): Promise<LocationData> {
58
+ // Cache check
100
59
  const cached = await this.getCachedLocation();
101
60
  if (cached) return cached;
102
61
 
103
- const hasPermission = await this.requestPermissions();
62
+ // Permission check
63
+ const hasPermission = await PermissionService.request();
104
64
  if (!hasPermission) {
105
- throw this.createError("PERMISSION_DENIED", "Location permission not granted");
65
+ throw LocationErrors.permissionDenied();
106
66
  }
107
67
 
108
- try {
109
- const location = await this.getPositionWithTimeout();
68
+ // Fetch with timeout
69
+ const location = await this.getPositionWithTimeout();
110
70
 
111
- let address: LocationAddress | undefined;
112
- if (this.config.withAddress) {
113
- address = await this.reverseGeocode(
114
- location.coords.latitude,
115
- location.coords.longitude,
116
- );
117
- }
71
+ // Optional reverse geocoding
72
+ let address: LocationAddress | undefined;
73
+ if (this.config.withAddress) {
74
+ address = await this.reverseGeocode(
75
+ location.coords.latitude,
76
+ location.coords.longitude
77
+ );
78
+ }
118
79
 
119
- const locationData: LocationData = {
120
- coords: {
121
- latitude: location.coords.latitude,
122
- longitude: location.coords.longitude,
123
- },
124
- timestamp: location.timestamp,
125
- address,
126
- };
80
+ const locationData: LocationData = {
81
+ coords: {
82
+ latitude: location.coords.latitude,
83
+ longitude: location.coords.longitude,
84
+ },
85
+ timestamp: location.timestamp,
86
+ address,
87
+ };
88
+
89
+ await this.cacheLocation(locationData);
90
+ return locationData;
91
+ }
127
92
 
128
- await this.cacheLocation(locationData);
129
- return locationData;
130
- } catch (error) {
131
- if (__DEV__) console.error("[LocationService] Error getting location:", error);
93
+ private async getCachedLocation(): Promise<LocationData | null> {
94
+ if (!this.config.enableCache) return null;
95
+
96
+ const cacheKey = CacheUtils.generateKey(this.config.cacheKey, this.config.withAddress);
97
+ const cached = await this.cache.get(cacheKey);
98
+
99
+ if (!cached) return null;
132
100
 
133
- const message = error instanceof Error ? error.message : "Unknown error getting location";
134
- throw this.createError("UNKNOWN_ERROR", message);
101
+ if (CacheUtils.isExpired(cached.timestamp, this.config.cacheDuration)) {
102
+ await this.cache.remove(cacheKey);
103
+ return null;
135
104
  }
105
+
106
+ this.logger.debug("Using cached location");
107
+ return cached.location;
108
+ }
109
+
110
+ private async cacheLocation(location: LocationData): Promise<void> {
111
+ if (!this.config.enableCache) return;
112
+
113
+ const cacheKey = CacheUtils.generateKey(this.config.cacheKey, this.config.withAddress);
114
+ const data: CachedLocationData = { location, timestamp: Date.now() };
115
+
116
+ await this.cache.set(cacheKey, data);
136
117
  }
137
118
 
138
119
  private async getPositionWithTimeout(): Promise<Location.LocationObject> {
@@ -144,17 +125,18 @@ export class LocationService {
144
125
 
145
126
  const timeoutPromise = new Promise<never>((_, reject) => {
146
127
  timeoutId = setTimeout(() => {
147
- reject(this.createError("TIMEOUT", `Location request timed out after ${this.config.timeout}ms`));
128
+ reject(
129
+ LocationErrors.timeout(
130
+ `Location request timed out after ${this.config.timeout}ms`
131
+ )
132
+ );
148
133
  }, this.config.timeout);
149
134
  });
150
135
 
151
136
  try {
152
- const result = await Promise.race([locationPromise, timeoutPromise]);
153
- return result;
137
+ return await Promise.race([locationPromise, timeoutPromise]);
154
138
  } finally {
155
- if (timeoutId !== null) {
156
- clearTimeout(timeoutId);
157
- }
139
+ if (timeoutId !== null) clearTimeout(timeoutId);
158
140
  }
159
141
  }
160
142
 
@@ -163,30 +145,35 @@ export class LocationService {
163
145
  const [address] = await Location.reverseGeocodeAsync({ latitude, longitude });
164
146
  if (!address) return undefined;
165
147
 
166
- return LocationUtils.buildFormattedAddress(address);
148
+ return this.buildAddress(address);
167
149
  } catch (error) {
168
- if (__DEV__) console.error("[LocationService] Reverse geocode failed:", error);
150
+ this.logger.error("Reverse geocode failed", error);
169
151
  return undefined;
170
152
  }
171
153
  }
172
154
 
155
+ private buildAddress(
156
+ address: Pick<Location.LocationGeocodedAddress, "street" | "city" | "region" | "country">
157
+ ): LocationAddress {
158
+ const parts = [address.street, address.city, address.region, address.country].filter(
159
+ Boolean
160
+ ) as string[];
161
+
162
+ return {
163
+ city: address.city ?? null,
164
+ region: address.region ?? null,
165
+ country: address.country ?? null,
166
+ street: address.street ?? null,
167
+ formattedAddress: parts.length > 0 ? parts.join(", ") : null,
168
+ };
169
+ }
170
+
173
171
  async isLocationEnabled(): Promise<boolean> {
174
- try {
175
- return await Location.hasServicesEnabledAsync();
176
- } catch (error) {
177
- if (__DEV__) console.error("[LocationService] Error checking location enabled:", error);
178
- return false;
179
- }
172
+ return PermissionService.areServicesEnabled();
180
173
  }
181
174
 
182
- async getPermissionStatus(): Promise<Location.PermissionStatus> {
183
- try {
184
- const { status } = await Location.getForegroundPermissionsAsync();
185
- return status;
186
- } catch (error) {
187
- if (__DEV__) console.error("[LocationService] Error getting permission status:", error);
188
- return Location.PermissionStatus.UNDETERMINED;
189
- }
175
+ async getPermissionStatus(): Promise<string> {
176
+ return await PermissionService.getStatus();
190
177
  }
191
178
 
192
179
  async getLastKnownPosition(): Promise<LocationData | null> {
@@ -202,15 +189,8 @@ export class LocationService {
202
189
  timestamp: location.timestamp,
203
190
  };
204
191
  } catch (error) {
205
- if (__DEV__) console.error("[LocationService] Error getting last known position:", error);
192
+ this.logger.error("Error getting last known position", error);
206
193
  return null;
207
194
  }
208
195
  }
209
-
210
- private createError(code: LocationErrorCode, message: string): LocationError & Error {
211
- const error = new Error(message) as Error & LocationError;
212
- error.name = "LocationError";
213
- error.code = code;
214
- return error;
215
- }
216
196
  }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Infrastructure Layer - Location Watcher Service
3
+ *
4
+ * Refactored watcher using domain services.
5
+ * Shares permission logic with LocationService (DRY).
6
+ */
7
+
8
+ import * as Location from "expo-location";
9
+ import { LocationData, LocationWatcherOptions } from "../../types/location.types";
10
+ import { ILocationWatcherPort } from "../../application/ports/ILocationPort";
11
+ import { ILoggerPort } from "../../application/ports/ILoggerPort";
12
+ import { PermissionService } from "../../domain/services/PermissionService";
13
+ import { LocationErrors } from "../../domain/errors/LocationErrors";
14
+
15
+ const DEFAULT_OPTIONS: Required<LocationWatcherOptions> = {
16
+ accuracy: Location.Accuracy.Balanced,
17
+ distanceInterval: 10,
18
+ timeInterval: 10000,
19
+ };
20
+
21
+ export class LocationWatcher implements ILocationWatcherPort {
22
+ private subscription: Location.LocationSubscription | null = null;
23
+ private options: Required<LocationWatcherOptions>;
24
+
25
+ constructor(
26
+ private logger: ILoggerPort,
27
+ options: LocationWatcherOptions = {}
28
+ ) {
29
+ this.options = { ...DEFAULT_OPTIONS, ...options };
30
+ }
31
+
32
+ async watchPosition(
33
+ onSuccess: (location: LocationData) => void,
34
+ onError?: (error: Error) => void
35
+ ): Promise<void> {
36
+ this.clearWatch();
37
+
38
+ // Permission check
39
+ const hasPermission = await PermissionService.request();
40
+ if (!hasPermission) {
41
+ const error = LocationErrors.permissionDenied();
42
+ onError?.(error);
43
+ return;
44
+ }
45
+
46
+ try {
47
+ this.subscription = await Location.watchPositionAsync(
48
+ {
49
+ accuracy: this.options.accuracy,
50
+ distanceInterval: this.options.distanceInterval,
51
+ timeInterval: this.options.timeInterval,
52
+ },
53
+ (location) => {
54
+ onSuccess(this.mapToLocationData(location));
55
+ }
56
+ );
57
+
58
+ this.logger.debug("Started watching location");
59
+ } catch (error) {
60
+ const locationError = this.handleError(error);
61
+ onError?.(locationError);
62
+ }
63
+ }
64
+
65
+ clearWatch(): void {
66
+ if (this.subscription) {
67
+ this.logger.debug("Clearing location watch");
68
+ this.subscription.remove();
69
+ this.subscription = null;
70
+ }
71
+ }
72
+
73
+ isWatching(): boolean {
74
+ return this.subscription !== null;
75
+ }
76
+
77
+ private mapToLocationData(location: Location.LocationObject): LocationData {
78
+ return {
79
+ coords: {
80
+ latitude: location.coords.latitude,
81
+ longitude: location.coords.longitude,
82
+ },
83
+ timestamp: location.timestamp,
84
+ };
85
+ }
86
+
87
+ private handleError(error: unknown): Error {
88
+ this.logger.error("Error watching position", error);
89
+
90
+ const code = LocationErrors.extractErrorCode(error);
91
+ const message = LocationErrors.extractMessage(error);
92
+
93
+ return LocationErrors.unknown(message);
94
+ }
95
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Infrastructure Layer - Accuracy Utilities
3
+ *
4
+ * Accuracy formatting and validation utilities.
5
+ * Extracted from LocationUtils for single responsibility.
6
+ */
7
+
8
+ export class AccuracyUtils {
9
+ private static readonly EXCELLENT_THRESHOLD = 10;
10
+ private static readonly GOOD_THRESHOLD = 50;
11
+ private static readonly FAIR_THRESHOLD = 100;
12
+ private static readonly DEFAULT_MAX_ACCURACY = 100;
13
+
14
+ /**
15
+ * Accuracy değerini formatla
16
+ */
17
+ static format(accuracy: number | null | undefined): string {
18
+ if (accuracy === null || accuracy === undefined) {
19
+ return "Unknown";
20
+ }
21
+
22
+ if (accuracy < this.EXCELLENT_THRESHOLD) {
23
+ return `Excellent (±${Math.round(accuracy)}m)`;
24
+ }
25
+ if (accuracy < this.GOOD_THRESHOLD) {
26
+ return `Good (±${Math.round(accuracy)}m)`;
27
+ }
28
+ if (accuracy < this.FAIR_THRESHOLD) {
29
+ return `Fair (±${Math.round(accuracy)}m)`;
30
+ }
31
+ return `Poor (±${Math.round(accuracy)}m)`;
32
+ }
33
+
34
+ /**
35
+ * Accuracy değeri geçerli mi?
36
+ */
37
+ static isValid(
38
+ accuracy: number | null | undefined,
39
+ maxAccuracy = this.DEFAULT_MAX_ACCURACY
40
+ ): boolean {
41
+ if (accuracy === null || accuracy === undefined) {
42
+ return false;
43
+ }
44
+
45
+ if (!Number.isFinite(accuracy)) {
46
+ return false;
47
+ }
48
+
49
+ return accuracy > 0 && accuracy <= maxAccuracy;
50
+ }
51
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Infrastructure Layer - Object Comparison Utilities
3
+ *
4
+ * Deep comparison utilities for React hooks.
5
+ * Used for stable reference generation.
6
+ */
7
+
8
+ export class ObjectComparison {
9
+ /**
10
+ * İki objeyi derinlemesine karşılaştır
11
+ */
12
+ static deepEqual(obj1: unknown, obj2: unknown): boolean {
13
+ return JSON.stringify(obj1) === JSON.stringify(obj2);
14
+ }
15
+
16
+ /**
17
+ * Stable string referans oluştur
18
+ */
19
+ static getStableReference(obj: unknown): string {
20
+ return JSON.stringify(obj ?? {});
21
+ }
22
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Presentation Layer - useLocation Hook
3
+ *
4
+ * Refactored using domain errors and ports.
5
+ * Simplified error handling using LocationErrors.
6
+ */
7
+
8
+ import { useState, useCallback, useRef, useEffect } from "react";
9
+ import { LocationService } from "../../infrastructure/services/LocationService";
10
+ import { LocationCacheRepository } from "../../infrastructure/repositories/LocationCache.repository";
11
+ import { LocationLogger } from "../../infrastructure/services/LocationLoggerService";
12
+ import { LocationData, LocationError, LocationConfig } from "../../types/location.types";
13
+ import { LocationErrors } from "../../domain/errors/LocationErrors";
14
+ import { ILoggerPort } from "../../application/ports/ILoggerPort";
15
+
16
+ // Default logger instance
17
+ const defaultLogger: ILoggerPort = new LocationLogger("useLocation");
18
+
19
+ export interface UseLocationResult {
20
+ location: LocationData | null;
21
+ isLoading: boolean;
22
+ error: LocationError | null;
23
+ getCurrentLocation: () => Promise<LocationData | null>;
24
+ }
25
+
26
+ /**
27
+ * Stable reference generator for config comparison
28
+ */
29
+ function getStableReference(obj: unknown): string {
30
+ return JSON.stringify(obj ?? {});
31
+ }
32
+
33
+ export function useLocation(
34
+ config?: LocationConfig,
35
+ logger: ILoggerPort = defaultLogger
36
+ ): UseLocationResult {
37
+ // Stable references for config and service
38
+ const configStringRef = useRef<string>(getStableReference(config));
39
+ const serviceRef = useRef<LocationService | null>(null);
40
+
41
+ // Update service when config changes
42
+ const currentConfigString = getStableReference(config);
43
+ if (configStringRef.current !== currentConfigString) {
44
+ configStringRef.current = currentConfigString;
45
+ serviceRef.current = new LocationService(
46
+ new LocationCacheRepository(),
47
+ logger,
48
+ config
49
+ );
50
+ }
51
+
52
+ // Initialize service on first render
53
+ if (!serviceRef.current) {
54
+ serviceRef.current = new LocationService(new LocationCacheRepository(), logger, config);
55
+ }
56
+
57
+ const [location, setLocation] = useState<LocationData | null>(null);
58
+ const [isLoading, setIsLoading] = useState(false);
59
+ const [error, setError] = useState<LocationError | null>(null);
60
+ const mountedRef = useRef(true);
61
+
62
+ useEffect(() => {
63
+ mountedRef.current = true;
64
+ return () => {
65
+ mountedRef.current = false;
66
+ };
67
+ }, []);
68
+
69
+ const getCurrentLocation = useCallback(async () => {
70
+ if (!serviceRef.current) return null;
71
+
72
+ setIsLoading(true);
73
+ setError(null);
74
+
75
+ try {
76
+ const data = await serviceRef.current.getCurrentPosition();
77
+ if (mountedRef.current) {
78
+ setLocation(data);
79
+ }
80
+ return data;
81
+ } catch (err) {
82
+ const locationError: LocationError = {
83
+ code: LocationErrors.extractErrorCode(err),
84
+ message: LocationErrors.extractMessage(err),
85
+ };
86
+
87
+ if (mountedRef.current) {
88
+ setError(locationError);
89
+ }
90
+ return null;
91
+ } finally {
92
+ if (mountedRef.current) {
93
+ setIsLoading(false);
94
+ }
95
+ }
96
+ }, [configStringRef]);
97
+
98
+ return { location, isLoading, error, getCurrentLocation };
99
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Presentation Layer - useLocationWatch Hook
3
+ *
4
+ * Refactored using LocationWatcher and domain services.
5
+ * Simplified state management.
6
+ */
7
+
8
+ import { useEffect, useRef, useState, useCallback } from "react";
9
+ import { LocationWatcher } from "../../infrastructure/services/LocationWatcherService";
10
+ import { LocationLogger } from "../../infrastructure/services/LocationLoggerService";
11
+ import { LocationData, LocationError, LocationWatcherOptions } from "../../types/location.types";
12
+ import { ILoggerPort } from "../../application/ports/ILoggerPort";
13
+ import { LocationErrors } from "../../domain/errors/LocationErrors";
14
+
15
+ // Default logger instance
16
+ const defaultLogger: ILoggerPort = new LocationLogger("useLocationWatch");
17
+
18
+ export interface UseLocationWatchResult {
19
+ location: LocationData | null;
20
+ error: LocationError | null;
21
+ isWatching: boolean;
22
+ startWatching: () => Promise<void>;
23
+ stopWatching: () => void;
24
+ }
25
+
26
+ /**
27
+ * Stable reference generator for options comparison
28
+ */
29
+ function getStableReference(obj: unknown): string {
30
+ return JSON.stringify(obj ?? {});
31
+ }
32
+
33
+ export function useLocationWatch(
34
+ options?: LocationWatcherOptions,
35
+ logger: ILoggerPort = defaultLogger
36
+ ): UseLocationWatchResult {
37
+ // Stable reference for options
38
+ const optionsStringRef = useRef<string>(getStableReference(options));
39
+ const watcherRef = useRef<LocationWatcher | null>(null);
40
+
41
+ // State
42
+ const [location, setLocation] = useState<LocationData | null>(null);
43
+ const [error, setError] = useState<LocationError | null>(null);
44
+ const [isWatching, setIsWatching] = useState(false);
45
+
46
+ // Initialize watcher on first render
47
+ if (!watcherRef.current) {
48
+ watcherRef.current = new LocationWatcher(logger, options);
49
+ }
50
+
51
+ const stopWatching = useCallback(() => {
52
+ if (watcherRef.current) {
53
+ watcherRef.current.clearWatch();
54
+ setIsWatching(false);
55
+ }
56
+ }, []);
57
+
58
+ const startWatching = useCallback(async () => {
59
+ if (!watcherRef.current) return;
60
+
61
+ stopWatching();
62
+ setError(null);
63
+
64
+ // Recreate watcher if options changed
65
+ const currentOptionsString = getStableReference(options);
66
+ if (optionsStringRef.current !== currentOptionsString) {
67
+ optionsStringRef.current = currentOptionsString;
68
+ watcherRef.current = new LocationWatcher(logger, options);
69
+ }
70
+
71
+ await watcherRef.current.watchPosition(
72
+ (data) => {
73
+ setLocation(data);
74
+ setError(null);
75
+ },
76
+ (err) => {
77
+ const locationError: LocationError = {
78
+ code: LocationErrors.extractErrorCode(err),
79
+ message: LocationErrors.extractMessage(err),
80
+ };
81
+ setError(locationError);
82
+ setIsWatching(false);
83
+ }
84
+ );
85
+
86
+ if (watcherRef.current.isWatching()) {
87
+ setIsWatching(true);
88
+ }
89
+ }, [optionsStringRef, stopWatching, logger, options]);
90
+
91
+ useEffect(() => {
92
+ return () => {
93
+ stopWatching();
94
+ };
95
+ }, [stopWatching]);
96
+
97
+ return { location, error, isWatching, startWatching, stopWatching };
98
+ }