sg-delivery-customer/app/services/locationServices.ts

230 lines
7.3 KiB
TypeScript

import { Platform, PermissionsAndroid, Alert, Linking } from 'react-native';
import Geolocation from 'react-native-geolocation-service';
import type { GeoPosition, GeoError } from 'react-native-geolocation-service';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface LatLng {
latitude: number;
longitude: number;
}
export interface LocationResult {
coords: LatLng;
address: string;
}
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const GOOGLE_MAPS_API_KEY = 'AIzaSyDKaKVyQCmSpHMXcMkNCccSXpILpuvwsVM';
/** Default region (Bengaluru) used as a fallback before the user's location loads. */
export const DEFAULT_LOCATION: LatLng = {
latitude: 12.9352,
longitude: 77.6245,
};
// ---------------------------------------------------------------------------
// Permission helpers
// ---------------------------------------------------------------------------
/**
* Request location permission from the user.
* Returns `true` if permission was granted, `false` otherwise.
*/
export const requestLocationPermission = async (): Promise<boolean> => {
if (Platform.OS === 'ios') {
try {
const result = await Geolocation.requestAuthorization('whenInUse');
return result === 'granted';
} catch {
return false;
}
}
// Android — check first to avoid showing the dialog when already granted
try {
const alreadyGranted = await PermissionsAndroid.check(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
);
if (alreadyGranted) {
return true;
}
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Location Permission',
message:
'This app needs access to your location to set it on the map.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
},
);
console.log('granted', granted);
return granted === PermissionsAndroid.RESULTS.GRANTED;
} catch {
return false;
}
};
/**
* Show an alert when the user denies location permission, with an option
* to open app settings.
*/
export const showPermissionDeniedAlert = (): void => {
Alert.alert(
'Location Permission Required',
'Please enable location permission in your device settings to use this feature.',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
],
);
};
// ---------------------------------------------------------------------------
// Position helpers
// ---------------------------------------------------------------------------
/**
* Get the device's current GPS position.
* Wraps the callback-based Geolocation API in a Promise.
*/
/**
* Wraps Geolocation.getCurrentPosition in a Promise.
* Tries high-accuracy first; falls back to low-accuracy on failure.
*/
const getPositionOnce = (highAccuracy: boolean): Promise<LatLng> =>
new Promise<LatLng>((resolve, reject) => {
Geolocation.getCurrentPosition(
(position: GeoPosition) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});
},
(error: GeoError) => {
console.log('Geolocation error (highAccuracy=' + highAccuracy + '):', error);
reject(error);
},
{
enableHighAccuracy: highAccuracy,
timeout: highAccuracy ? 5000 : 10000,
maximumAge: 10000,
forceRequestLocation: false, // avoid native dialog that can crash
showLocationDialog: true,
},
);
});
export const getCurrentPosition = async (): Promise<LatLng> => {
console.log('getCurrentPosition called');
try {
// First attempt: high accuracy (GPS)
return await getPositionOnce(true);
} catch (highAccuracyError) {
console.warn('High-accuracy location failed, retrying with low accuracy…', highAccuracyError);
try {
// Fallback: network / cell-tower accuracy
return await getPositionOnce(false);
} catch (lowAccuracyError) {
console.error('Failed to get current position:', lowAccuracyError);
throw lowAccuracyError;
}
}
};
// ---------------------------------------------------------------------------
// Geocoding
// ---------------------------------------------------------------------------
/**
* Reverse-geocode a lat/lng pair into a human-readable address string
* using the Google Maps Geocoding API.
*/
export const reverseGeocode = async (coords: LatLng): Promise<string> => {
try {
const url =
`https://maps.googleapis.com/maps/api/geocode/json` +
`?latlng=${coords.latitude},${coords.longitude}` +
`&key=${GOOGLE_MAPS_API_KEY}`;
const response = await fetch(url);
const data = await response.json();
if (data.status === 'OK' && data.results.length > 0) {
// Return the most specific formatted address.
return data.results[0].formatted_address;
}
console.warn('Google Maps Geocoding failed:', data.status, data.error_message || '');
// Fallback to OpenStreetMap Nominatim
const osmUrl = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${coords.latitude}&lon=${coords.longitude}`;
const osmResponse = await fetch(osmUrl, {
headers: {
'User-Agent': 'SGDeliveryCustomerApp/1.0',
},
});
const osmData = await osmResponse.json();
if (osmData && osmData.display_name) {
return osmData.display_name;
}
return 'Address not found';
} catch (error) {
console.log('reverseGeocode error', error);
try {
// Fallback to OpenStreetMap Nominatim on catch
const osmUrl = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${coords.latitude}&lon=${coords.longitude}`;
const osmResponse = await fetch(osmUrl, {
headers: {
'User-Agent': 'SGDeliveryCustomerApp/1.0',
},
});
const osmData = await osmResponse.json();
if (osmData && osmData.display_name) {
return osmData.display_name;
}
} catch (osmError) {
console.log('OSM fallback error', osmError);
}
return 'Unable to fetch address';
}
};
// ---------------------------------------------------------------------------
// Combined helper
// ---------------------------------------------------------------------------
/**
* All-in-one helper: request permissions → get position → reverse geocode.
* Returns both the coordinates and the human-readable address.
*
* Throws if permission is denied or position cannot be obtained.
*/
export const getCurrentLocationWithAddress =
async (): Promise<LocationResult> => {
const hasPermission = await requestLocationPermission();
console.log('hasPermission', hasPermission);
if (!hasPermission) {
// Alert shown by the caller so it doesn't fire inside a Promise chain
// on mount (which can cause a crash on some Android versions).
throw new Error('Location permission denied');
}
const coords = await getCurrentPosition();
const address = await reverseGeocode(coords);
console.log('coords', coords);
console.log('address', address);
return { coords, address };
};