239 lines
7.4 KiB
TypeScript
239 lines
7.4 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 };
|
|
};
|