1- import React , {
1+ import {
22 forwardRef ,
33 useEffect ,
44 useImperativeHandle ,
55 useRef ,
66 useState ,
77} from 'react' ;
8+ import type { StyleProp , TextStyle , ViewStyle } from 'react-native' ;
89import {
910 ActivityIndicator ,
1011 FlatList ,
@@ -26,7 +27,89 @@ import {
2627 isRTLText ,
2728} from './services/googlePlacesApi' ;
2829
29- const GooglePlacesTextInput = forwardRef (
30+ // Type definitions
31+ interface PlaceStructuredFormat {
32+ mainText : {
33+ text : string ;
34+ } ;
35+ secondaryText ?: {
36+ text : string ;
37+ } ;
38+ }
39+
40+ interface PlacePrediction {
41+ placeId : string ;
42+ structuredFormat : PlaceStructuredFormat ;
43+ types : string [ ] ;
44+ }
45+
46+ interface PlaceDetailsFields {
47+ [ key : string ] : any ;
48+ }
49+
50+ interface Place {
51+ placeId : string ;
52+ structuredFormat : PlaceStructuredFormat ;
53+ types : string [ ] ;
54+ details ?: PlaceDetailsFields ; // ✅ Optional details when fetchDetails is true
55+ }
56+
57+ interface GooglePlacesTextInputStyles {
58+ container ?: StyleProp < ViewStyle > ;
59+ input ?: StyleProp < TextStyle > ;
60+ suggestionsContainer ?: StyleProp < ViewStyle > ;
61+ suggestionsList ?: StyleProp < ViewStyle > ;
62+ suggestionItem ?: StyleProp < ViewStyle > ;
63+ suggestionText ?: {
64+ main ?: StyleProp < TextStyle > ;
65+ secondary ?: StyleProp < TextStyle > ;
66+ } ;
67+ loadingIndicator ?: {
68+ color ?: string ; // ✅ Keep as string, not StyleProp
69+ } ;
70+ placeholder ?: {
71+ color ?: string ; // ✅ Keep as string, not StyleProp
72+ } ;
73+ }
74+
75+ interface GooglePlacesTextInputProps {
76+ apiKey : string ;
77+ value ?: string ;
78+ placeHolderText ?: string ;
79+ proxyUrl ?: string ;
80+ languageCode ?: string ;
81+ includedRegionCodes ?: string [ ] ;
82+ types ?: string [ ] ;
83+ biasPrefixText ?: ( text : string ) => string ; // ✅ Function that transforms text
84+ minCharsToFetch ?: number ;
85+ onPlaceSelect : ( place : Place , sessionToken ?: string | null ) => void ; // ✅ Remove | null
86+ onTextChange ?: ( text : string ) => void ;
87+ debounceDelay ?: number ;
88+ showLoadingIndicator ?: boolean ;
89+ showClearButton ?: boolean ;
90+ forceRTL ?: boolean ;
91+ style ?: GooglePlacesTextInputStyles ;
92+ hideOnKeyboardDismiss ?: boolean ;
93+ fetchDetails ?: boolean ;
94+ detailsProxyUrl ?: string | null ; // ✅ Add | null to match JS
95+ detailsFields ?: string [ ] ;
96+ onError ?: ( error : any ) => void ;
97+ }
98+
99+ interface GooglePlacesTextInputRef {
100+ clear : ( ) => void ;
101+ focus : ( ) => void ;
102+ getSessionToken : ( ) => string | null ;
103+ }
104+
105+ interface PredictionItem {
106+ placePrediction : PlacePrediction ;
107+ }
108+
109+ const GooglePlacesTextInput = forwardRef <
110+ GooglePlacesTextInputRef ,
111+ GooglePlacesTextInputProps
112+ > (
30113 (
31114 {
32115 apiKey,
@@ -53,18 +136,18 @@ const GooglePlacesTextInput = forwardRef(
53136 } ,
54137 ref
55138 ) => {
56- const [ predictions , setPredictions ] = useState ( [ ] ) ;
57- const [ loading , setLoading ] = useState ( false ) ;
58- const [ inputText , setInputText ] = useState ( value || '' ) ;
59- const [ showSuggestions , setShowSuggestions ] = useState ( false ) ;
60- const [ sessionToken , setSessionToken ] = useState ( null ) ;
61- const [ detailsLoading , setDetailsLoading ] = useState ( false ) ;
62- const debounceTimeout = useRef ( null ) ;
63- const inputRef = useRef ( null ) ;
64- const suggestionPressing = useRef ( false ) ;
65- const skipNextFocusFetch = useRef ( false ) ;
66-
67- const generateSessionToken = ( ) => {
139+ const [ predictions , setPredictions ] = useState < PredictionItem [ ] > ( [ ] ) ;
140+ const [ loading , setLoading ] = useState < boolean > ( false ) ;
141+ const [ inputText , setInputText ] = useState < string > ( value || '' ) ;
142+ const [ showSuggestions , setShowSuggestions ] = useState < boolean > ( false ) ;
143+ const [ sessionToken , setSessionToken ] = useState < string | null > ( null ) ;
144+ const [ detailsLoading , setDetailsLoading ] = useState < boolean > ( false ) ;
145+ const debounceTimeout = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
146+ const inputRef = useRef < TextInput > ( null ) ;
147+ const suggestionPressing = useRef < boolean > ( false ) ;
148+ const skipNextFocusFetch = useRef < boolean > ( false ) ;
149+
150+ const generateSessionToken = ( ) : string => {
68151 return generateUUID ( ) ;
69152 } ;
70153
@@ -91,7 +174,6 @@ const GooglePlacesTextInput = forwardRef(
91174 keyboardDidHideSubscription . remove ( ) ;
92175 } ;
93176 }
94- // Return empty cleanup function if not using the listener
95177 return ( ) => { } ;
96178 } , [ hideOnKeyboardDismiss ] ) ;
97179
@@ -113,7 +195,21 @@ const GooglePlacesTextInput = forwardRef(
113195 getSessionToken : ( ) => sessionToken ,
114196 } ) ) ;
115197
116- const fetchPredictions = async ( text ) => {
198+ // RTL detection logic
199+ const isRTL =
200+ forceRTL !== undefined ? forceRTL : isRTLText ( placeHolderText ?? '' ) ;
201+
202+ // Add missing CORS warning effect
203+ useEffect ( ( ) => {
204+ if ( Platform . OS === 'web' && fetchDetails && ! detailsProxyUrl ) {
205+ console . warn (
206+ 'Google Places Details API does not support CORS. ' +
207+ 'To fetch place details on web, provide a detailsProxyUrl prop that points to a CORS-enabled proxy.'
208+ ) ;
209+ }
210+ } , [ fetchDetails , detailsProxyUrl ] ) ;
211+
212+ const fetchPredictions = async ( text : string ) : Promise < void > => {
117213 if ( ! text || text . length < minCharsToFetch ) {
118214 setPredictions ( [ ] ) ;
119215 return ;
@@ -144,7 +240,9 @@ const GooglePlacesTextInput = forwardRef(
144240 setLoading ( false ) ;
145241 } ;
146242
147- const fetchPlaceDetails = async ( placeId ) => {
243+ const fetchPlaceDetails = async (
244+ placeId : string
245+ ) : Promise < PlaceDetailsFields | null > => {
148246 if ( ! fetchDetails || ! placeId ) return null ;
149247
150248 setDetailsLoading ( true ) ;
@@ -168,7 +266,7 @@ const GooglePlacesTextInput = forwardRef(
168266 return details ;
169267 } ;
170268
171- const handleTextChange = ( text ) => {
269+ const handleTextChange = ( text : string ) : void => {
172270 setInputText ( text ) ;
173271 onTextChange ?.( text ) ;
174272
@@ -181,31 +279,27 @@ const GooglePlacesTextInput = forwardRef(
181279 } , debounceDelay ) ;
182280 } ;
183281
184- const handleSuggestionPress = async ( suggestion ) => {
282+ const handleSuggestionPress = async (
283+ suggestion : PredictionItem
284+ ) : Promise < void > => {
185285 const place = suggestion . placePrediction ;
186286 setInputText ( place . structuredFormat . mainText . text ) ;
187287 setShowSuggestions ( false ) ;
188288 Keyboard . dismiss ( ) ;
189289
190290 if ( fetchDetails ) {
191- // Show loading indicator while fetching details
192291 setLoading ( true ) ;
193- // Fetch the place details - Note that placeId is already in the correct format
194292 const details = await fetchPlaceDetails ( place . placeId ) ;
195- // Merge the details with the place data
196- const enrichedPlace = details ? { ...place , details } : place ;
197- // Pass both the enriched place and session token to parent
293+ const enrichedPlace : Place = details ? { ...place , details } : place ;
198294 onPlaceSelect ?.( enrichedPlace , sessionToken ) ;
199295 setLoading ( false ) ;
200296 } else {
201- // Original behavior when fetchDetails is false
202297 onPlaceSelect ?.( place , sessionToken ) ;
203298 }
204- // Generate a new token after a place is selected
205299 setSessionToken ( generateSessionToken ( ) ) ;
206300 } ;
207301
208- const handleFocus = ( ) => {
302+ const handleFocus = ( ) : void => {
209303 if ( skipNextFocusFetch . current ) {
210304 skipNextFocusFetch . current = false ;
211305 return ;
@@ -216,40 +310,34 @@ const GooglePlacesTextInput = forwardRef(
216310 }
217311 } ;
218312
219- // RTL detection logic
220- const isRTL =
221- forceRTL !== undefined ? forceRTL : isRTLText ( placeHolderText ) ;
222-
223- const renderSuggestion = ( { item } ) => {
313+ const renderSuggestion = ( { item } : { item : PredictionItem } ) => {
224314 const { mainText, secondaryText } = item . placePrediction . structuredFormat ;
225315
316+ // Safely extract backgroundColor from style
317+ const suggestionsContainerStyle = StyleSheet . flatten (
318+ style . suggestionsContainer
319+ ) ;
320+ const backgroundColor =
321+ suggestionsContainerStyle ?. backgroundColor || '#efeff1' ;
322+
226323 return (
227324 < TouchableOpacity
228325 style = { [
229326 styles . suggestionItem ,
230- // Inherit background color from container if not specified
231- {
232- backgroundColor :
233- style . suggestionsContainer ?. backgroundColor || '#efeff1' ,
234- } ,
327+ { backgroundColor } ,
235328 style . suggestionItem ,
236329 ] }
237330 onPress = { ( ) => {
238331 suggestionPressing . current = false ;
239332 handleSuggestionPress ( item ) ;
240333 } }
241334 // Fix for web: onBlur fires before onPress, hiding suggestions too early.
242- // We use suggestionPressing.current to delay hiding until selection is handled.
243- onMouseDown = { ( ) => {
244- if ( Platform . OS === 'web' ) {
245- suggestionPressing . current = true ;
246- }
247- } }
248- onTouchStart = { ( ) => {
249- if ( Platform . OS === 'web' ) {
250- suggestionPressing . current = true ;
251- }
252- } }
335+ { ...( Platform . OS === 'web' &&
336+ ( {
337+ onMouseDown : ( ) => {
338+ suggestionPressing . current = true ;
339+ } ,
340+ } as any ) ) }
253341 >
254342 < Text
255343 style = { [
@@ -293,31 +381,20 @@ const GooglePlacesTextInput = forwardRef(
293381 const getTextAlign = ( ) => {
294382 const isDeviceRTL = I18nManager . isRTL ;
295383 if ( isDeviceRTL ) {
296- // Device is RTL, so "left" and "right" are swapped
297- return { textAlign : isRTL ? 'left' : 'right' } ;
384+ return { textAlign : isRTL ? 'left' : ( 'right' as 'left' | 'right' ) } ;
298385 } else {
299- // Device is LTR, normal behavior
300- return { textAlign : isRTL ? 'right' : 'left' } ;
386+ return { textAlign : isRTL ? 'right' : ( 'left' as 'left' | 'right' ) } ;
301387 }
302388 } ;
303389
304- const getIconPosition = ( paddingValue ) => {
390+ const getIconPosition = ( paddingValue : number ) => {
305391 const physicalRTL = I18nManager . isRTL ;
306392 if ( isRTL !== physicalRTL ) {
307393 return { start : paddingValue } ;
308394 }
309395 return { end : paddingValue } ;
310396 } ;
311397
312- useEffect ( ( ) => {
313- if ( Platform . OS === 'web' && fetchDetails && ! detailsProxyUrl ) {
314- console . warn (
315- 'Google Places Details API does not support CORS. ' +
316- 'To fetch place details on web, provide a detailsProxyUrl prop that points to a CORS-enabled proxy.'
317- ) ;
318- }
319- } , [ fetchDetails , detailsProxyUrl ] ) ;
320-
321398 return (
322399 < View style = { [ styles . container , style . container ] } >
323400 < View >
@@ -383,6 +460,7 @@ const GooglePlacesTextInput = forwardRef(
383460 />
384461 ) }
385462 </ View >
463+
386464 { /* Suggestions */ }
387465 { showSuggestions && predictions . length > 0 && (
388466 < View
@@ -477,4 +555,14 @@ const styles = StyleSheet.create({
477555 } ,
478556} ) ;
479557
558+ export type {
559+ GooglePlacesTextInputProps ,
560+ GooglePlacesTextInputRef ,
561+ GooglePlacesTextInputStyles ,
562+ Place ,
563+ PlaceDetailsFields ,
564+ PlacePrediction ,
565+ PlaceStructuredFormat ,
566+ } ;
567+
480568export default GooglePlacesTextInput ;
0 commit comments