├── .gitignore
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.md
│ ├── bug_report.md
│ └── usage-help.md
└── FUNDING.yml
├── .prettierrc
├── Assets
├── screenshot.png
└── expensify-logo.png
├── example
├── babel.config.js
├── .gitignore
├── package.json
├── app.json
├── README.md
└── App.js
├── images
├── powered_by_google_on_white.png
├── powered_by_google_on_white@2x.png
└── powered_by_google_on_white@3x.png
├── .npmignore
├── .release-it.json
├── .prettierignore
├── .eslintignore
├── .eslintrc
├── LICENSE
├── package.json
├── GooglePlacesAutocomplete.d.ts
├── README.md
└── GooglePlacesAutocomplete.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 | node_modules/
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "jsxSingleQuote": true,
3 | "singleQuote": true,
4 | "trailingComma": "all"
5 | }
6 |
--------------------------------------------------------------------------------
/Assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-google-places-autocomplete/HEAD/Assets/screenshot.png
--------------------------------------------------------------------------------
/Assets/expensify-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-google-places-autocomplete/HEAD/Assets/expensify-logo.png
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/images/powered_by_google_on_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-google-places-autocomplete/HEAD/images/powered_by_google_on_white.png
--------------------------------------------------------------------------------
/images/powered_by_google_on_white@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-google-places-autocomplete/HEAD/images/powered_by_google_on_white@2x.png
--------------------------------------------------------------------------------
/images/powered_by_google_on_white@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-google-places-autocomplete/HEAD/images/powered_by_google_on_white@3x.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | GooglePlacesAutocompleteExample
3 | Assets
4 | yarn.lock
5 | .eslintrc
6 | .prettierrc
7 | .*ignore
8 | .github
9 | .release-it.json
10 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "git": {
3 | "requireBranch": "master"
4 | },
5 | "github": {
6 | "release": true
7 | },
8 | "npm": {
9 | "publish": false
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore all png files:
2 | *.png
3 |
4 | # Lock files
5 | yarn.lock
6 | **/yarn.lock
7 | package-lock.json
8 | **/package-lock.json
9 |
10 | # Dependencies
11 | node_modules/
12 | **/node_modules/
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | .expo-shared/
4 | dist/
5 | npm-debug.*
6 | *.jks
7 | *.p8
8 | *.p12
9 | *.key
10 | *.mobileprovision
11 | *.orig.*
12 | web-build/
13 |
14 | # macOS
15 | .DS_Store
16 |
17 | # Environment variables
18 | .env
19 | .env.local
20 |
21 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | node_modules/
3 | **/node_modules/
4 |
5 | # Lock files
6 | yarn.lock
7 | package-lock.json
8 | **/yarn.lock
9 | **/package-lock.json
10 |
11 | # Build outputs
12 | dist/
13 | build/
14 | *.min.js
15 |
16 | # Config files that don't need linting
17 | *.config.js
18 | babel.config.js
19 |
20 | # Other
21 | .DS_Store
22 | *.log
23 | .expo/
24 | .expo-shared/
25 |
26 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@react-native-community", "prettier"],
3 | "plugins": ["prettier"],
4 | "rules": {
5 | "prettier/prettier": "warn"
6 | },
7 | "ignorePatterns": [
8 | "node_modules/",
9 | "**/node_modules/",
10 | "yarn.lock",
11 | "**/yarn.lock",
12 | "package-lock.json",
13 | "**/package-lock.json",
14 | "dist/",
15 | "build/",
16 | "*.min.js",
17 | ".expo/",
18 | ".expo-shared/"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "google-places-autocomplete-example",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "expo": "^54.0.25",
13 | "expo-status-bar": "~3.0.8",
14 | "react": "19.1.0",
15 | "react-native": "0.81.5",
16 | "react-native-google-places-autocomplete": "file:..",
17 | "react-native-safe-area-context": "~5.6.0"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "^7.20.0"
21 | },
22 | "private": true
23 | }
24 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "Google Places Autocomplete Example",
4 | "slug": "google-places-autocomplete-example",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "userInterfaceStyle": "light",
8 | "splash": {
9 | "resizeMode": "contain",
10 | "backgroundColor": "#ffffff"
11 | },
12 | "assetBundlePatterns": ["**/*"],
13 | "ios": {
14 | "supportsTablet": true,
15 | "bundleIdentifier": "com.example.googleplacesautocomplete"
16 | },
17 | "android": {
18 | "adaptiveIcon": {
19 | "backgroundColor": "#ffffff"
20 | },
21 | "package": "com.example.googleplacesautocomplete"
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [faridsafi]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: 'Feature Request'
5 | labels: enhancement
6 | assignees: ''
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 |
11 | A clear and concise description of what the problem is.
12 | Ex. I'm always frustrated when... or I would like the ability to be able to...
13 |
14 | **Describe the solution you'd like**
15 |
16 | A clear and concise description of what you want to happen.
17 |
18 | **Describe alternatives you've considered**
19 |
20 | A clear and concise description of any alternative solutions or features you've considered.
21 |
22 | **Additional context**
23 |
24 | Add any other context or screenshots about the feature request here.
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report a bug in the library to help us improve
4 | title: 'Bug Report'
5 | labels: bug
6 | assignees: ''
7 | ---
8 |
9 | # Describe the bug
10 |
11 | A clear and concise description of what the bug is.
12 |
13 | # Reproduction - (required - issue will be closed without this)
14 |
15 | Steps to reproduce the behavior - a minimal reproducible code example, link to a [snack](https://snack.expo.dev/) or a repository.
16 |
17 | **Please provide a FULLY REPRODUCIBLE example.**
18 |
19 |
20 | Click to expand!
21 |
22 | ```javascript
23 |
24 |
25 | ```
26 |
27 |
28 | _Please remember to remove you google API key from the code you provide here_
29 |
30 | # Additional context
31 |
32 | - Library Version: [e.g. 1.4.2]
33 | - React Native Version: [e.g. 0.62.2]
34 |
35 | - [ ] iOS
36 | - [ ] Android
37 | - [ ] Web
38 |
39 | If you are using expo please indicate here:
40 |
41 | - [ ] I am using expo
42 |
43 | Add any other context about the problem here, screenshots etc
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/usage-help.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Usage Help
3 | about: Use this if you are having a problem using the library
4 | title: ''
5 | labels: question
6 | assignees: ''
7 | ---
8 |
9 | # Describe the problem
10 |
11 | A clear and concise description of what you are trying to do and where things are going wrong.
12 |
13 | # Reproduction - (required - issue will be closed without this)
14 |
15 | Steps to reproduce the behavior - a minimal reproducible code example, link to a [snack](https://snack.expo.io) or a repository.
16 |
17 | **Please provide a FULLY REPRODUCIBLE example.**
18 |
19 |
20 | Click to expand!
21 |
22 | ```javascript
23 |
24 |
25 | ```
26 |
27 |
28 | _Please remember to remove you google API key from the code you provide here_
29 |
30 | # Additional context
31 |
32 | - Library Version: [e.g. 1.4.2]
33 | - React Native Version: [e.g. 0.62.2]
34 |
35 | - [ ] iOS
36 | - [ ] Android
37 | - [ ] Web
38 |
39 | If you are using expo please indicate here:
40 |
41 | - [ ] I am using expo
42 |
43 | Add any other context about the problem here, screenshots etc
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Farid from Safi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-google-places-autocomplete",
3 | "version": "2.6.1",
4 | "description": "Customizable Google Places autocomplete component for iOS and Android React-Native apps",
5 | "main": "GooglePlacesAutocomplete.js",
6 | "types": "GooglePlacesAutocomplete.d.ts",
7 | "scripts": {
8 | "lint": "eslint . && prettier --write .",
9 | "release": "npx release-it",
10 | "prettier": "prettier --write ."
11 | },
12 | "author": "Farid from Safi",
13 | "license": "MIT",
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/FaridSafi/react-native-google-places-autocomplete.git"
17 | },
18 | "keywords": [
19 | "autocomplete",
20 | "google",
21 | "places",
22 | "react-component",
23 | "react-native",
24 | "ios",
25 | "android"
26 | ],
27 | "bugs": {
28 | "url": "https://github.com/FaridSafi/react-native-google-places-autocomplete/issues"
29 | },
30 | "homepage": "https://github.com/FaridSafi/react-native-google-places-autocomplete#readme",
31 | "dependencies": {
32 | "lodash.debounce": "^4.0.8",
33 | "qs": "~6.9.1",
34 | "react-native-uuid": "^2.0.3"
35 | },
36 | "devDependencies": {
37 | "@react-native-community/eslint-config": "2.0.0",
38 | "eslint": "7.10.0",
39 | "eslint-plugin-prettier": "^3.1.2",
40 | "husky": "4.3.0",
41 | "lint-staged": "10.4.0",
42 | "prettier": "2.1.2",
43 | "typescript": "^3.8.3"
44 | },
45 | "peerDependencies": {
46 | "react-native": ">= 0.59"
47 | },
48 | "husky": {
49 | "hooks": {
50 | "pre-commit": "lint-staged"
51 | }
52 | },
53 | "lint-staged": {
54 | "*/**": "yarn lint"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # Google Places Autocomplete Example
2 |
3 | This is an example Expo project to test the `react-native-google-places-autocomplete` library locally.
4 |
5 | ## Setup
6 |
7 | 1. **Install dependencies:**
8 |
9 | ```bash
10 | cd example
11 | npm install
12 | # or
13 | yarn install
14 | ```
15 |
16 | 2. **Get your Google Places API Key:**
17 |
18 | - Go to [Google Cloud Console](https://console.cloud.google.com/)
19 | - Create a new project or select an existing one
20 | - Enable the "Places API" (Web Service)
21 | - Create credentials (API Key)
22 | - Optionally, enable "Geocoding API" if you want to use current location features
23 |
24 | 3. **Add your API Key:**
25 |
26 | - Open `App.js`
27 | - Replace `YOUR_API_KEY_HERE` with your actual Google Places API key
28 |
29 | 4. **Run the app:**
30 |
31 | ```bash
32 | npm start
33 | # or
34 | yarn start
35 | ```
36 |
37 | Then press:
38 |
39 | - `i` for iOS simulator
40 | - `a` for Android emulator
41 | - `w` for web browser
42 |
43 | ## Features Demonstrated
44 |
45 | This example app demonstrates:
46 |
47 | - **Basic Search**: Simple autocomplete search functionality
48 | - **Current Location**: Using the current location feature
49 | - **Predefined Places**: Adding predefined places like Home and Work
50 | - **Place Details**: Fetching detailed information about selected places
51 |
52 | ## Testing Local Changes
53 |
54 | Since this example uses `file:..` to link to the parent library, any changes you make to the library files in the parent directory will be reflected in this example app. You may need to:
55 |
56 | 1. Restart the Expo development server after making changes to the library
57 | 2. Clear the cache if changes aren't reflected: `npm start -- --clear` or `yarn start --clear`
58 |
59 | ## Notes
60 |
61 | - Make sure you have the Expo CLI installed globally: `npm install -g expo-cli` (optional, as npx can be used)
62 | - For iOS, you'll need Xcode installed
63 | - For Android, you'll need Android Studio and an emulator set up
64 | - The library requires a valid Google Places API key to function
65 |
--------------------------------------------------------------------------------
/example/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | StyleSheet,
4 | View,
5 | Text,
6 | KeyboardAvoidingView,
7 | Platform,
8 | ScrollView,
9 | Alert,
10 | } from 'react-native';
11 | import { StatusBar } from 'expo-status-bar';
12 | import { GooglePlacesAutocomplete } from 'react-native-google-places-autocomplete';
13 | import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
14 |
15 | // Replace with your Google Places API Key
16 | const GOOGLE_PLACES_API_KEY = 'YOUR_API_KEY_HERE';
17 |
18 | export default function App() {
19 | const [selectedPlace, setSelectedPlace] = useState(null);
20 |
21 | return (
22 |
23 |
27 |
28 |
33 |
39 |
40 | Google Places Autocomplete
41 | Example App
42 |
43 |
44 |
45 | {
48 | console.log('Selected place:', data);
49 | console.log('Place details:', details);
50 | setSelectedPlace({ data, details });
51 | Alert.alert('Place Selected', data.description);
52 | }}
53 | query={{
54 | key: GOOGLE_PLACES_API_KEY,
55 | language: 'en',
56 | }}
57 | fetchDetails={true}
58 | styles={{
59 | textInputContainer: {
60 | backgroundColor: 'transparent',
61 | borderTopWidth: 0,
62 | borderBottomWidth: 0,
63 | },
64 | textInput: {
65 | marginLeft: 0,
66 | marginRight: 0,
67 | height: 50,
68 | color: '#5d5d5d',
69 | fontSize: 16,
70 | borderWidth: 1,
71 | borderColor: '#ddd',
72 | borderRadius: 8,
73 | paddingHorizontal: 12,
74 | },
75 | predefinedPlacesDescription: {
76 | color: '#1faadb',
77 | },
78 | }}
79 | debounce={200}
80 | />
81 |
82 |
83 | {selectedPlace && (
84 |
85 | Selected Place
86 |
87 | Description:
88 |
89 | {selectedPlace.data.description}
90 |
91 | {selectedPlace.details && (
92 | <>
93 | Address:
94 |
95 | {selectedPlace.details.formatted_address}
96 |
97 | {selectedPlace.details.geometry && (
98 | <>
99 | Coordinates:
100 |
101 | Lat: {selectedPlace.details.geometry.location.lat},
102 | Lng: {selectedPlace.details.geometry.location.lng}
103 |
104 | >
105 | )}
106 | >
107 | )}
108 |
109 |
110 | )}
111 |
112 | {GOOGLE_PLACES_API_KEY === 'YOUR_API_KEY_HERE' && (
113 |
114 |
115 | Make sure to replace YOUR_API_KEY_HERE with your actual Google
116 | Places API key in App.js
117 |
118 |
119 | )}
120 |
121 |
122 |
123 |
124 | );
125 | }
126 |
127 | const styles = StyleSheet.create({
128 | container: {
129 | flex: 1,
130 | backgroundColor: '#f5f5f5',
131 | },
132 | keyboardAvoidingView: {
133 | flex: 1,
134 | },
135 | scrollView: {
136 | flex: 1,
137 | },
138 | contentContainer: {
139 | padding: 16,
140 | },
141 | header: {
142 | marginBottom: 24,
143 | alignItems: 'center',
144 | },
145 | title: {
146 | fontSize: 28,
147 | fontWeight: 'bold',
148 | color: '#333',
149 | marginBottom: 8,
150 | },
151 | subtitle: {
152 | fontSize: 16,
153 | color: '#666',
154 | },
155 | section: {
156 | backgroundColor: '#fff',
157 | padding: 16,
158 | borderRadius: 12,
159 | shadowColor: '#000',
160 | shadowOffset: {
161 | width: 0,
162 | height: 2,
163 | },
164 | shadowOpacity: 0.1,
165 | shadowRadius: 3.84,
166 | elevation: 5,
167 | },
168 | sectionTitle: {
169 | fontSize: 18,
170 | fontWeight: '600',
171 | color: '#333',
172 | marginBottom: 12,
173 | },
174 | resultSection: {
175 | marginTop: 24,
176 | backgroundColor: '#fff',
177 | padding: 16,
178 | borderRadius: 12,
179 | shadowColor: '#000',
180 | shadowOffset: {
181 | width: 0,
182 | height: 2,
183 | },
184 | shadowOpacity: 0.1,
185 | shadowRadius: 3.84,
186 | elevation: 5,
187 | },
188 | resultBox: {
189 | backgroundColor: '#f9f9f9',
190 | padding: 12,
191 | borderRadius: 8,
192 | borderWidth: 1,
193 | borderColor: '#e0e0e0',
194 | },
195 | resultLabel: {
196 | fontSize: 14,
197 | fontWeight: '600',
198 | color: '#666',
199 | marginTop: 8,
200 | marginBottom: 4,
201 | },
202 | resultText: {
203 | fontSize: 14,
204 | color: '#333',
205 | marginBottom: 4,
206 | },
207 | footer: {
208 | marginTop: 16,
209 | marginBottom: 32,
210 | padding: 12,
211 | backgroundColor: '#fff3cd',
212 | borderRadius: 8,
213 | borderWidth: 1,
214 | borderColor: '#ffc107',
215 | },
216 | footerText: {
217 | fontSize: 12,
218 | color: '#856404',
219 | textAlign: 'center',
220 | },
221 | });
222 |
--------------------------------------------------------------------------------
/GooglePlacesAutocomplete.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | ImageStyle,
4 | StyleProp,
5 | TextInput,
6 | TextInputProps,
7 | TextStyle,
8 | ViewStyle,
9 | } from 'react-native';
10 |
11 | /** @see https://developers.google.com/maps/faq#languagesupport */
12 | type Language =
13 | | 'af'
14 | | 'am'
15 | | 'ar'
16 | | 'az'
17 | | 'be'
18 | | 'bg'
19 | | 'bn'
20 | | 'bs'
21 | | 'ca'
22 | | 'cs'
23 | | 'da'
24 | | 'de'
25 | | 'el'
26 | | 'en-AU'
27 | | 'en-GB'
28 | | 'en'
29 | | 'es-419'
30 | | 'es'
31 | | 'et'
32 | | 'eu'
33 | | 'fa'
34 | | 'fi'
35 | | 'fil'
36 | | 'fr-CA'
37 | | 'fr'
38 | | 'gl'
39 | | 'gu'
40 | | 'hi'
41 | | 'hr'
42 | | 'hu'
43 | | 'hy'
44 | | 'id'
45 | | 'is'
46 | | 'it'
47 | | 'iw'
48 | | 'ja'
49 | | 'ka'
50 | | 'kk'
51 | | 'km'
52 | | 'kn'
53 | | 'ko'
54 | | 'ky'
55 | | 'lo'
56 | | 'lt'
57 | | 'lv'
58 | | 'mk'
59 | | 'ml'
60 | | 'mn'
61 | | 'mr'
62 | | 'ms'
63 | | 'my'
64 | | 'ne'
65 | | 'nl'
66 | | 'no'
67 | | 'pa'
68 | | 'pl'
69 | | 'pt-BR'
70 | | 'pt-PT'
71 | | 'pt'
72 | | 'ro'
73 | | 'ru'
74 | | 'si'
75 | | 'sk'
76 | | 'sl'
77 | | 'sq'
78 | | 'sr'
79 | | 'sv'
80 | | 'sw'
81 | | 'ta'
82 | | 'te'
83 | | 'th'
84 | | 'tr'
85 | | 'uk'
86 | | 'ur'
87 | | 'uz'
88 | | 'vi'
89 | | 'zh-CN'
90 | | 'zh-HK'
91 | | 'zh-TW'
92 | | 'zh'
93 | | 'zu';
94 |
95 | /** @see https://developers.google.com/places/web-service/supported_types#table1 */
96 | type SearchType =
97 | | 'accounting'
98 | | 'airport'
99 | | 'amusement_park'
100 | | 'aquarium'
101 | | 'art_gallery'
102 | | 'atm'
103 | | 'bakery'
104 | | 'bank'
105 | | 'bar'
106 | | 'beauty_salon'
107 | | 'bicycle_store'
108 | | 'book_store'
109 | | 'bowling_alley'
110 | | 'bus_station'
111 | | 'cafe'
112 | | 'campground'
113 | | 'car_dealer'
114 | | 'car_rental'
115 | | 'car_repair'
116 | | 'car_wash'
117 | | 'casino'
118 | | 'cemetery'
119 | | 'church'
120 | | 'city_hall'
121 | | 'clothing_store'
122 | | 'convenience_store'
123 | | 'courthouse'
124 | | 'dentist'
125 | | 'department_store'
126 | | 'doctor'
127 | | 'drugstore'
128 | | 'electrician'
129 | | 'electronics_store'
130 | | 'embassy'
131 | | 'fire_station'
132 | | 'florist'
133 | | 'funeral_home'
134 | | 'furniture_store'
135 | | 'gas_station'
136 | | 'gym'
137 | | 'hair_care'
138 | | 'hardware_store'
139 | | 'hindu_temple'
140 | | 'home_goods_store'
141 | | 'hospital'
142 | | 'insurance_agency'
143 | | 'jewelry_store'
144 | | 'laundry'
145 | | 'lawyer'
146 | | 'library'
147 | | 'light_rail_station'
148 | | 'liquor_store'
149 | | 'local_government_office'
150 | | 'locksmith'
151 | | 'lodging'
152 | | 'meal_delivery'
153 | | 'meal_takeaway'
154 | | 'mosque'
155 | | 'movie_rental'
156 | | 'movie_theater'
157 | | 'moving_company'
158 | | 'museum'
159 | | 'night_club'
160 | | 'painter'
161 | | 'park'
162 | | 'parking'
163 | | 'pet_store'
164 | | 'pharmacy'
165 | | 'physiotherapist'
166 | | 'plumber'
167 | | 'police'
168 | | 'post_office'
169 | | 'primary_school'
170 | | 'real_estate_agency'
171 | | 'restaurant'
172 | | 'roofing_contractor'
173 | | 'rv_park'
174 | | 'school'
175 | | 'secondary_school'
176 | | 'shoe_store'
177 | | 'shopping_mall'
178 | | 'spa'
179 | | 'stadium'
180 | | 'storage'
181 | | 'store'
182 | | 'subway_station'
183 | | 'supermarket'
184 | | 'synagogue'
185 | | 'taxi_stand'
186 | | 'tourist_attraction'
187 | | 'train_station'
188 | | 'transit_station'
189 | | 'travel_agency'
190 | | 'university'
191 | | 'veterinary_care'
192 | | 'zoo';
193 |
194 | /** @see https://developers.google.com/places/web-service/supported_types#table2 */
195 | type PlaceType =
196 | | 'administrative_area_level_1'
197 | | 'administrative_area_level_2'
198 | | 'administrative_area_level_3'
199 | | 'administrative_area_level_4'
200 | | 'administrative_area_level_5'
201 | | 'archipelago'
202 | | 'colloquial_area'
203 | | 'continent'
204 | | 'country'
205 | | 'establishment'
206 | | 'finance'
207 | | 'floor'
208 | | 'food'
209 | | 'general_contractor'
210 | | 'geocode'
211 | | 'health'
212 | | 'intersection'
213 | | 'locality'
214 | | 'natural_feature'
215 | | 'neighborhood'
216 | | 'place_of_worship'
217 | | 'plus_code'
218 | | 'point_of_interest'
219 | | 'political'
220 | | 'post_box'
221 | | 'postal_code'
222 | | 'postal_code_prefix'
223 | | 'postal_code_suffix'
224 | | 'postal_town'
225 | | 'premise'
226 | | 'room'
227 | | 'route'
228 | | 'street_address'
229 | | 'street_number'
230 | | 'sublocality'
231 | | 'sublocality_level_1'
232 | | 'sublocality_level_2'
233 | | 'sublocality_level_3'
234 | | 'sublocality_level_4'
235 | | 'sublocality_level_5'
236 | | 'subpremise'
237 | | 'town_square';
238 |
239 | /** @see https://developers.google.com/places/web-service/supported_types#table3 */
240 | type AutocompleteRequestType =
241 | | '(regions)'
242 | | '(cities)'
243 | | 'geocode'
244 | | 'address'
245 | | 'establishment';
246 |
247 | interface DescriptionRow {
248 | description: string;
249 | id: string;
250 | matched_substrings: MatchedSubString[];
251 | place_id: string;
252 | reference: string;
253 | structured_formatting: StructuredFormatting;
254 | terms: Term[];
255 | types: PlaceType[];
256 | }
257 |
258 | interface MatchedSubString {
259 | length: number;
260 | offset: number;
261 | }
262 |
263 | interface Term {
264 | offset: number;
265 | value: string;
266 | }
267 |
268 | interface StructuredFormatting {
269 | main_text: string;
270 | main_text_matched_substrings: Object[][];
271 | secondary_text: string;
272 | secondary_text_matched_substrings: Object[][];
273 | terms: Term[];
274 | types: PlaceType[];
275 | }
276 |
277 | interface GooglePlaceData {
278 | description: string;
279 | id: string;
280 | matched_substrings: MatchedSubString[];
281 | place_id: string;
282 | reference: string;
283 | structured_formatting: StructuredFormatting;
284 | }
285 |
286 | interface Point {
287 | lat: number;
288 | lng: number;
289 | latitude: number;
290 | longitude: number;
291 | }
292 |
293 | interface AddressComponent {
294 | long_name: string;
295 | short_name: string;
296 | longText: string;
297 | shortText: string;
298 | types: PlaceType[];
299 | }
300 |
301 | interface Geometry {
302 | location: Point;
303 | viewport: {
304 | northeast: Point;
305 | southwest: Point;
306 | };
307 | }
308 |
309 | interface PlusCode {
310 | compound_code: string;
311 | global_code: string;
312 | }
313 |
314 | interface GooglePlaceDetail {
315 | address_components: AddressComponent[];
316 | adr_address: string;
317 | formatted_address: string;
318 | geometry: Geometry;
319 | icon: string;
320 | id: string;
321 | name: string;
322 | place_id: string;
323 | plus_code: PlusCode;
324 | reference: string;
325 | scope: 'GOOGLE';
326 | types: PlaceType[];
327 | url: string;
328 | utc_offset: number;
329 | vicinity: string;
330 | // New Places API parameters
331 | addressComponents: AddressComponent[];
332 | adrFormatAddress: string;
333 | formattedAddress: string;
334 | location: Point;
335 | }
336 |
337 | /** @see https://developers.google.com/places/web-service/autocomplete */
338 | interface Query {
339 | key: string;
340 | sessiontoken?: string;
341 | offset?: number;
342 | location?: string;
343 | radius?: number;
344 | language?: Language;
345 | components?: string;
346 | rankby?: string;
347 | type?: T;
348 | strictbounds?: boolean;
349 | /** @deprecated @see https://github.com/FaridSafi/react-native-google-places-autocomplete/pull/384 */
350 | types?: T;
351 | }
352 |
353 | interface Styles {
354 | container: StyleProp;
355 | description: StyleProp;
356 | textInputContainer: StyleProp;
357 | textInput: StyleProp;
358 | loader: StyleProp;
359 | listView: StyleProp;
360 | predefinedPlacesDescription: StyleProp;
361 | poweredContainer: StyleProp;
362 | powered: StyleProp;
363 | separator: StyleProp;
364 | row: StyleProp;
365 | }
366 |
367 | interface Place {
368 | description: string;
369 | geometry: { location: Point };
370 | }
371 |
372 | interface RequestUrl {
373 | url: string;
374 | useOnPlatform: 'web' | 'all';
375 | headers?: Record;
376 | }
377 |
378 | interface GooglePlacesAutocompleteProps {
379 | autoFillOnNotFound?: boolean;
380 | /** Will add a 'Current location' button at the top of the predefined places list */
381 | currentLocation?: boolean;
382 | currentLocationLabel?: string;
383 | /** debounce the requests in ms. Set to 0 to remove debounce. By default 0ms. */
384 | debounce?: number;
385 | disableScroll?: boolean;
386 | enableHighAccuracyLocation?: boolean;
387 | enablePoweredByContainer?: boolean;
388 | fetchDetails?: boolean;
389 | /** filter the reverse geocoding results by types - ['locality', 'administrative_area_level_3'] if you want to display only cities */
390 | filterReverseGeocodingByTypes?: PlaceType[];
391 | /** available options for GooglePlacesDetails API: https://developers.google.com/places/web-service/details */
392 | GooglePlacesDetailsQuery?: Partial & { fields?: string };
393 | /** available options for GooglePlacesSearch API: https://developers.google.com/places/web-service/search */
394 | GooglePlacesSearchQuery?: Partial>;
395 | /** available options for GoogleReverseGeocoding API: https://developers.google.com/maps/documentation/geocoding/intro */
396 | GoogleReverseGeocodingQuery?: {
397 | bounds?: number;
398 | language?: Language;
399 | region?: string;
400 | components?: string;
401 | };
402 | inbetweenCompo?: React.ReactNode;
403 | isRowScrollable?: boolean;
404 | keyboardShouldPersistTaps?: 'never' | 'always' | 'handled';
405 | /** use the ListEmptyComponent prop when no autocomplete results are found. */
406 | listEmptyComponent?: JSX.Element | React.ComponentType<{}>;
407 | /** use the ListLoaderComponent prop when no results are loading. */
408 | listLoaderComponent?: JSX.Element | React.ComponentType<{}>;
409 | listHoverColor?: string;
410 | listUnderlayColor?: string;
411 | listViewDisplayed?: 'auto' | boolean;
412 | /** minimum length of text to search */
413 | minLength?: number;
414 | keepResultsAfterBlur?: boolean;
415 | /** Which API to use: GoogleReverseGeocoding or GooglePlacesSearch */
416 | nearbyPlacesAPI?: 'GoogleReverseGeocoding' | 'GooglePlacesSearch';
417 | numberOfLines?: number;
418 | onFail?: (error?: any) => void;
419 | onNotFound?: () => void;
420 | onPress?: (data: GooglePlaceData, detail: GooglePlaceDetail | null) => void;
421 | onTimeout?: () => void;
422 | placeholder: string;
423 | predefinedPlaces?: Place[];
424 | predefinedPlacesAlwaysVisible?: boolean;
425 | preProcess?: (text: string) => string;
426 | query: Query | Object;
427 | renderDescription?: (description: DescriptionRow) => string;
428 | renderHeaderComponent?: () => JSX.Element | React.ComponentType<{}>;
429 | renderLeftButton?: () => JSX.Element | React.ComponentType<{}>;
430 | renderRightButton?: () => JSX.Element | React.ComponentType<{}>;
431 | renderRow?: (
432 | data: GooglePlaceData,
433 | index: number,
434 | ) => JSX.Element | React.ComponentType<{}>;
435 | /** sets the request URL to something other than the google api. Helpful if you want web support or to use your own api. */
436 | requestUrl?: RequestUrl;
437 | styles?: Partial | Object;
438 | suppressDefaultStyles?: boolean;
439 | textInputHide?: boolean;
440 | /** text input props */
441 | textInputProps?: TextInputProps | Object;
442 | timeout?: number;
443 | isNewPlacesAPI?: boolean;
444 | fields?: string;
445 | }
446 |
447 | export type GooglePlacesAutocompleteRef = {
448 | setAddressText(address: string): void;
449 | getAddressText(): string;
450 | getCurrentLocation(): void;
451 | } & TextInput;
452 |
453 | export const GooglePlacesAutocomplete: React.ForwardRefExoticComponent<
454 | React.PropsWithoutRef &
455 | React.RefAttributes
456 | >;
457 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Google Maps Search Component for React Native
6 |
7 | **Customizable Google Places autocomplete component for iOS and Android React-Native apps**
8 |
9 | ## Preview
10 |
11 | 
12 |
13 | ## Installation
14 |
15 | **Step 1.**
16 |
17 | ```
18 | npm install react-native-google-places-autocomplete --save
19 | ```
20 |
21 | or
22 |
23 | ```
24 | yarn add react-native-google-places-autocomplete
25 | ```
26 |
27 | **Step 2.**
28 |
29 | Get your [Google Places API keys](https://developers.google.com/maps/documentation/places/web-service/get-api-key/) and enable "Google Places API Web Service" (NOT Android or iOS) in the console. Billing must be enabled on the account.
30 |
31 | **Step 3.**
32 |
33 | Enable "Google Maps Geocoding API" if you want to use GoogleReverseGeocoding for Current Location
34 |
35 | ## Basic Example
36 |
37 | **Basic Address Search**
38 |
39 | ```js
40 | import React from 'react';
41 | import { GooglePlacesAutocomplete } from 'react-native-google-places-autocomplete';
42 |
43 | const GooglePlacesInput = () => {
44 | return (
45 | {
48 | // 'details' is provided when fetchDetails = true
49 | console.log(data, details);
50 | }}
51 | query={{
52 | key: 'YOUR API KEY',
53 | language: 'en',
54 | }}
55 | />
56 | );
57 | };
58 |
59 | export default GooglePlacesInput;
60 | ```
61 |
62 | You can also try the basic example in a snack [here](https://snack.expo.io/@sbell/react-native-google-places-autocomplete)
63 |
64 | ## More Examples
65 |
66 | **Get Current Location**
67 |
68 |
69 | Click to expand
70 |
71 | _Extra step required!_
72 |
73 | If you are targeting React Native 0.60.0+ you must install either `@react-native-community/geolocation` ([link](https://github.com/react-native-community/react-native-geolocation)) or `react-native-geolocation-service`([link](https://github.com/Agontuk/react-native-geolocation-service)).
74 |
75 | Please make sure you follow the installation instructions there and add `navigator.geolocation = require(GEOLOCATION_PACKAGE)` somewhere in you application before ``.
76 |
77 | ```js
78 | import React from 'react';
79 | import { GooglePlacesAutocomplete } from 'react-native-google-places-autocomplete';
80 |
81 | // navigator.geolocation = require('@react-native-community/geolocation');
82 | // navigator.geolocation = require('react-native-geolocation-service');
83 |
84 | const GooglePlacesInput = () => {
85 | return (
86 | {
89 | // 'details' is provided when fetchDetails = true
90 | console.log(data, details);
91 | }}
92 | query={{
93 | key: 'YOUR API KEY',
94 | language: 'en',
95 | }}
96 | currentLocation={true}
97 | currentLocationLabel='Current location'
98 | />
99 | );
100 | };
101 |
102 | export default GooglePlacesInput;
103 | ```
104 |
105 |
106 |
107 | **Search with predefined option**
108 |
109 |
110 | Click to expand
111 |
112 | ```js
113 | import React from 'react';
114 | import { GooglePlacesAutocomplete } from 'react-native-google-places-autocomplete';
115 |
116 | const homePlace = {
117 | description: 'Home',
118 | geometry: { location: { lat: 48.8152937, lng: 2.4597668 } },
119 | };
120 | const workPlace = {
121 | description: 'Work',
122 | geometry: { location: { lat: 48.8496818, lng: 2.2940881 } },
123 | };
124 |
125 | const GooglePlacesInput = () => {
126 | return (
127 | {
130 | // 'details' is provided when fetchDetails = true
131 | console.log(data, details);
132 | }}
133 | query={{
134 | key: 'YOUR API KEY',
135 | language: 'en',
136 | }}
137 | predefinedPlaces={[homePlace, workPlace]}
138 | />
139 | );
140 | };
141 |
142 | export default GooglePlacesInput;
143 | ```
144 |
145 |
146 |
147 | **Limit results to one country**
148 |
149 |
150 | Click to expand
151 |
152 | ```js
153 | import React from 'react';
154 | import { GooglePlacesAutocomplete } from 'react-native-google-places-autocomplete';
155 |
156 | const GooglePlacesInput = () => {
157 | return (
158 | {
161 | // 'details' is provided when fetchDetails = true
162 | console.log(data, details);
163 | }}
164 | query={{
165 | key: 'YOUR API KEY',
166 | language: 'en',
167 | components: 'country:us',
168 | }}
169 | />
170 | );
171 | };
172 |
173 | export default GooglePlacesInput;
174 | ```
175 |
176 |
177 |
178 | **Use a custom Text Input Component**
179 |
180 |
181 | Click to expand
182 |
183 | This is an example using the Text Input from [`react-native-elements`](https://reactnativeelements.com/docs/components/input).
184 |
185 | ```js
186 | import React from 'react';
187 | import { Text, View, Image } from 'react-native';
188 | import { GooglePlacesAutocomplete } from 'react-native-google-places-autocomplete';
189 | import { Input } from 'react-native-elements';
190 |
191 | const GOOGLE_PLACES_API_KEY = 'YOUR_GOOGLE_API_KEY';
192 |
193 | const GooglePlacesInput = () => {
194 | return (
195 | console.log(data, details)}
201 | textInputProps={{
202 | InputComp: Input,
203 | leftIcon: { type: 'font-awesome', name: 'chevron-left' },
204 | errorStyle: { color: 'red' },
205 | }}
206 | />
207 | );
208 | };
209 |
210 | export default GooglePlacesInput;
211 | ```
212 |
213 |
214 |
215 | ## Props
216 |
217 | _This list is a work in progress. PRs welcome!_
218 |
219 | | Prop Name | type | description | default value | Options |
220 | | ----------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------- |
221 | | autoFillOnNotFound | boolean | displays the result from autocomplete if the place details api return not found | false | true \| false |
222 | | currentLocation | boolean | Will add a 'Current location' button at the top of the predefined places list | false | true \| false |
223 | | currentLocationLabel | string | change the display label for the current location button | Current Location | Any string |
224 | | debounce | number | debounce the requests (in ms) | 0 | |
225 | | disableScroll | boolean | disable scroll on the results list | | |
226 | | enableHighAccuracyLocation | boolean | use GPS or not. If set to true, a GPS position will be requested. If set to false, a WIFI location will be requested. use GPS or not. If set to true, a GPS position will be requested. If set to false, a WIFI location will be requested. | true | |
227 | | enablePoweredByContainer | boolean | show "powered by Google" at the bottom of the search results list | true | |
228 | | fetchDetails | boolean | get more place details about the selected option from the Place Details API | false | |
229 | | filterReverseGeocodingByTypes | array | filter the reverse geocoding results by types - ['locality', 'administrative_area_level_3'] if you want to display only cities | | |
230 | | GooglePlacesDetailsQuery | object | "query" object for the Google Place Details API (when you press on a suggestion) | | |
231 | | GooglePlacesSearchQuery | object | "query" object for the Google Places Nearby API (when you use current location to find nearby places) | `{ rankby: 'distance', type: 'restaurant' }` | |
232 | | GoogleReverseGeocodingQuery | object | "query" object for the Google Geocode API (when you use current location to get the current address) | | |
233 | | isRowScrollable | boolean | enable/disable horizontal scrolling of a list result https://reactnative.dev/docs/scrollview#scrollenabled | true |
234 | | inbetweenCompo | React.ReactNode | Insert a ReactNode in between the search bar and the search results Flatlist |
235 | | keepResultsAfterBlur | boolean | show list of results after blur | false | true \| false |
236 | | keyboardShouldPersistTaps | string | Determines when the keyboard should stay visible after a tap https://reactnative.dev/docs/scrollview#keyboardshouldpersisttaps | 'always' | 'never' \| 'always' \| 'handled' |
237 | | listEmptyComponent | function | use FlatList's ListEmptyComponent prop when no autocomplete results are found. | | |
238 | | listLoaderComponent | function | show this component while results are loading. | | |
239 | | listHoverColor | string | underlay color of the list result when hovered (web only) | '#ececec' | |
240 | | listUnderlayColor | string | underlay color of the list result when pressed https://reactnative.dev/docs/touchablehighlight#underlaycolor | '#c8c7cc' | |
241 | | listViewDisplayed | string | override the default behavior of showing the list (results) view | 'auto' | 'auto' \| true \| false |
242 | | minLength | number | minimum length of text to trigger a search | 0 | |
243 | | nearbyPlacesAPI | string | which API to use for current location | 'GooglePlacesSearch' | 'none' \| 'GooglePlacesSearch' \| 'GoogleReverseGeocoding' |
244 | | numberOfLines | number | number of lines (android - multiline must be set to true) https://reactnative.dev/docs/textinput#numberoflines | 1 | |
245 | | onFail | function | returns if an unspecified error comes back from the API | | |
246 | | onNotFound | function | returns if the Google Places Details API returns a 'not found' code (when you press a suggestion). | | |
247 | | onPress | function | returns when after a suggestion is selected | | |
248 | | onTimeout | function | callback when a request timeout | `()=>console.warn('google places autocomplete: request timeout')` | |
249 | | placeholder | string | placeholder text https://reactnative.dev/docs/textinput#placeholder | 'Search' | |
250 | | predefinedPlaces | array | Allows you to show pre-defined places (e.g. home, work) | | |
251 | | predefinedPlacesAlwaysVisible | boolean | Shows predefined places at the top of the search results | false | |
252 | | preProcess | function | do something to the text of the search input before a search request is sent | | |
253 | | query | object | "query" object for the Google Places Autocomplete API (link) | `{ key: 'missing api key', language: 'en', types: 'geocode' }` | |
254 | | renderDescription | function | determines the data passed to each renderRow (search result) | | |
255 | | renderHeaderComponent | function | use the `ListHeaderComponent` from `FlatList` when showing autocomplete results | | |
256 | | renderLeftButton | function | add a component to the left side of the Text Input | | |
257 | | renderRightButton | function | add a component to the right side of the Text Input | | |
258 | | renderRow | function | custom component to render each result row (use this to show an icon beside each result). `data` and `index` will be passed as input parameters | | |
259 | | requestUrl | object | used to set the request url for the library | | |
260 | | returnKeyType | string | the return key text https://reactnative.dev/docs/textinput#returnkeytype | 'search' | |
261 | | styles | object | See styles section below | | |
262 | | suppressDefaultStyles | boolean | removes all default styling from the library | false | true \| false |
263 | | textInputHide | boolean | Hide the Search input | false | true \| false |
264 | | textInputProps | object | define props for the [textInput](https://reactnative.dev/docs/textinput), or provide a custom input component | | |
265 | | timeout | number | how many ms until the request will timeout | 20000 | |
266 |
267 | ## Methods
268 |
269 | | method name | type | description |
270 | | -------------------- | ------------------------- | ----------------------------------------------------------------------- |
271 | | `getAddressText` | `() => string` | return the value of TextInput |
272 | | `setAddressText` | `(value: string) => void` | set the value of TextInput |
273 | | `focus` | `void` | makes the TextInput focus |
274 | | `blur` | `void` | makes the TextInput lose focus |
275 | | `clear` | `void` | removes all text from the TextInput |
276 | | `isFocused` | `() => boolean` | returns `true` if the TextInput is currently focused; `false` otherwise |
277 | | `getCurrentLocation` | `() => void` | makes a query to find nearby places based on current location |
278 |
279 | You can access these methods using a ref.
280 |
281 | ### Example
282 |
283 | ```js
284 | import React, { useEffect, useRef } from 'react';
285 | import { GooglePlacesAutocomplete } from 'react-native-google-places-autocomplete';
286 |
287 | const GooglePlacesInput = () => {
288 | const ref = useRef();
289 |
290 | useEffect(() => {
291 | ref.current?.setAddressText('Some Text');
292 | }, []);
293 |
294 | return (
295 | {
299 | // 'details' is provided when fetchDetails = true
300 | console.log(data, details);
301 | }}
302 | query={{
303 | key: 'YOUR API KEY',
304 | language: 'en',
305 | }}
306 | />
307 | );
308 | };
309 |
310 | export default GooglePlacesInput;
311 | ```
312 |
313 | ## Styling
314 |
315 | `GooglePlacesAutocomplete` can be easily customized to meet styles of your app. Pass styles props to `GooglePlacesAutocomplete` with style object for different elements (keys for style object are listed below)
316 |
317 | | key | type |
318 | | --------------------------- | ----------------------- |
319 | | container | object (View) |
320 | | textInputContainer | object (View style) |
321 | | textInput | object (style) |
322 | | listView | object (ListView style) |
323 | | row | object (View style) |
324 | | loader | object (View style) |
325 | | description | object (Text style) |
326 | | predefinedPlacesDescription | object (Text style) |
327 | | separator | object (View style) |
328 | | poweredContainer | object (View style) |
329 | | powered | object (Image style) |
330 |
331 | #### Example
332 |
333 | ```js
334 |
354 | ```
355 |
356 | ### Default Styles
357 |
358 | ```js
359 | {
360 | container: {
361 | flex: 1,
362 | },
363 | textInputContainer: {
364 | flexDirection: 'row',
365 | },
366 | textInput: {
367 | backgroundColor: '#FFFFFF',
368 | height: 44,
369 | borderRadius: 5,
370 | paddingVertical: 5,
371 | paddingHorizontal: 10,
372 | fontSize: 15,
373 | flex: 1,
374 | },
375 | poweredContainer: {
376 | justifyContent: 'flex-end',
377 | alignItems: 'center',
378 | borderBottomRightRadius: 5,
379 | borderBottomLeftRadius: 5,
380 | borderColor: '#c8c7cc',
381 | borderTopWidth: 0.5,
382 | },
383 | powered: {},
384 | listView: {},
385 | row: {
386 | backgroundColor: '#FFFFFF',
387 | padding: 13,
388 | height: 44,
389 | flexDirection: 'row',
390 | },
391 | separator: {
392 | height: 0.5,
393 | backgroundColor: '#c8c7cc',
394 | },
395 | description: {},
396 | loader: {
397 | flexDirection: 'row',
398 | justifyContent: 'flex-end',
399 | height: 20,
400 | },
401 | }
402 | ```
403 |
404 | ## Web Support
405 |
406 | Web support can be enabled via the `requestUrl` prop, by passing in a URL that you can use to proxy your requests. CORS implemented by the Google Places API prevent using this library directly on the web. You will need to use a proxy server. Please be mindful of this limitation when opening an issue.
407 |
408 | The `requestUrl` prop takes an object with two required properties: `useOnPlatform` and `url`, and an optional `headers` property.
409 |
410 | The `url` property is used to set the url that requests will be made to. If you are using the regular google maps API, you need to make sure you are ultimately hitting https://maps.googleapis.com/maps/api.
411 |
412 | `useOnPlatform` configures when the proxy url is used. It can be set to either `web`- will be used only when the device platform is detected as web (but not iOS or Android, or `all` - will always be used.
413 |
414 | You can optionally specify headers to apply to your request in the `headers` object.
415 |
416 | ### Example:
417 |
418 | ```js
419 | import React from 'react';
420 | import { GooglePlacesAutocomplete } from 'react-native-google-places-autocomplete';
421 |
422 | const GooglePlacesInput = () => {
423 | return (
424 | {
427 | // 'details' is provided when fetchDetails = true
428 | console.log(data, details);
429 | }}
430 | query={{
431 | key: 'YOUR API KEY',
432 | language: 'en',
433 | }}
434 | requestUrl={{
435 | useOnPlatform: 'web', // or "all"
436 | url:
437 | 'https://cors-anywhere.herokuapp.com/https://maps.googleapis.com/maps/api', // or any proxy server that hits https://maps.googleapis.com/maps/api
438 | headers: {
439 | Authorization: `an auth token`, // if required for your proxy
440 | },
441 | }}
442 | />
443 | );
444 | };
445 |
446 | export default GooglePlacesInput;
447 | ```
448 |
449 | **_Note:_** The library expects the same response that the Google Maps API would return.
450 |
451 | ## Features
452 |
453 | - [x] Places autocompletion
454 | - [x] iOS and Android compatibility
455 | - [x] Places details fetching + ActivityIndicatorIOS/ProgressBarAndroid loaders
456 | - [x] Customizable using the `styles` parameter
457 | - [x] XHR cancellations when typing fast
458 | - [x] Google Places terms compliant
459 | - [x] Predefined places
460 | - [x] typescript types
461 | - [x] Current location
462 |
463 | ## Compatibility
464 |
465 | This library does not use the iOS, Android or JS SDKs from Google. This comes with some Pros and Cons.
466 |
467 | **Pros:**
468 |
469 | - smaller app size
470 | - better privacy for your users (although Google still tracks server calls)
471 | - no need to keep up with sdk updates
472 |
473 | **Cons:**
474 |
475 | - the library is not compatible with a Application key restrictions
476 | - doesn't work directly on the web without a proxy server
477 | - any Google API change can be a breaking change for the library.
478 |
479 | ### Use Inside a `` or ``
480 |
481 | If you need to include this component inside a ScrolView or FlatList, remember to apply the `keyboardShouldPersistTaps` attribute to all ancestors ScrollView or FlatList (see [this](https://github.com/FaridSafi/react-native-google-places-autocomplete/issues/486#issuecomment-665602257) issue comment).
482 |
483 | ## A word about the Google Maps APIs
484 |
485 | Google Provides a bunch of web APIs for finding an address or place, and getting it’s details.
486 | There are the Google Places Web APIs ([Place Search](https://developers.google.com/places/web-service/search), [Place Details](https://developers.google.com/places/web-service/details), [Place Photos](https://developers.google.com/places/web-service/photos), [Place Autocomplete](https://developers.google.com/places/web-service/autocomplete), [Query Autocomplete](https://developers.google.com/places/web-service/query)) and the [Google Geocode API](https://developers.google.com/maps/documentation/geocoding/intro) .
487 |
488 | The 5 Google Places Web APIs are:
489 |
490 | - **Place Autocomplete -** automatically fills in the name and/or address of a place as users type.
491 | - **Place Details -** returns more detailed information about a specific place (using a place_id that you get from Place Search, Place Autocomplete, or Query Autocomplete).
492 | - **Place Photos -** provides access to the millions of place-related photos stored in Google's Place database (using a reference_id that you get from Place Search, Place Autocomplete, or Query Autocomplete).
493 | - **Place Search -** returns a list of places based on a user's location or search string.
494 | - **Query Autocomplete -** provides a query prediction service for text-based geographic searches, returning suggested queries as users type.
495 |
496 | The **Geocoding API** allows you to convert an address into geographic coordinates (lat, long) and to "reverse geocode", which is the process of converting geographic coordinates into a human-readable address.
497 |
498 | ### Which APIs does this library use?
499 |
500 | Place Autocomplete API, Place Details API, Place Search API and the Geocoding API.
501 |
502 | We use the **Place Autocomplete API** to get suggestions as you type. When you click on a suggestion, we use the **Place Details API** to get more information about the place.
503 |
504 | We use the **Geocoding API** and the **Place Search API** to use your current location to get results.
505 |
506 | Because the query parameters are different for each API, there are 4 different "query" props.
507 |
508 | 1. Autocomplete -> `query` ([options](https://developers.google.com/places/web-service/autocomplete#place_autocomplete_requests))
509 | 2. Place Details -> `GooglePlacesDetailsQuery` ([options](https://developers.google.com/places/web-service/details#PlaceDetailsRequests))
510 | 3. Nearby Search -> `GooglePlacesSearchQuery` ([options](https://developers.google.com/places/web-service/search#PlaceSearchRequests))
511 | 4. Geocode -> `GoogleReverseGeocodingQuery` ([options](https://developers.google.com/maps/documentation/geocoding/intro#GeocodingRequests))
512 |
513 | Number 1 is used while getting autocomplete results.
514 | Number 2 is used when you click on a result.
515 | Number 3 is used when you select 'Current Location' to load nearby results.
516 | Number 4 is used when `nearbyPlacesAPI='GoogleReverseGeocoding'` is set and you select 'Current Location' to load nearby results.
517 |
518 | ## Changelog
519 |
520 | Please see the [releases](https://github.com/FaridSafi/react-native-google-places-autocomplete/releases) tab for the changelog information.
521 |
522 | ## License
523 |
524 | [MIT](LICENSE)
525 |
526 | ### Authors
527 |
528 | - [Farid Safi](https://www.twitter.com/FaridSafi)
529 | - [Maxim Yaskevich](https://www.twitter.com/mayaskme)
530 | - [Guilherme Pontes](https://www.twitter.com/guiiipontes)
531 |
--------------------------------------------------------------------------------
/GooglePlacesAutocomplete.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-native/no-inline-styles */
2 | import debounce from 'lodash.debounce';
3 | import Qs from 'qs';
4 | import uuid from 'react-native-uuid';
5 | import React, {
6 | forwardRef,
7 | useMemo,
8 | useEffect,
9 | useImperativeHandle,
10 | useRef,
11 | useState,
12 | useCallback,
13 | } from 'react';
14 | import {
15 | ActivityIndicator,
16 | FlatList,
17 | Image,
18 | Keyboard,
19 | Platform,
20 | Pressable,
21 | ScrollView,
22 | StyleSheet,
23 | Text,
24 | TextInput,
25 | View,
26 | } from 'react-native';
27 |
28 | // ============================================================================
29 | // CONSTANTS
30 | // ============================================================================
31 |
32 | const defaultStyles = {
33 | container: {
34 | flex: 1,
35 | },
36 | textInputContainer: {
37 | flexDirection: 'row',
38 | },
39 | textInput: {
40 | backgroundColor: '#FFFFFF',
41 | height: 44,
42 | borderRadius: 5,
43 | paddingVertical: 5,
44 | paddingHorizontal: 10,
45 | fontSize: 15,
46 | flex: 1,
47 | marginBottom: 5,
48 | },
49 | listView: {
50 | backgroundColor: '#FFFFFF',
51 | },
52 | row: {
53 | backgroundColor: '#FFFFFF',
54 | padding: 13,
55 | minHeight: 44,
56 | flexDirection: 'row',
57 | },
58 | loader: {
59 | flexDirection: 'row',
60 | justifyContent: 'flex-end',
61 | height: 20,
62 | },
63 | description: {},
64 | separator: {
65 | height: StyleSheet.hairlineWidth,
66 | backgroundColor: '#c8c7cc',
67 | },
68 | poweredContainer: {
69 | justifyContent: 'flex-end',
70 | alignItems: 'center',
71 | borderBottomRightRadius: 5,
72 | borderBottomLeftRadius: 5,
73 | borderColor: '#c8c7cc',
74 | borderTopWidth: 0.5,
75 | },
76 | powered: {},
77 | };
78 |
79 | // ============================================================================
80 | // COMPONENT
81 | // ============================================================================
82 |
83 | export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
84 | // ==========================================================================
85 | // PROPS DESTRUCTURING
86 | // ==========================================================================
87 | const {
88 | autoFillOnNotFound = false,
89 | currentLocation = false,
90 | currentLocationLabel = 'Current location',
91 | debounce: debounceMs = 0,
92 | disableScroll = false,
93 | enableHighAccuracyLocation = true,
94 | enablePoweredByContainer = true,
95 | fetchDetails = false,
96 | filterReverseGeocodingByTypes = [],
97 | GooglePlacesDetailsQuery = {},
98 | GooglePlacesSearchQuery = {
99 | rankby: 'distance',
100 | type: 'restaurant',
101 | },
102 | GoogleReverseGeocodingQuery = {},
103 | isRowScrollable = true,
104 | keyboardShouldPersistTaps = 'always',
105 | listHoverColor = '#ececec',
106 | listUnderlayColor = '#c8c7cc',
107 | listViewDisplayed: listViewDisplayedProp = 'auto',
108 | keepResultsAfterBlur = false,
109 | minLength = 0,
110 | nearbyPlacesAPI = 'GooglePlacesSearch',
111 | numberOfLines = 1,
112 | onFail = () => {},
113 | onNotFound = () => {},
114 | onPress = () => {},
115 | onTimeout = () =>
116 | console.warn('google places autocomplete: request timeout'),
117 | placeholder = '',
118 | predefinedPlaces: predefinedPlacesProp = [],
119 | predefinedPlacesAlwaysVisible = false,
120 | query = {
121 | key: 'missing api key',
122 | language: 'en',
123 | types: 'geocode',
124 | },
125 | styles = {},
126 | suppressDefaultStyles = false,
127 | textInputHide = false,
128 | textInputProps = {},
129 | timeout = 20000,
130 | isNewPlacesAPI = false,
131 | fields = '*',
132 | ...restProps
133 | } = props;
134 |
135 | // ==========================================================================
136 | // STATE & REFS
137 | // ==========================================================================
138 | const predefinedPlaces = useMemo(() => predefinedPlacesProp || [], [
139 | predefinedPlacesProp,
140 | ]);
141 |
142 | // Store results array - useRef prevents re-renders when updating results, allows access to latest results in callbacks
143 | const resultsRef = useRef([]);
144 |
145 | // Store active XMLHttpRequest objects - needed to abort requests when component unmounts or new search starts
146 | const requestsRef = useRef([]);
147 |
148 | // Track if navigator warning has been shown - prevents duplicate console warnings
149 | const hasWarnedAboutNavigator = useRef(false);
150 |
151 | // Reference to TextInput component - enables imperative methods (focus, blur, clear) via ref
152 | const inputRef = useRef(null);
153 |
154 | // Store current query object - allows access to latest query in callbacks without stale closures
155 | const queryRef = useRef(query);
156 |
157 | // Store previous query string - used to detect query changes without causing re-renders
158 | const prevQueryStringRef = useRef(JSON.stringify(query));
159 |
160 | // Store latest _request function - ensures debounced function always calls current version with latest closures
161 | const requestRef = useRef(_request);
162 | const queryString = useMemo(() => JSON.stringify(query), [query]);
163 |
164 | const [stateText, setStateText] = useState('');
165 | const [dataSource, setDataSource] = useState([]);
166 | const [listViewDisplayed, setListViewDisplayed] = useState(
167 | listViewDisplayedProp === 'auto' ? false : listViewDisplayedProp,
168 | );
169 | const [url, setUrl] = useState('');
170 | const [listLoaderDisplayed, setListLoaderDisplayed] = useState(false);
171 | const [sessionToken, setSessionToken] = useState(uuid.v4());
172 |
173 | // ==========================================================================
174 | // UTILITY FUNCTIONS
175 | // ==========================================================================
176 |
177 | const hasNavigator = useCallback(() => {
178 | if (navigator?.geolocation) {
179 | return true;
180 | }
181 | if (!hasWarnedAboutNavigator.current) {
182 | if (Platform.OS === 'web') {
183 | console.warn(
184 | 'Geolocation is not available. For web, ensure your site is served over HTTPS or localhost to use geolocation features.',
185 | );
186 | } else {
187 | console.warn(
188 | 'Geolocation is not available. For React Native, you may need to install and configure @react-native-community/geolocation or expo-location to enable currentLocation.',
189 | );
190 | }
191 | hasWarnedAboutNavigator.current = true;
192 | }
193 | return false;
194 | }, []);
195 |
196 | const supportedPlatform = () => {
197 | if (Platform.OS === 'web' && !props.requestUrl) {
198 | console.warn(
199 | 'This library cannot be used for the web unless you specify the requestUrl prop. See https://git.io/JflFv for more for details.',
200 | );
201 | return false;
202 | }
203 | return true;
204 | };
205 |
206 | const getRequestUrl = (requestUrl) => {
207 | if (requestUrl) {
208 | if (requestUrl.useOnPlatform === 'all') {
209 | return requestUrl.url;
210 | }
211 | if (requestUrl.useOnPlatform === 'web') {
212 | return Platform.select({
213 | web: requestUrl.url,
214 | default: 'https://maps.googleapis.com/maps/api',
215 | });
216 | }
217 | }
218 | return 'https://maps.googleapis.com/maps/api';
219 | };
220 |
221 | const getRequestHeaders = (requestUrl) => {
222 | return requestUrl?.headers || {};
223 | };
224 |
225 | const setRequestHeaders = (request, headers) => {
226 | Object.keys(headers).forEach((headerKey) =>
227 | request.setRequestHeader(headerKey, headers[headerKey]),
228 | );
229 | };
230 |
231 | const requestShouldUseWithCredentials = useCallback(() => {
232 | return url === 'https://maps.googleapis.com/maps/api';
233 | }, [url]);
234 |
235 | const _abortRequests = useCallback(() => {
236 | requestsRef.current.forEach((request) => {
237 | request.onreadystatechange = null;
238 | request.abort();
239 | });
240 | requestsRef.current = [];
241 | }, []);
242 |
243 | // ==========================================================================
244 | // DATA PROCESSING FUNCTIONS
245 | // ==========================================================================
246 |
247 | const buildRowsFromResults = useCallback(
248 | (results, text) => {
249 | let res = [];
250 | // Show predefined places if:
251 | // 1. No text entered and no results, OR
252 | // 2. predefinedPlacesAlwaysVisible is true
253 | const shouldDisplayPredefinedPlaces =
254 | (!text || text.length === 0) && results.length === 0;
255 | if (
256 | shouldDisplayPredefinedPlaces ||
257 | predefinedPlacesAlwaysVisible === true
258 | ) {
259 | if (predefinedPlaces.length > 0) {
260 | res = [
261 | ...predefinedPlaces.filter((place) => place?.description?.length),
262 | ];
263 | }
264 |
265 | if (currentLocation === true && hasNavigator()) {
266 | res.unshift({
267 | description: currentLocationLabel,
268 | isCurrentLocation: true,
269 | });
270 | }
271 | }
272 |
273 | res = res.map((place) => ({
274 | ...place,
275 | isPredefinedPlace: true,
276 | }));
277 |
278 | return [...res, ...results];
279 | },
280 | [
281 | predefinedPlacesAlwaysVisible,
282 | predefinedPlaces,
283 | currentLocation,
284 | currentLocationLabel,
285 | hasNavigator,
286 | ],
287 | );
288 |
289 | const _filterResultsByTypes = useCallback((unfilteredResults, types) => {
290 | if (!types || types.length === 0) return unfilteredResults;
291 |
292 | const results = [];
293 | for (let i = 0; i < unfilteredResults.length; i++) {
294 | let found = false;
295 |
296 | for (let j = 0; j < types.length; j++) {
297 | if (unfilteredResults[i].types?.indexOf(types[j]) !== -1) {
298 | found = true;
299 | break;
300 | }
301 | }
302 |
303 | if (found === true) {
304 | results.push(unfilteredResults[i]);
305 | }
306 | }
307 | return results;
308 | }, []);
309 |
310 | const _filterResultsByPlacePredictions = (unfilteredResults) => {
311 | const results = [];
312 | for (let i = 0; i < unfilteredResults.length; i++) {
313 | if (unfilteredResults[i].placePrediction) {
314 | results.push({
315 | description: unfilteredResults[i].placePrediction.text?.text,
316 | place_id: unfilteredResults[i].placePrediction.placeId,
317 | reference: unfilteredResults[i].placePrediction.placeId,
318 | structured_formatting: {
319 | main_text:
320 | unfilteredResults[i].placePrediction.structuredFormat?.mainText
321 | ?.text,
322 | secondary_text:
323 | unfilteredResults[i].placePrediction.structuredFormat
324 | ?.secondaryText?.text,
325 | },
326 | types: unfilteredResults[i].placePrediction.types ?? [],
327 | });
328 | }
329 | }
330 | return results;
331 | };
332 |
333 | const _getPredefinedPlace = (rowData) => {
334 | if (rowData.isPredefinedPlace !== true) {
335 | return rowData;
336 | }
337 |
338 | if (predefinedPlaces.length > 0) {
339 | for (let i = 0; i < predefinedPlaces.length; i++) {
340 | if (predefinedPlaces[i].description === rowData.description) {
341 | return predefinedPlaces[i];
342 | }
343 | }
344 | }
345 |
346 | return rowData;
347 | };
348 |
349 | // ==========================================================================
350 | // API REQUEST FUNCTIONS
351 | // ==========================================================================
352 |
353 | const _requestNearby = useCallback(
354 | (latitude, longitude) => {
355 | _abortRequests();
356 |
357 | if (
358 | latitude !== undefined &&
359 | longitude !== undefined &&
360 | latitude !== null &&
361 | longitude !== null
362 | ) {
363 | const request = new XMLHttpRequest();
364 | requestsRef.current.push(request);
365 | request.timeout = timeout;
366 | request.ontimeout = onTimeout;
367 | request.onreadystatechange = () => {
368 | if (request.readyState !== 4) {
369 | setListLoaderDisplayed(true);
370 | return;
371 | }
372 |
373 | setListLoaderDisplayed(false);
374 | if (request.status === 200) {
375 | const responseJSON = JSON.parse(request.responseText);
376 |
377 | _disableRowLoaders();
378 |
379 | if (typeof responseJSON.results !== 'undefined') {
380 | let results = [];
381 | if (nearbyPlacesAPI === 'GoogleReverseGeocoding') {
382 | results = _filterResultsByTypes(
383 | responseJSON.results,
384 | filterReverseGeocodingByTypes,
385 | );
386 | } else {
387 | results = responseJSON.results;
388 | }
389 |
390 | resultsRef.current = results;
391 | const newDataSource = buildRowsFromResults(results);
392 | setDataSource(newDataSource);
393 | // Auto-show list when results arrive if in 'auto' mode
394 | if (
395 | listViewDisplayedProp === 'auto' &&
396 | newDataSource.length > 0
397 | ) {
398 | setListViewDisplayed(true);
399 | }
400 | }
401 | if (typeof responseJSON.error_message !== 'undefined') {
402 | if (!onFail) {
403 | console.warn(
404 | 'google places autocomplete: ' + responseJSON.error_message,
405 | );
406 | } else {
407 | onFail(responseJSON.error_message);
408 | }
409 | }
410 | }
411 | };
412 |
413 | let requestUrl = '';
414 | if (nearbyPlacesAPI === 'GoogleReverseGeocoding') {
415 | // your key must be allowed to use Google Maps Geocoding API
416 | requestUrl =
417 | `${url}/geocode/json?` +
418 | Qs.stringify({
419 | latlng: latitude + ',' + longitude,
420 | key: query.key,
421 | ...GoogleReverseGeocodingQuery,
422 | });
423 | } else {
424 | requestUrl =
425 | `${url}/place/nearbysearch/json?` +
426 | Qs.stringify({
427 | location: latitude + ',' + longitude,
428 | key: query.key,
429 | ...GooglePlacesSearchQuery,
430 | });
431 | }
432 |
433 | request.open('GET', requestUrl);
434 |
435 | request.withCredentials = requestShouldUseWithCredentials();
436 | setRequestHeaders(request, getRequestHeaders(props.requestUrl));
437 |
438 | request.send();
439 | } else {
440 | resultsRef.current = [];
441 | setDataSource(buildRowsFromResults([]));
442 | }
443 | },
444 | [
445 | _abortRequests,
446 | timeout,
447 | onTimeout,
448 | _disableRowLoaders,
449 | nearbyPlacesAPI,
450 | _filterResultsByTypes,
451 | filterReverseGeocodingByTypes,
452 | buildRowsFromResults,
453 | listViewDisplayedProp,
454 | onFail,
455 | url,
456 | query,
457 | GoogleReverseGeocodingQuery,
458 | GooglePlacesSearchQuery,
459 | requestShouldUseWithCredentials,
460 | props.requestUrl,
461 | ],
462 | );
463 |
464 | const _request = (text) => {
465 | _abortRequests();
466 |
467 | if (!url) {
468 | return;
469 | }
470 |
471 | if (supportedPlatform() && text && text.length >= minLength) {
472 | const request = new XMLHttpRequest();
473 | requestsRef.current.push(request);
474 |
475 | request.timeout = timeout;
476 | request.ontimeout = onTimeout;
477 | request.onreadystatechange = () => {
478 | if (request.readyState !== 4) {
479 | setListLoaderDisplayed(true);
480 | return;
481 | }
482 |
483 | setListLoaderDisplayed(false);
484 |
485 | if (request.status === 200) {
486 | const responseJSON = JSON.parse(request.responseText);
487 |
488 | if (typeof responseJSON.predictions !== 'undefined') {
489 | const results =
490 | nearbyPlacesAPI === 'GoogleReverseGeocoding'
491 | ? _filterResultsByTypes(
492 | responseJSON.predictions,
493 | filterReverseGeocodingByTypes,
494 | )
495 | : responseJSON.predictions;
496 |
497 | resultsRef.current = results;
498 | const newDataSource = buildRowsFromResults(results, text);
499 | setDataSource(newDataSource);
500 | // Auto-show list when results arrive if in 'auto' mode
501 | if (listViewDisplayedProp === 'auto' && newDataSource.length > 0) {
502 | setListViewDisplayed(true);
503 | }
504 | }
505 | if (typeof responseJSON.suggestions !== 'undefined') {
506 | const results = _filterResultsByPlacePredictions(
507 | responseJSON.suggestions,
508 | );
509 |
510 | resultsRef.current = results;
511 | const newDataSource = buildRowsFromResults(results, text);
512 | setDataSource(newDataSource);
513 | // Auto-show list when results arrive if in 'auto' mode
514 | if (listViewDisplayedProp === 'auto' && newDataSource.length > 0) {
515 | setListViewDisplayed(true);
516 | }
517 | }
518 | if (typeof responseJSON.error_message !== 'undefined') {
519 | if (!onFail) {
520 | console.warn(
521 | 'google places autocomplete: ' + responseJSON.error_message,
522 | );
523 | } else {
524 | onFail(responseJSON.error_message);
525 | }
526 | }
527 | } else {
528 | console.warn(
529 | 'google places autocomplete: request could not be completed or has been aborted',
530 | );
531 | }
532 | };
533 |
534 | if (props.preProcess) {
535 | setStateText(props.preProcess(text));
536 | }
537 |
538 | if (isNewPlacesAPI) {
539 | const keyQueryParam = query.key
540 | ? '?' +
541 | Qs.stringify({
542 | key: query.key,
543 | })
544 | : '';
545 | request.open('POST', `${url}/v1/places:autocomplete${keyQueryParam}`);
546 | } else {
547 | request.open(
548 | 'GET',
549 | `${url}/place/autocomplete/json?input=` +
550 | encodeURIComponent(text) +
551 | '&' +
552 | Qs.stringify(query),
553 | );
554 | }
555 |
556 | request.withCredentials = requestShouldUseWithCredentials();
557 | setRequestHeaders(request, getRequestHeaders(props.requestUrl));
558 |
559 | if (isNewPlacesAPI) {
560 | const { key, locationbias, types, ...rest } = query;
561 | request.send(
562 | JSON.stringify({
563 | input: text,
564 | sessionToken,
565 | ...rest,
566 | }),
567 | );
568 | } else {
569 | request.send();
570 | }
571 | } else {
572 | resultsRef.current = [];
573 | setDataSource(buildRowsFromResults([]));
574 | }
575 | };
576 |
577 | const getCurrentLocation = useCallback(() => {
578 | let options = {
579 | enableHighAccuracy: false,
580 | timeout: 20000,
581 | maximumAge: 1000,
582 | };
583 |
584 | if (enableHighAccuracyLocation && Platform.OS === 'android') {
585 | options = {
586 | enableHighAccuracy: true,
587 | timeout: 20000,
588 | };
589 | }
590 | const getCurrentPosition =
591 | navigator.geolocation.getCurrentPosition ||
592 | navigator.geolocation.default?.getCurrentPosition;
593 |
594 | if (getCurrentPosition) {
595 | getCurrentPosition(
596 | (position) => {
597 | if (nearbyPlacesAPI === 'None') {
598 | const currentLocationData = {
599 | description: currentLocationLabel,
600 | geometry: {
601 | location: {
602 | lat: position.coords.latitude,
603 | lng: position.coords.longitude,
604 | },
605 | },
606 | };
607 |
608 | _disableRowLoaders();
609 | onPress(currentLocationData, currentLocationData);
610 | } else {
611 | _requestNearby(position.coords.latitude, position.coords.longitude);
612 | }
613 | },
614 | (error) => {
615 | _disableRowLoaders();
616 | console.error(error.message);
617 | },
618 | options,
619 | );
620 | }
621 | }, [
622 | enableHighAccuracyLocation,
623 | currentLocationLabel,
624 | nearbyPlacesAPI,
625 | _disableRowLoaders,
626 | onPress,
627 | _requestNearby,
628 | ]);
629 |
630 | // ==========================================================================
631 | // EVENT HANDLERS
632 | // ==========================================================================
633 |
634 | const _enableRowLoader = (rowData) => {
635 | const rows = buildRowsFromResults(resultsRef.current);
636 | for (let i = 0; i < rows.length; i++) {
637 | if (
638 | rows[i].place_id === rowData.place_id ||
639 | (rows[i].isCurrentLocation === true &&
640 | rowData.isCurrentLocation === true)
641 | ) {
642 | rows[i].isLoading = true;
643 | setDataSource(rows);
644 | break;
645 | }
646 | }
647 | };
648 |
649 | const _disableRowLoaders = useCallback(() => {
650 | for (let i = 0; i < resultsRef.current.length; i++) {
651 | if (resultsRef.current[i].isLoading === true) {
652 | resultsRef.current[i].isLoading = false;
653 | }
654 | }
655 |
656 | setDataSource(buildRowsFromResults(resultsRef.current));
657 | }, [buildRowsFromResults]);
658 |
659 | const _onPress = (rowData) => {
660 | if (rowData.isPredefinedPlace !== true && fetchDetails === true) {
661 | if (rowData.isLoading === true) {
662 | // already requesting
663 | return;
664 | }
665 |
666 | Keyboard.dismiss();
667 |
668 | _abortRequests();
669 |
670 | // display loader
671 | _enableRowLoader(rowData);
672 |
673 | // fetch details
674 | const request = new XMLHttpRequest();
675 | requestsRef.current.push(request);
676 | request.timeout = timeout;
677 | request.ontimeout = onTimeout;
678 | request.onreadystatechange = () => {
679 | if (request.readyState !== 4) return;
680 |
681 | if (request.status === 200) {
682 | const responseJSON = JSON.parse(request.responseText);
683 | if (
684 | responseJSON.status === 'OK' ||
685 | (isNewPlacesAPI && responseJSON.id)
686 | ) {
687 | const details = isNewPlacesAPI ? responseJSON : responseJSON.result;
688 | _disableRowLoaders();
689 | _onBlur();
690 |
691 | setStateText(_renderDescription(rowData));
692 |
693 | delete rowData.isLoading;
694 | onPress(rowData, details);
695 | } else {
696 | _disableRowLoaders();
697 |
698 | if (autoFillOnNotFound) {
699 | setStateText(_renderDescription(rowData));
700 | delete rowData.isLoading;
701 | }
702 |
703 | if (!onNotFound) {
704 | console.warn(
705 | 'google places autocomplete: ' + responseJSON.status,
706 | );
707 | } else {
708 | onNotFound(responseJSON);
709 | }
710 | }
711 | } else {
712 | _disableRowLoaders();
713 |
714 | if (!onFail) {
715 | console.warn(
716 | 'google places autocomplete: request could not be completed or has been aborted',
717 | );
718 | } else {
719 | onFail('request could not be completed or has been aborted');
720 | }
721 | }
722 | };
723 |
724 | if (isNewPlacesAPI) {
725 | request.open(
726 | 'GET',
727 | `${url}/v1/places/${rowData.place_id}?` +
728 | Qs.stringify({
729 | key: query.key,
730 | sessionToken,
731 | fields,
732 | }),
733 | );
734 | setSessionToken(uuid.v4());
735 | } else {
736 | request.open(
737 | 'GET',
738 | `${url}/place/details/json?` +
739 | Qs.stringify({
740 | key: query.key,
741 | placeid: rowData.place_id,
742 | language: query.language,
743 | ...GooglePlacesDetailsQuery,
744 | }),
745 | );
746 | }
747 |
748 | request.withCredentials = requestShouldUseWithCredentials();
749 | setRequestHeaders(request, getRequestHeaders(props.requestUrl));
750 |
751 | request.send();
752 | } else if (rowData.isCurrentLocation === true) {
753 | // display loader
754 | _enableRowLoader(rowData);
755 |
756 | setStateText(_renderDescription(rowData));
757 |
758 | delete rowData.isLoading;
759 | getCurrentLocation();
760 | } else {
761 | setStateText(_renderDescription(rowData));
762 |
763 | _onBlur();
764 | delete rowData.isLoading;
765 | const predefinedPlace = _getPredefinedPlace(rowData);
766 |
767 | // sending predefinedPlace as details for predefined places
768 | onPress(predefinedPlace, predefinedPlace);
769 | }
770 | };
771 |
772 | const _onChangeText = (text) => {
773 | setStateText(text);
774 | debounceData(text);
775 | };
776 |
777 | const _handleChangeText = (text) => {
778 | _onChangeText(text);
779 |
780 | const onChangeText = textInputProps?.onChangeText;
781 |
782 | if (onChangeText) {
783 | onChangeText(text);
784 | }
785 | };
786 |
787 | const isNewFocusInAutocompleteResultList = ({
788 | relatedTarget,
789 | currentTarget,
790 | }) => {
791 | if (!relatedTarget) return false;
792 |
793 | let node = relatedTarget.parentNode;
794 |
795 | while (node) {
796 | if (node.id === 'result-list-id') return true;
797 | node = node.parentNode;
798 | }
799 |
800 | return false;
801 | };
802 |
803 | const _onBlur = (e) => {
804 | if (e && isNewFocusInAutocompleteResultList(e)) return;
805 |
806 | if (!keepResultsAfterBlur) {
807 | setListViewDisplayed(false);
808 | }
809 | inputRef?.current?.blur();
810 | };
811 |
812 | const _onFocus = () => {
813 | setListViewDisplayed(true);
814 | };
815 |
816 | // ==========================================================================
817 | // RENDER FUNCTIONS
818 | // ==========================================================================
819 |
820 | const _renderDescription = (rowData) => {
821 | if (props.renderDescription) {
822 | return props.renderDescription(rowData);
823 | }
824 |
825 | return rowData.description || rowData.formatted_address || rowData.name;
826 | };
827 |
828 | const _getRowLoader = () => {
829 | return ;
830 | };
831 |
832 | const _renderLoader = (rowData) => {
833 | if (rowData.isLoading === true) {
834 | return (
835 |
841 | {_getRowLoader()}
842 |
843 | );
844 | }
845 |
846 | return null;
847 | };
848 |
849 | const _renderRowData = (rowData, index) => {
850 | if (props.renderRow) {
851 | return props.renderRow(rowData, index);
852 | }
853 |
854 | return (
855 |
863 | {_renderDescription(rowData)}
864 |
865 | );
866 | };
867 |
868 | const _renderRow = (rowData = {}, index) => {
869 | return (
870 |
880 | [
882 | isRowScrollable ? { minWidth: '100%' } : { width: '100%' },
883 | {
884 | backgroundColor: pressed
885 | ? listUnderlayColor
886 | : hovered
887 | ? listHoverColor
888 | : undefined,
889 | },
890 | ]}
891 | onPress={() => _onPress(rowData)}
892 | onBlur={_onBlur}
893 | >
894 |
901 | {_renderLoader(rowData)}
902 | {_renderRowData(rowData, index)}
903 |
904 |
905 |
906 | );
907 | };
908 |
909 | const _renderSeparator = (sectionID, rowID) => {
910 | if (rowID === dataSource.length - 1) {
911 | return null;
912 | }
913 |
914 | return (
915 |
922 | );
923 | };
924 |
925 | const _shouldShowPoweredLogo = () => {
926 | if (!enablePoweredByContainer || dataSource.length === 0) {
927 | return false;
928 | }
929 |
930 | for (let i = 0; i < dataSource.length; i++) {
931 | const row = dataSource[i];
932 |
933 | if (!('isCurrentLocation' in row) && !('isPredefinedPlace' in row)) {
934 | return true;
935 | }
936 | }
937 |
938 | return false;
939 | };
940 |
941 | const _renderPoweredLogo = () => {
942 | if (!_shouldShowPoweredLogo()) {
943 | return null;
944 | }
945 |
946 | return (
947 |
954 |
962 |
963 | );
964 | };
965 |
966 | const _renderLeftButton = () => {
967 | if (props.renderLeftButton) {
968 | return props.renderLeftButton();
969 | }
970 | return null;
971 | };
972 |
973 | const _renderRightButton = () => {
974 | if (props.renderRightButton) {
975 | return props.renderRightButton();
976 | }
977 | return null;
978 | };
979 |
980 | const _getFlatList = () => {
981 | const keyExtractor = (item, index) => {
982 | // Use stable keys based on item data
983 | if (item.place_id) {
984 | return `place_${item.place_id}_${index}`;
985 | }
986 | if (item.isCurrentLocation) {
987 | return 'current_location';
988 | }
989 | if (item.isPredefinedPlace && item.description) {
990 | return `predefined_${item.description}_${index}`;
991 | }
992 | // Fallback to index-based key (should rarely happen)
993 | return `item_${index}`;
994 | };
995 |
996 | // Show list if:
997 | // 1. Platform is supported
998 | // 2. There's data to show (dataSource has items)
999 | // 3. listViewDisplayed is true OR we're in 'auto' mode (auto-shows when data exists)
1000 | const isAutoMode =
1001 | listViewDisplayedProp === 'auto' || listViewDisplayedProp === undefined;
1002 | const shouldShowList =
1003 | supportedPlatform() &&
1004 | dataSource.length > 0 &&
1005 | (listViewDisplayed === true || isAutoMode);
1006 |
1007 | if (shouldShowList) {
1008 | return (
1009 | _renderRow(item, index)}
1022 | ListEmptyComponent={
1023 | listLoaderDisplayed
1024 | ? props.listLoaderComponent
1025 | : stateText.length > minLength && props.listEmptyComponent
1026 | }
1027 | ListHeaderComponent={
1028 | props.renderHeaderComponent &&
1029 | props.renderHeaderComponent(stateText)
1030 | }
1031 | ListFooterComponent={_renderPoweredLogo}
1032 | {...restProps}
1033 | />
1034 | );
1035 | }
1036 |
1037 | return null;
1038 | };
1039 |
1040 | // ==========================================================================
1041 | // EFFECTS
1042 | // ==========================================================================
1043 |
1044 | // Update query ref when query changes
1045 | useEffect(() => {
1046 | queryRef.current = query;
1047 | }, [query]);
1048 |
1049 | // Initialize URL from requestUrl prop
1050 | useEffect(() => {
1051 | setUrl(getRequestUrl(props.requestUrl));
1052 | }, [props.requestUrl]);
1053 |
1054 | // Initialize dataSource on mount
1055 | useEffect(() => {
1056 | setDataSource(buildRowsFromResults([]));
1057 | // eslint-disable-next-line react-hooks/exhaustive-deps
1058 | }, []);
1059 |
1060 | // Keep requestRef updated
1061 | requestRef.current = _request;
1062 |
1063 | // Debounce setup
1064 | const debounceData = useMemo(() => {
1065 | return debounce((text) => requestRef.current(text), debounceMs);
1066 | }, [debounceMs]);
1067 |
1068 | useEffect(() => {
1069 | return () => {
1070 | // Cleanup debounced function on unmount
1071 | if (debounceData.cancel) {
1072 | debounceData.cancel();
1073 | }
1074 | };
1075 | }, [debounceData]);
1076 |
1077 | // Reload search when query changes (using string comparison to avoid object reference issues)
1078 | useEffect(() => {
1079 | const queryChanged = prevQueryStringRef.current !== queryString;
1080 |
1081 | if (queryChanged) {
1082 | prevQueryStringRef.current = queryString;
1083 | if (stateText && stateText.length >= minLength) {
1084 | debounceData(stateText);
1085 | }
1086 | }
1087 |
1088 | return () => {
1089 | _abortRequests();
1090 | };
1091 | }, [queryString, debounceData, stateText, minLength, _abortRequests]);
1092 |
1093 | // Auto-show list when dataSource has items in 'auto' mode
1094 | useEffect(() => {
1095 | if (
1096 | listViewDisplayedProp === 'auto' &&
1097 | dataSource.length > 0 &&
1098 | !listViewDisplayed
1099 | ) {
1100 | setListViewDisplayed(true);
1101 | }
1102 | }, [dataSource.length, listViewDisplayedProp, listViewDisplayed]);
1103 |
1104 | // ==========================================================================
1105 | // IMPERATIVE HANDLE
1106 | // ==========================================================================
1107 |
1108 | useImperativeHandle(
1109 | ref,
1110 | () => ({
1111 | setAddressText: (address) => {
1112 | setStateText(address);
1113 | },
1114 | getAddressText: () => stateText,
1115 | blur: () => inputRef.current?.blur(),
1116 | focus: () => inputRef.current?.focus(),
1117 | isFocused: () => inputRef.current?.isFocused(),
1118 | clear: () => inputRef.current?.clear(),
1119 | getCurrentLocation,
1120 | }),
1121 | [stateText, getCurrentLocation],
1122 | );
1123 |
1124 | // ==========================================================================
1125 | // MAIN RENDER
1126 | // ==========================================================================
1127 |
1128 | const {
1129 | onFocus: textInputOnFocus,
1130 | onBlur: textInputOnBlur,
1131 | onChangeText: textInputOnChangeText, // destructuring here stops this being set after onChangeText={_handleChangeText}
1132 | clearButtonMode,
1133 | InputComp,
1134 | ...userProps
1135 | } = textInputProps || {};
1136 | const TextInputComp = InputComp || TextInput;
1137 |
1138 | return (
1139 |
1146 | {!textInputHide && (
1147 |
1153 | {_renderLeftButton()}
1154 | {
1165 | _onFocus();
1166 | textInputOnFocus(e);
1167 | }
1168 | : _onFocus
1169 | }
1170 | onBlur={
1171 | textInputOnBlur
1172 | ? (e) => {
1173 | _onBlur(e);
1174 | textInputOnBlur(e);
1175 | }
1176 | : _onBlur
1177 | }
1178 | clearButtonMode={clearButtonMode || 'while-editing'}
1179 | onChangeText={_handleChangeText}
1180 | {...userProps}
1181 | />
1182 | {_renderRightButton()}
1183 |
1184 | )}
1185 | {props.inbetweenCompo}
1186 | {_getFlatList()}
1187 | {props.children}
1188 |
1189 | );
1190 | });
1191 |
1192 | GooglePlacesAutocomplete.displayName = 'GooglePlacesAutocomplete';
1193 |
1194 | export default { GooglePlacesAutocomplete };
1195 |
--------------------------------------------------------------------------------