import React, { useEffect, useRef, useState } from 'react'; import { ActivityIndicator, Alert, Linking, Platform, Pressable, StyleSheet, Text, View, } from 'react-native'; import MapView, { Marker, PROVIDER_GOOGLE, Region } from 'react-native-maps'; import * as Location from 'expo-location'; import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { useT } from '@/i18n'; import { AppBar } from '@/components/AppBar'; import { Button } from '@/components/ui'; import { ensureGeocodePermission, ensureLocationPermission, openPermissionSettings, } from '@/lib/permissions'; import { setPendingLocation } from '@/lib/pendingLocation'; import { colors, fontSize, radii, shadow, spacing } from '@/theme'; /** Fallback view when the property has no pin yet: Milan. */ const DEFAULT_REGION: Region = { latitude: 45.4642, longitude: 9.19, latitudeDelta: 0.08, longitudeDelta: 0.08, }; export default function LocationPickerScreen() { const router = useRouter(); const t = useT(); const params = useLocalSearchParams<{ lat?: string; lon?: string; address?: string }>(); const mapRef = useRef(null); const initial = params.lat && params.lon ? { latitude: Number(params.lat), longitude: Number(params.lon) } : null; const [pin, setPin] = useState<{ latitude: number; longitude: number } | null>(initial); const [label, setLabel] = useState(null); const [place, setPlace] = useState(null); const [busy, setBusy] = useState<'gps' | 'address' | null>(null); const region: Region = pin ? { ...pin, latitudeDelta: 0.01, longitudeDelta: 0.01 } : DEFAULT_REGION; // With no pin yet, derive one from the address already typed in the form. // Silent: runs only if permission is already granted. useEffect(() => { if (pin || !params.address) return; (async () => { if (await ensureGeocodePermission(false)) { void locateFromAddress(params.address as string, true); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (!pin) return; let cancelled = false; (async () => { try { // The label is a nicety, so stay silent without permission. if (!(await ensureGeocodePermission(false))) return; const [found] = await Location.reverseGeocodeAsync(pin); if (cancelled || !found) return; setPlace(found); setLabel( [found.street, found.streetNumber, found.postalCode, found.city] .filter(Boolean) .join(', ') || null, ); } catch { // reverse geocoding is a nicety, never a blocker } })(); return () => { cancelled = true; }; }, [pin?.latitude, pin?.longitude]); function moveTo(next: { latitude: number; longitude: number }) { setPin(next); setPlace(null); // stale until the new point is reverse-geocoded setLabel(null); mapRef.current?.animateToRegion({ ...next, latitudeDelta: 0.01, longitudeDelta: 0.01 }, 400); } async function locateFromAddress(address: string, silent = false) { // User-initiated, so prompting here is expected. if (!(await ensureGeocodePermission(!silent))) { if (!silent) openPermissionSettings('permissions.locationBody'); return; } setBusy('address'); try { const [hit] = await Location.geocodeAsync(address); if (hit) moveTo({ latitude: hit.latitude, longitude: hit.longitude }); else if (!silent) Alert.alert(t('location.notFound'), t('location.notFoundBody')); } catch (e: any) { if (!silent) Alert.alert(t('location.notFound'), e?.message ?? t('common.tryAgain')); } finally { setBusy(null); } } async function useCurrentPosition() { setBusy('gps'); try { // With location off device-wide, iOS rejects the request without a // dialog and the app has no row in its own settings — send them elsewhere. if (!(await Location.hasServicesEnabledAsync())) { Alert.alert(t('location.servicesOff'), t('location.servicesOffBody'), [ { text: t('common.cancel'), style: 'cancel' }, { text: t('permissions.openSettings'), onPress: () => void Linking.openSettings() }, ]); return; } if (!(await ensureLocationPermission(true))) { openPermissionSettings('permissions.locationBody'); return; } // A cold GPS fix can hang indefinitely; fall back to the last known one. const pos = (await Promise.race([ Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced }), new Promise((resolve) => setTimeout(() => resolve(null), 12000)), ])) ?? (await Location.getLastKnownPositionAsync()); if (!pos) { Alert.alert(t('location.gpsFailed'), t('common.tryAgain')); return; } moveTo({ latitude: pos.coords.latitude, longitude: pos.coords.longitude }); } catch (e: any) { Alert.alert(t('location.gpsFailed'), e?.message ?? t('common.tryAgain')); } finally { setBusy(null); } } function confirm() { if (!pin) return; // Street and city travel back too, so the form can offer to fill them in. setPendingLocation({ ...pin, address: place ? [place.street, place.streetNumber].filter(Boolean).join(' ') || place.name || null : null, postalCode: place?.postalCode ?? null, city: place?.city ?? null, country: place?.country ?? null, }); router.back(); } return ( setPin(e.nativeEvent.coordinate)} > {pin ? ( setPin(e.nativeEvent.coordinate)} pinColor={colors.teal600} /> ) : null} {!pin ? ( {t('location.tapHint')} ) : null} {busy === 'gps' ? ( ) : ( )} {pin ? `${pin.latitude.toFixed(6)}, ${pin.longitude.toFixed(6)}` : t('location.noPin')} {label ? ( {label} ) : null} {params.address ? (