Skip to content

Commit 4b361b0

Browse files
committed
feat: add support for fetching extended place details upon place selection
1 parent d762d9f commit 4b361b0

File tree

4 files changed

+355
-79
lines changed

4 files changed

+355
-79
lines changed

README.md

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ A customizable React Native TextInput component for Google Places Autocomplete u
1414
- Multi-language support
1515
- TypeScript support
1616
- Session token support for reduced billing costs
17+
- Extended place details fetching (Optional)
1718

1819
## Preview
1920

@@ -185,6 +186,48 @@ const SessionTokenExample = () => {
185186
```
186187
</details>
187188

189+
<details>
190+
<summary>Example with Place Details Fetching</summary>
191+
192+
```javascript
193+
const PlaceDetailsExample = () => {
194+
const handlePlaceSelect = (place) => {
195+
console.log('Selected place:', place);
196+
197+
// Access detailed place information
198+
if (place.details) {
199+
console.log('Address components:', place.details.addressComponents);
200+
console.log('Geometry:', place.details.geometry);
201+
console.log('Photos:', place.details.photos);
202+
// And other fields you requested
203+
}
204+
};
205+
206+
const handleError = (error) => {
207+
console.error('API error:', error);
208+
};
209+
210+
return (
211+
<GooglePlacesTextInput
212+
apiKey="YOUR_GOOGLE_PLACES_API_KEY"
213+
onPlaceSelect={handlePlaceSelect}
214+
onError={handleError}
215+
fetchDetails={true}
216+
detailsFields={[
217+
'address_components',
218+
'formatted_address',
219+
'geometry',
220+
'viewport',
221+
'photos',
222+
'place_id',
223+
'types'
224+
]}
225+
/>
226+
);
227+
};
228+
```
229+
</details>
230+
188231
## Props
189232

190233
| Prop | Type | Required | Default | Description |
@@ -202,6 +245,10 @@ const SessionTokenExample = () => {
202245
| includedRegionCodes | string[] | No | - | Array of region codes to filter results |
203246
| types | string[] | No | [] | Array of place types to filter |
204247
| biasPrefixText | string | No | - | Text to prepend to search query |
248+
| **Place Details Configuration** |
249+
| fetchDetails | boolean | No | false | Automatically fetch place details when a place is selected |
250+
| detailsProxyUrl | string | No | null | Custom proxy URL for place details requests |
251+
| detailsFields | string[] | No | [] | Array of fields to include in the place details response |
205252
| **UI Customization** |
206253
| style | StyleProp | No | {} | Custom styles object |
207254
| showLoadingIndicator | boolean | No | true | Show/hide loading indicator |
@@ -211,25 +258,37 @@ hideOnKeyboardDismiss | boolean | No | false | Hide suggestions when keyboard is
211258
| **Event Handlers** |
212259
| onPlaceSelect | (place: Place \| null, sessionToken?: string) => void | Yes | - | Callback when place is selected |
213260
| onTextChange | (text: string) => void | No | - | Callback triggered on text input changes |
261+
| onError | (error: any) => void | No | - | Callback for handling API errors |
214262

215-
## Session Tokens and Billing
263+
## Place Details Fetching
216264

217-
This component implements automatic session token management to help reduce your Google Places API billing costs:
265+
You can automatically fetch detailed place information when a user selects a place suggestion by enabling the `fetchDetails` prop:
266+
267+
```javascript
268+
<GooglePlacesTextInput
269+
apiKey="YOUR_GOOGLE_PLACES_API_KEY"
270+
fetchDetails={true}
271+
detailsFields={['formatted_address', 'geometry', 'viewport', 'photos']}
272+
onPlaceSelect={(place) => console.log(place.details)}
273+
/>
274+
```
275+
276+
When `fetchDetails` is enabled:
277+
1. The component fetches place details immediately when a user selects a place suggestion
278+
2. The details are attached to the place object passed to your `onPlaceSelect` callback in the `details` property
279+
3. Use the `detailsFields` prop to specify which fields to include in the response, reducing API costs
280+
281+
For a complete list of available fields, see the [Place Details API documentation](https://developers.google.com/maps/documentation/places/web-service/place-details#fieldmask).
282+
283+
## Session Tokens and Billing
218284

219-
- A session token is automatically generated when the component mounts
220-
- The same token is used for all autocomplete requests in a session
221-
- When a place is selected, the token is passed to your `onPlaceSelect` callback
222-
- Session tokens are automatically reset:
223-
- After a place is selected
224-
- When the input is manually cleared using the clear button
225-
- When the `clear()` method is called programmatically
285+
This component automatically manages session tokens to optimize your Google Places API billing:
226286

227-
**How this reduces costs:**
228-
When you make a series of autocomplete requests followed by a place details request using the same session token, Google Places API charges you only once for the entire session rather than for each individual request.
287+
- A session token is generated when the component mounts
288+
- The same token is automatically used for all autocomplete requests and place details requests
289+
- The component automatically resets tokens after place selection, input clearing, or calling `clear()`
229290

230-
To benefit from this billing optimization:
231-
1. Use the session token passed to your `onPlaceSelect` handler when making subsequent place details requests
232-
2. No configuration is required - the feature works automatically
291+
**Note:** This automatic session token management ensures Google treats your autocomplete and details requests as part of the same session, reducing your billing costs with no additional configuration needed.
233292

234293
## Methods
235294

@@ -283,4 +342,4 @@ MIT
283342
284343
Written by Amit Palomo
285344
286-
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
345+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)

src/GooglePlacesTextInput.js

Lines changed: 72 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@ import {
1818
Platform,
1919
} from 'react-native';
2020

21-
const DEFAULT_GOOGLE_API_URL =
22-
'https://places.googleapis.com/v1/places:autocomplete';
21+
// Import the API functions
22+
import {
23+
fetchPredictions as fetchPredictionsApi,
24+
fetchPlaceDetails as fetchPlaceDetailsApi,
25+
generateUUID,
26+
isRTLText,
27+
} from './services/googlePlacesApi';
2328

2429
const GooglePlacesTextInput = forwardRef(
2530
(
@@ -41,6 +46,10 @@ const GooglePlacesTextInput = forwardRef(
4146
forceRTL = undefined,
4247
style = {},
4348
hideOnKeyboardDismiss = false,
49+
fetchDetails = false,
50+
detailsProxyUrl = null,
51+
detailsFields = [],
52+
onError,
4453
},
4554
ref
4655
) => {
@@ -49,6 +58,7 @@ const GooglePlacesTextInput = forwardRef(
4958
const [inputText, setInputText] = useState(value || '');
5059
const [showSuggestions, setShowSuggestions] = useState(false);
5160
const [sessionToken, setSessionToken] = useState(null);
61+
const [detailsLoading, setDetailsLoading] = useState(false);
5262
const debounceTimeout = useRef(null);
5363
const inputRef = useRef(null);
5464

@@ -103,51 +113,57 @@ const GooglePlacesTextInput = forwardRef(
103113
return;
104114
}
105115

106-
const processedText = biasPrefixText ? biasPrefixText(text) : text;
116+
setLoading(true);
107117

108-
try {
109-
setLoading(true);
110-
const API_URL = proxyUrl ? proxyUrl : DEFAULT_GOOGLE_API_URL;
111-
const headers = {
112-
'Content-Type': 'application/json',
113-
};
114-
if (apiKey || apiKey !== '') {
115-
headers['X-Goog-Api-Key'] = apiKey;
116-
}
117-
118-
const body = {
119-
input: processedText,
118+
const { error, predictions: fetchedPredictions } =
119+
await fetchPredictionsApi({
120+
text,
121+
apiKey,
122+
proxyUrl,
123+
sessionToken,
120124
languageCode,
121-
...(sessionToken && { sessionToken }),
122-
...(includedRegionCodes?.length > 0 && { includedRegionCodes }),
123-
...(types.length > 0 && { includedPrimaryTypes: types }),
124-
};
125-
126-
const response = await fetch(API_URL, {
127-
method: 'POST',
128-
headers,
129-
body: JSON.stringify(body),
125+
includedRegionCodes,
126+
types,
127+
biasPrefixText,
130128
});
131129

132-
const data = await response.json();
133-
134-
if (data.suggestions) {
135-
setPredictions(data.suggestions);
136-
setShowSuggestions(true);
137-
} else {
138-
setPredictions([]);
139-
}
140-
} catch (error) {
141-
console.error('Error fetching predictions:', error);
130+
if (error) {
131+
onError?.(error);
142132
setPredictions([]);
143-
} finally {
144-
setLoading(false);
133+
} else {
134+
setPredictions(fetchedPredictions);
135+
setShowSuggestions(fetchedPredictions.length > 0);
136+
}
137+
138+
setLoading(false);
139+
};
140+
141+
const fetchPlaceDetails = async (placeId) => {
142+
if (!fetchDetails || !placeId) return null;
143+
144+
setDetailsLoading(true);
145+
146+
const { error, details } = await fetchPlaceDetailsApi({
147+
placeId,
148+
apiKey,
149+
detailsProxyUrl,
150+
sessionToken,
151+
languageCode,
152+
detailsFields,
153+
});
154+
155+
setDetailsLoading(false);
156+
157+
if (error) {
158+
onError?.(error);
159+
return null;
145160
}
161+
162+
return details;
146163
};
147164

148165
const handleTextChange = (text) => {
149166
setInputText(text);
150-
onPlaceSelect(null);
151167
onTextChange?.(text);
152168

153169
if (debounceTimeout.current) {
@@ -159,14 +175,29 @@ const GooglePlacesTextInput = forwardRef(
159175
}, debounceDelay);
160176
};
161177

162-
const handleSuggestionPress = (suggestion) => {
178+
const handleSuggestionPress = async (suggestion) => {
163179
const place = suggestion.placePrediction;
164180
setInputText(place.structuredFormat.mainText.text);
165181
setShowSuggestions(false);
166182
Keyboard.dismiss();
167183

168-
// Pass both the place and session token to parent
169-
onPlaceSelect?.(place, sessionToken);
184+
if (fetchDetails) {
185+
// Show loading indicator while fetching details
186+
setLoading(true);
187+
188+
// Fetch the place details - Note that placeId is already in the correct format
189+
const details = await fetchPlaceDetails(place.placeId);
190+
191+
// Merge the details with the place data
192+
const enrichedPlace = details ? { ...place, details } : place;
193+
194+
// Pass both the enriched place and session token to parent
195+
onPlaceSelect?.(enrichedPlace, sessionToken);
196+
setLoading(false);
197+
} else {
198+
// Original behavior when fetchDetails is false
199+
onPlaceSelect?.(place, sessionToken);
200+
}
170201

171202
// Generate a new token after a place is selected
172203
setSessionToken(generateSessionToken());
@@ -305,7 +336,6 @@ const GooglePlacesTextInput = forwardRef(
305336
setInputText('');
306337
setPredictions([]);
307338
setShowSuggestions(false);
308-
onPlaceSelect?.(null);
309339
onTextChange?.('');
310340
setSessionToken(generateSessionToken());
311341
inputRef.current?.focus();
@@ -323,7 +353,7 @@ const GooglePlacesTextInput = forwardRef(
323353
)}
324354

325355
{/* Loading indicator - position adjusts based on showClearButton */}
326-
{loading && showLoadingIndicator && (
356+
{(loading || detailsLoading) && showLoadingIndicator && (
327357
<ActivityIndicator
328358
style={[styles.loadingIndicator, getIconPosition(45)]}
329359
size={'small'}
@@ -409,24 +439,4 @@ const styles = StyleSheet.create({
409439
},
410440
});
411441

412-
const isRTLText = (text) => {
413-
if (!text) return false;
414-
// Hebrew: \u0590-\u05FF
415-
// Arabic: \u0600-\u06FF, \u0750-\u077F (Arabic Supplement), \u0870-\u089F (Arabic Extended-B)
416-
// Arabic Presentation Forms: \uFB50-\uFDFF, \uFE70-\uFEFF
417-
const rtlRegex =
418-
/[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u0870-\u089F\uFB50-\uFDFF\uFE70-\uFEFF]/;
419-
return rtlRegex.test(text);
420-
};
421-
422-
// Helper function to generate UUID v4
423-
const generateUUID = () => {
424-
// RFC4122 version 4 compliant UUID
425-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
426-
var r = (Math.random() * 16) | 0,
427-
v = c === 'x' ? r : (r & 0x3) | 0x8;
428-
return v.toString(16);
429-
});
430-
};
431-
432442
export default GooglePlacesTextInput;

0 commit comments

Comments
 (0)