Skip to content

Commit 8c52229

Browse files
committed
feat: add support for Google Places Details API with CORS proxy for Expo Web
1 parent c9ea05f commit 8c52229

File tree

5 files changed

+161
-2
lines changed

5 files changed

+161
-2
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ const PlaceDetailsExample = () => {
202202
| biasPrefixText | string | No | - | Text to prepend to search query |
203203
| **Place Details Configuration** |
204204
| fetchDetails | boolean | No | false | Automatically fetch place details when a place is selected |
205-
| detailsProxyUrl | string | No | null | Custom proxy URL for place details requests |
205+
| detailsProxyUrl | string | No | null | Custom proxy URL for place details requests (Required on Expo web)|
206206
| detailsFields | string[] | No | ['displayName', 'formattedAddress', 'location', 'id'] | Array of fields to include in the place details response. see [Valid Fields](https://developers.google.com/maps/documentation/places/web-service/place-details#fieldmask) |
207207
| **UI Customization** |
208208
| style | StyleProp | No | {} | Custom styles object |
@@ -235,6 +235,23 @@ When `fetchDetails` is enabled:
235235

236236
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).
237237

238+
### Place Details on Expo Web
239+
240+
**Important:** To use Google Places Details on Expo Web, you must provide a `detailsProxyUrl` prop that points to a CORS-enabled proxy for the Google Places Details API. This is required due to browser security restrictions.
241+
242+
```javascript
243+
<GooglePlacesTextInput
244+
apiKey="YOUR_GOOGLE_PLACES_API_KEY"
245+
fetchDetails={true}
246+
detailsProxyUrl="https://your-backend-proxy.com/places-details"
247+
onPlaceSelect={(place) => console.log(place.details)}
248+
/>
249+
```
250+
251+
Without a proxy, the component will still work on Expo Web for place search, but place details fetching will fail with CORS errors.
252+
253+
For a complete guide on setting up a proxy server for Expo Web, see [Expo Web Details Proxy Guide](./docs/expo-web-details-proxy.md).
254+
238255
## Session Tokens and Billing
239256

240257
This component automatically manages session tokens to optimize your Google Places API billing:

docs/expo-web-details-proxy.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Using Place Details with Expo Web
2+
3+
## The CORS Issue with Google Places Details API
4+
5+
When using the Google Places Details API in a web browser (including Expo Web), you'll encounter Cross-Origin Resource Sharing (CORS) restrictions. Google's Places API doesn't include the necessary CORS headers to allow direct browser requests, which results in errors like:
6+
7+
```
8+
Access to fetch at 'https://places.googleapis.com/v1/places/...' has been blocked by CORS policy:
9+
No 'Access-Control-Allow-Origin' header is present on the requested resource.
10+
```
11+
12+
## Solution: Use a Proxy Server
13+
14+
To work around this limitation, you need to set up a proxy server that:
15+
16+
1. Receives requests from your Expo Web application
17+
2. Forwards them to the Google Places Details API
18+
3. Returns the response back to your application
19+
20+
The proxy server adds the necessary CORS headers to make the browser happy.
21+
22+
## Setting Up the Proxy Server
23+
24+
We've included an example proxy server in the repository. Here's how to set it up:
25+
26+
### 1. Install Dependencies
27+
28+
Navigate to your project directory and install the required packages:
29+
30+
```bash
31+
npm install express axios
32+
# or
33+
yarn add express axios
34+
```
35+
36+
### 2. Copy the Proxy Server Code
37+
38+
Copy the example proxy server code from `example/places-details-proxy.js` to your project. You can place it in your project root or in a server directory.
39+
40+
### 3. Run the Proxy Server
41+
42+
Start the proxy server:
43+
44+
```bash
45+
node places-details-proxy.js
46+
```
47+
48+
By default, the server will run at `http://localhost:3001`.
49+
50+
### 4. Configure Your React Native Component
51+
52+
Update your GooglePlacesTextInput component to use the proxy:
53+
54+
```javascript
55+
<GooglePlacesTextInput
56+
apiKey="YOUR_GOOGLE_PLACES_API_KEY"
57+
fetchDetails={true}
58+
detailsProxyUrl="http://localhost:3001/places-details"
59+
detailsFields={['formattedAddress', 'location']}
60+
onPlaceSelect={handlePlaceSelect}
61+
/>
62+
```
63+
64+
## Deploying to Production
65+
66+
For production use:
67+
68+
1. Deploy the proxy server to a hosting service (like Heroku, Vercel, AWS, etc.)
69+
2. Update your `detailsProxyUrl` to point to your deployed proxy
70+
71+
## Security Considerations
72+
73+
When implementing this in production:
74+
75+
1. **Don't expose your Google API key**: Consider validating requests and using your API key on the server side only
76+
2. **Limit access**: Restrict who can use your proxy with authentication or origin checking
77+
3. **Monitor usage**: Keep track of API usage to avoid unexpected costs
78+
79+
By following these steps, you can use Google Places Details API with Expo Web applications effectively.

example/places-details-proxy.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Example of a minimal Express proxy for Google Places Details API (v1) with CORS support
2+
3+
const express = require('express');
4+
const axios = require('axios');
5+
const app = express();
6+
7+
// CORS middleware for all routes and preflight
8+
app.use((req, res, next) => {
9+
res.header('Access-Control-Allow-Origin', '*');
10+
res.header('Access-Control-Allow-Methods', 'GET,OPTIONS');
11+
res.header(
12+
'Access-Control-Allow-Headers',
13+
'Content-Type,X-Goog-Api-Key,X-Goog-SessionToken,X-Goog-FieldMask'
14+
);
15+
if (req.method === 'OPTIONS') {
16+
return res.sendStatus(200);
17+
}
18+
next();
19+
});
20+
21+
app.get('/places-details/:placeId', async (req, res) => {
22+
const { placeId } = req.params;
23+
const { languageCode } = req.query;
24+
25+
// Forward required headers
26+
const headers = {};
27+
if (req.header('X-Goog-Api-Key'))
28+
headers['X-Goog-Api-Key'] = req.header('X-Goog-Api-Key');
29+
if (req.header('X-Goog-SessionToken'))
30+
headers['X-Goog-SessionToken'] = req.header('X-Goog-SessionToken');
31+
if (req.header('X-Goog-FieldMask'))
32+
headers['X-Goog-FieldMask'] = req.header('X-Goog-FieldMask');
33+
34+
let url = `https://places.googleapis.com/v1/places/${placeId}`;
35+
if (languageCode) {
36+
url += `?languageCode=${encodeURIComponent(languageCode)}`;
37+
}
38+
39+
try {
40+
const response = await axios.get(url, { headers });
41+
res.json(response.data);
42+
} catch (e) {
43+
res.status(500).json({ error: e.message });
44+
}
45+
});
46+
47+
const PORT = process.env.PORT || 3001;
48+
app.listen(PORT, () => {
49+
console.log(
50+
`Places Details proxy running on http://localhost:${PORT}/places-details/:placeId`
51+
);
52+
});

src/GooglePlacesTextInput.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,15 @@ const GooglePlacesTextInput = forwardRef(
309309
return { end: paddingValue };
310310
};
311311

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+
312321
return (
313322
<View style={[styles.container, style.container]}>
314323
<View>

src/services/googlePlacesApi.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ export const fetchPlaceDetails = async ({
7373
}
7474

7575
try {
76-
const API_URL = detailsProxyUrl || `${DEFAULT_PLACE_DETAILS_URL}${placeId}`;
76+
const API_URL = detailsProxyUrl
77+
? `${detailsProxyUrl}/${placeId}`
78+
: `${DEFAULT_PLACE_DETAILS_URL}${placeId}`;
7779
const headers = {
7880
'Content-Type': 'application/json',
7981
};

0 commit comments

Comments
 (0)