Skip to content

Commit f10260b

Browse files
committed
refactor: migrate codebase from JS to TypeScript
1 parent 3150e45 commit f10260b

File tree

7 files changed

+216
-339
lines changed

7 files changed

+216
-339
lines changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ A customizable React Native TextInput component for Google Places Autocomplete u
1212
- Custom place types filtering
1313
- RTL support
1414
- Multi-language support
15-
- TypeScript support
1615
- Session token support for reduced billing costs
1716
- Extended place details fetching (Optional)
1817
- Compatible with both Expo and non-Expo projects
@@ -34,7 +33,7 @@ npm install react-native-google-places-textinput
3433
yarn add react-native-google-places-textinput
3534
```
3635

37-
> **Note:** This package works seamlessly with both Expo and non-Expo React Native projects with no additional configuration required.
36+
> **Note:** This package is written in TypeScript and works seamlessly with both Expo and non-Expo React Native projects with no additional configuration required.
3837
3938
## Prerequisites
4039

@@ -199,7 +198,7 @@ const PlaceDetailsExample = () => {
199198
| languageCode | string | No | - | Language code (e.g., 'en', 'fr') |
200199
| includedRegionCodes | string[] | No | - | Array of region codes to filter results |
201200
| types | string[] | No | [] | Array of place types to filter |
202-
| biasPrefixText | string | No | - | Text to prepend to search query |
201+
| biasPrefixText | (text: string) => string | No | - | Optional function to modify the input text before sending to the Places API |
203202
| **Place Details Configuration** |
204203
| fetchDetails | boolean | No | false | Automatically fetch place details when a place is selected |
205204
| detailsProxyUrl | string | No | null | Custom proxy URL for place details requests (Required on Expo web)|

src/GooglePlacesTextInput.d.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/GooglePlacesTextInput.js renamed to src/GooglePlacesTextInput.tsx

Lines changed: 150 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
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';
89
import {
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+
480568
export default GooglePlacesTextInput;

0 commit comments

Comments
 (0)