1+ import * as Location from "expo-location" ;
12import { useMemo , useState } from "react" ;
2- import { Pressable , ScrollView , StyleSheet , Text , TextInput , View } from "react-native" ;
3+ import { Platform , Pressable , ScrollView , StyleSheet , Text , TextInput , View } from "react-native" ;
34import { SafeAreaView } from "react-native-safe-area-context" ;
45
56import BurgerButton from "../components/BurgerButton" ;
7+ import { geocodeAddressQuery , resolveAddressLabelForCoordinates } from "../services/geocoding" ;
68import type { FavoriteLocation } from "../state/appState" ;
79import { useAppTheme } from "../theme/useAppTheme" ;
810
@@ -17,6 +19,11 @@ function formatCoordLabel(value: number) {
1719 return value . toFixed ( 4 ) ;
1820}
1921
22+ function getErrorMessage ( err : unknown ) : string {
23+ if ( err instanceof Error && err . message ) return err . message ;
24+ return String ( err ) ;
25+ }
26+
2027export default function LocationSettingsScreen ( {
2128 onOpenMenu,
2229 favoriteLocations,
@@ -26,22 +33,78 @@ export default function LocationSettingsScreen({
2633 const { colors } = useAppTheme ( ) ;
2734 const styles = useMemo ( ( ) => createStyles ( colors ) , [ colors ] ) ;
2835 const [ name , setName ] = useState ( "" ) ;
36+ const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
2937 const [ latitudeText , setLatitudeText ] = useState ( "" ) ;
3038 const [ longitudeText , setLongitudeText ] = useState ( "" ) ;
39+ const [ isSearching , setIsSearching ] = useState ( false ) ;
40+ const [ isSavingFavorite , setIsSavingFavorite ] = useState ( false ) ;
3141 const [ errorMessage , setErrorMessage ] = useState < string | null > ( null ) ;
3242
3343 const sortedFavorites = useMemo (
3444 ( ) => [ ...favoriteLocations ] . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ,
3545 [ favoriteLocations ] ,
3646 ) ;
3747
38- const addFavorite = ( ) => {
39- const trimmedName = name . trim ( ) ;
40- if ( ! trimmedName ) {
41- setErrorMessage ( "Enter a name for the favorite location." ) ;
48+ const ensureAndroidGeocodePermission = async ( ) => {
49+ if ( Platform . OS !== "android" ) return true ;
50+
51+ const existing = await Location . getForegroundPermissionsAsync ( ) ;
52+ if ( existing . status === "granted" ) return true ;
53+ if ( ! existing . canAskAgain ) return false ;
54+
55+ const requested = await Location . requestForegroundPermissionsAsync ( ) ;
56+ return requested . status === "granted" ;
57+ } ;
58+
59+ const searchAddress = async ( ) => {
60+ const trimmedQuery = searchQuery . trim ( ) ;
61+ if ( ! trimmedQuery || isSearching ) {
62+ if ( ! trimmedQuery ) setErrorMessage ( "Enter an address or place to search." ) ;
4263 return ;
4364 }
4465
66+ setIsSearching ( true ) ;
67+ setErrorMessage ( null ) ;
68+
69+ try {
70+ const hasPermission = await ensureAndroidGeocodePermission ( ) ;
71+ if ( ! hasPermission ) {
72+ setErrorMessage ( "Location permission is required to search addresses." ) ;
73+ return ;
74+ }
75+
76+ const matches = await geocodeAddressQuery ( trimmedQuery ) ;
77+ const first = matches . find (
78+ ( item ) => Number . isFinite ( item . latitude ) && Number . isFinite ( item . longitude ) ,
79+ ) ;
80+ if ( ! first ) {
81+ setErrorMessage ( `No search results for "${ trimmedQuery } ".` ) ;
82+ return ;
83+ }
84+
85+ const lat = first . latitude ;
86+ const lon = first . longitude ;
87+ setLatitudeText ( lat . toFixed ( 6 ) ) ;
88+ setLongitudeText ( lon . toFixed ( 6 ) ) ;
89+ if ( ! name . trim ( ) ) {
90+ const resolved = await resolveAddressLabelForCoordinates ( lat , lon ) ;
91+ if ( resolved ) {
92+ setName ( resolved ) ;
93+ } else {
94+ setName ( trimmedQuery ) ;
95+ }
96+ }
97+ setErrorMessage ( null ) ;
98+ } catch ( err : unknown ) {
99+ setErrorMessage ( `Address search failed: ${ getErrorMessage ( err ) } ` ) ;
100+ } finally {
101+ setIsSearching ( false ) ;
102+ }
103+ } ;
104+
105+ const addFavorite = async ( ) => {
106+ if ( isSavingFavorite ) return ;
107+
45108 const lat = Number ( latitudeText ) ;
46109 if ( ! Number . isFinite ( lat ) || lat < - 90 || lat > 90 ) {
47110 setErrorMessage ( "Latitude must be a number between -90 and 90." ) ;
@@ -54,17 +117,37 @@ export default function LocationSettingsScreen({
54117 return ;
55118 }
56119
57- onAddFavoriteLocation ( {
58- name : trimmedName ,
59- lat,
60- lon,
61- } ) ;
62- setName ( "" ) ;
63- setLatitudeText ( "" ) ;
64- setLongitudeText ( "" ) ;
120+ setIsSavingFavorite ( true ) ;
65121 setErrorMessage ( null ) ;
122+ try {
123+ let resolvedName = name . trim ( ) ;
124+ if ( ! resolvedName ) {
125+ resolvedName = ( await resolveAddressLabelForCoordinates ( lat , lon ) ) ?? "" ;
126+ }
127+ if ( ! resolvedName ) {
128+ setErrorMessage ( "Enter a name or search for an address before saving." ) ;
129+ return ;
130+ }
131+
132+ onAddFavoriteLocation ( {
133+ name : resolvedName ,
134+ lat,
135+ lon,
136+ } ) ;
137+ setName ( "" ) ;
138+ setSearchQuery ( "" ) ;
139+ setLatitudeText ( "" ) ;
140+ setLongitudeText ( "" ) ;
141+ setErrorMessage ( null ) ;
142+ } finally {
143+ setIsSavingFavorite ( false ) ;
144+ }
66145 } ;
67146
147+ const canSearchAddress = searchQuery . trim ( ) . length > 0 && ! isSearching ;
148+ const canAddFavorite =
149+ latitudeText . trim ( ) . length > 0 && longitudeText . trim ( ) . length > 0 && ! isSavingFavorite ;
150+
68151 return (
69152 < SafeAreaView style = { styles . safe } edges = { [ "top" , "left" , "right" , "bottom" ] } >
70153 < View style = { styles . headerRow } >
@@ -78,10 +161,37 @@ export default function LocationSettingsScreen({
78161 < ScrollView contentContainerStyle = { styles . content } >
79162 < View style = { styles . formCard } >
80163 < Text style = { styles . formTitle } > Add Favorite Location</ Text >
164+ < TextInput
165+ value = { searchQuery }
166+ onChangeText = { setSearchQuery }
167+ placeholder = "Search address or place"
168+ placeholderTextColor = { colors . inputPlaceholder }
169+ style = { styles . input }
170+ autoCapitalize = "words"
171+ autoCorrect = { false }
172+ returnKeyType = "search"
173+ onSubmitEditing = { ( ) => {
174+ void searchAddress ( ) ;
175+ } }
176+ />
177+ < Pressable
178+ style = { [ styles . searchBtn , ! canSearchAddress ? styles . actionBtnDisabled : null ] }
179+ onPress = { ( ) => {
180+ void searchAddress ( ) ;
181+ } }
182+ disabled = { ! canSearchAddress }
183+ accessibilityRole = "button"
184+ accessibilityLabel = "Find coordinates for address search query"
185+ accessibilityState = { { disabled : ! canSearchAddress } }
186+ >
187+ < Text style = { styles . searchBtnText } >
188+ { isSearching ? "Searching..." : "Find Address" }
189+ </ Text >
190+ </ Pressable >
81191 < TextInput
82192 value = { name }
83193 onChangeText = { setName }
84- placeholder = "Name (e.g. Austin Home )"
194+ placeholder = "Name (optional if address resolves )"
85195 placeholderTextColor = { colors . inputPlaceholder }
86196 style = { styles . input }
87197 autoCapitalize = "words"
@@ -110,8 +220,14 @@ export default function LocationSettingsScreen({
110220 />
111221 </ View >
112222 { errorMessage ? < Text style = { styles . errorText } > { errorMessage } </ Text > : null }
113- < Pressable style = { styles . addBtn } onPress = { addFavorite } >
114- < Text style = { styles . addBtnText } > Add Favorite</ Text >
223+ < Pressable
224+ style = { [ styles . addBtn , ! canAddFavorite ? styles . actionBtnDisabled : null ] }
225+ onPress = { ( ) => {
226+ void addFavorite ( ) ;
227+ } }
228+ disabled = { ! canAddFavorite }
229+ >
230+ < Text style = { styles . addBtnText } > { isSavingFavorite ? "Saving..." : "Add Favorite" } </ Text >
115231 </ Pressable >
116232 </ View >
117233
@@ -199,6 +315,18 @@ function createStyles(colors: ReturnType<typeof useAppTheme>["colors"]) {
199315 paddingHorizontal : 12 ,
200316 fontSize : 14 ,
201317 } ,
318+ searchBtn : {
319+ borderRadius : 10 ,
320+ backgroundColor : colors . primary ,
321+ alignItems : "center" ,
322+ justifyContent : "center" ,
323+ paddingVertical : 11 ,
324+ } ,
325+ searchBtnText : {
326+ color : colors . primaryText ,
327+ fontSize : 13 ,
328+ fontWeight : "700" ,
329+ } ,
202330 coordRow : {
203331 flexDirection : "row" ,
204332 gap : 10 ,
@@ -217,6 +345,9 @@ function createStyles(colors: ReturnType<typeof useAppTheme>["colors"]) {
217345 justifyContent : "center" ,
218346 paddingVertical : 11 ,
219347 } ,
348+ actionBtnDisabled : {
349+ opacity : 0.7 ,
350+ } ,
220351 addBtnText : {
221352 color : colors . primaryText ,
222353 fontSize : 14 ,
0 commit comments