react-native-google-places-autocomplete 2.5.7 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,7 @@
1
1
  /* eslint-disable react-native/no-inline-styles */
2
2
  import debounce from 'lodash.debounce';
3
- import PropTypes from 'prop-types';
4
3
  import Qs from 'qs';
5
- import { v4 as uuidv4 } from 'uuid';
4
+ import { randomUUID } from 'expo-crypto';
6
5
  import React, {
7
6
  forwardRef,
8
7
  useMemo,
@@ -26,6 +25,10 @@ import {
26
25
  View,
27
26
  } from 'react-native';
28
27
 
28
+ // ============================================================================
29
+ // CONSTANTS
30
+ // ============================================================================
31
+
29
32
  const defaultStyles = {
30
33
  container: {
31
34
  flex: 1,
@@ -43,7 +46,9 @@ const defaultStyles = {
43
46
  flex: 1,
44
47
  marginBottom: 5,
45
48
  },
46
- listView: {},
49
+ listView: {
50
+ backgroundColor: '#FFFFFF',
51
+ },
47
52
  row: {
48
53
  backgroundColor: '#FFFFFF',
49
54
  padding: 13,
@@ -71,61 +76,134 @@ const defaultStyles = {
71
76
  powered: {},
72
77
  };
73
78
 
79
+ // ============================================================================
80
+ // COMPONENT
81
+ // ============================================================================
82
+
74
83
  export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
75
- let _results = [];
76
- let _requests = [];
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]);
77
163
 
78
- const hasNavigator = () => {
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(randomUUID());
172
+
173
+ // ==========================================================================
174
+ // UTILITY FUNCTIONS
175
+ // ==========================================================================
176
+
177
+ const hasNavigator = useCallback(() => {
79
178
  if (navigator?.geolocation) {
80
179
  return true;
81
- } else {
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) {
82
198
  console.warn(
83
- 'If you are using React Native v0.60.0+ you must follow these instructions to enable currentLocation: https://git.io/Jf4AR',
199
+ 'This library cannot be used for the web unless you specify the requestUrl prop. See https://git.io/JflFv for more for details.',
84
200
  );
85
201
  return false;
86
202
  }
203
+ return true;
87
204
  };
88
205
 
89
- const buildRowsFromResults = useCallback(
90
- (results, text) => {
91
- let res = [];
92
- const shouldDisplayPredefinedPlaces = text
93
- ? results.length === 0 && text.length === 0
94
- : results.length === 0;
95
- if (
96
- shouldDisplayPredefinedPlaces ||
97
- props.predefinedPlacesAlwaysVisible === true
98
- ) {
99
- res = [
100
- ...props.predefinedPlaces.filter(
101
- (place) => place?.description.length,
102
- ),
103
- ];
104
-
105
- if (props.currentLocation === true && hasNavigator()) {
106
- res.unshift({
107
- description: props.currentLocationLabel,
108
- isCurrentLocation: true,
109
- });
110
- }
111
- }
112
-
113
- res = res.map((place) => ({
114
- ...place,
115
- isPredefinedPlace: true,
116
- }));
117
-
118
- return [...res, ...results];
119
- },
120
- [
121
- props.currentLocation,
122
- props.currentLocationLabel,
123
- props.predefinedPlaces,
124
- props.predefinedPlacesAlwaysVisible,
125
- ],
126
- );
127
-
128
- const getRequestUrl = useCallback((requestUrl) => {
206
+ const getRequestUrl = (requestUrl) => {
129
207
  if (requestUrl) {
130
208
  if (requestUrl.useOnPlatform === 'all') {
131
209
  return requestUrl.url;
@@ -136,295 +214,87 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
136
214
  default: 'https://maps.googleapis.com/maps/api',
137
215
  });
138
216
  }
139
- } else {
140
- return 'https://maps.googleapis.com/maps/api';
141
217
  }
142
- }, []);
218
+ return 'https://maps.googleapis.com/maps/api';
219
+ };
143
220
 
144
221
  const getRequestHeaders = (requestUrl) => {
145
222
  return requestUrl?.headers || {};
146
223
  };
147
224
 
148
225
  const setRequestHeaders = (request, headers) => {
149
- Object.keys(headers).map((headerKey) =>
226
+ Object.keys(headers).forEach((headerKey) =>
150
227
  request.setRequestHeader(headerKey, headers[headerKey]),
151
228
  );
152
229
  };
153
230
 
154
- const [stateText, setStateText] = useState('');
155
- const [dataSource, setDataSource] = useState(buildRowsFromResults([]));
156
- const [listViewDisplayed, setListViewDisplayed] = useState(
157
- props.listViewDisplayed === 'auto' ? false : props.listViewDisplayed,
158
- );
159
- const [url, setUrl] = useState(getRequestUrl(props.requestUrl));
160
- const [listLoaderDisplayed, setListLoaderDisplayed] = useState(false);
161
-
162
- const inputRef = useRef();
163
- const [sessionToken, setSessionToken] = useState(uuidv4());
164
- useEffect(() => {
165
- setUrl(getRequestUrl(props.requestUrl));
166
- }, [getRequestUrl, props.requestUrl]);
167
-
168
- useEffect(() => {
169
- // This will load the search results after the query object ref gets changed
170
- _handleChangeText(stateText);
171
- return () => {
172
- _abortRequests();
173
- };
174
- // eslint-disable-next-line react-hooks/exhaustive-deps
175
- }, [props.query]);
231
+ const requestShouldUseWithCredentials = useCallback(() => {
232
+ return url === 'https://maps.googleapis.com/maps/api';
233
+ }, [url]);
176
234
 
177
- useEffect(() => {
178
- // Update dataSource if props.predefinedPlaces changed
179
- setDataSource(buildRowsFromResults([]));
180
- }, [buildRowsFromResults, props.predefinedPlaces]);
181
-
182
- useImperativeHandle(ref, () => ({
183
- setAddressText: (address) => {
184
- setStateText(address);
185
- },
186
- getAddressText: () => stateText,
187
- blur: () => inputRef.current.blur(),
188
- focus: () => inputRef.current.focus(),
189
- isFocused: () => inputRef.current.isFocused(),
190
- clear: () => inputRef.current.clear(),
191
- getCurrentLocation,
192
- }));
193
-
194
- const requestShouldUseWithCredentials = () =>
195
- url === 'https://maps.googleapis.com/maps/api';
196
-
197
- const _abortRequests = () => {
198
- _requests.map((i) => {
199
- i.onreadystatechange = null;
200
- i.abort();
235
+ const _abortRequests = useCallback(() => {
236
+ requestsRef.current.forEach((request) => {
237
+ request.onreadystatechange = null;
238
+ request.abort();
201
239
  });
202
- _requests = [];
203
- };
204
-
205
- const supportedPlatform = () => {
206
- if (Platform.OS === 'web' && !props.requestUrl) {
207
- console.warn(
208
- 'This library cannot be used for the web unless you specify the requestUrl prop. See https://git.io/JflFv for more for details.',
209
- );
210
- return false;
211
- } else {
212
- return true;
213
- }
214
- };
215
-
216
- const getCurrentLocation = () => {
217
- let options = {
218
- enableHighAccuracy: false,
219
- timeout: 20000,
220
- maximumAge: 1000,
221
- };
222
-
223
- if (props.enableHighAccuracyLocation && Platform.OS === 'android') {
224
- options = {
225
- enableHighAccuracy: true,
226
- timeout: 20000,
227
- };
228
- }
229
- const getCurrentPosition =
230
- navigator.geolocation.getCurrentPosition ||
231
- navigator.geolocation.default.getCurrentPosition;
232
-
233
- getCurrentPosition &&
234
- getCurrentPosition(
235
- (position) => {
236
- if (props.nearbyPlacesAPI === 'None') {
237
- let currentLocation = {
238
- description: props.currentLocationLabel,
239
- geometry: {
240
- location: {
241
- lat: position.coords.latitude,
242
- lng: position.coords.longitude,
243
- },
244
- },
245
- };
246
-
247
- _disableRowLoaders();
248
- props.onPress(currentLocation, currentLocation);
249
- } else {
250
- _requestNearby(position.coords.latitude, position.coords.longitude);
251
- }
252
- },
253
- (error) => {
254
- _disableRowLoaders();
255
- console.error(error.message);
256
- },
257
- options,
258
- );
259
- };
260
-
261
- const _onPress = (rowData) => {
262
- if (rowData.isPredefinedPlace !== true && props.fetchDetails === true) {
263
- if (rowData.isLoading === true) {
264
- // already requesting
265
- return;
266
- }
267
-
268
- Keyboard.dismiss();
269
-
270
- _abortRequests();
271
-
272
- // display loader
273
- _enableRowLoader(rowData);
274
-
275
- // fetch details
276
- const request = new XMLHttpRequest();
277
- _requests.push(request);
278
- request.timeout = props.timeout;
279
- request.ontimeout = props.onTimeout;
280
- request.onreadystatechange = () => {
281
- if (request.readyState !== 4) return;
282
-
283
- if (request.status === 200) {
284
- const responseJSON = JSON.parse(request.responseText);
285
- if (
286
- responseJSON.status === 'OK' ||
287
- (props.isNewPlacesAPI && responseJSON.id)
288
- ) {
289
- // if (_isMounted === true) {
290
- const details = props.isNewPlacesAPI
291
- ? responseJSON
292
- : responseJSON.result;
293
- _disableRowLoaders();
294
- _onBlur();
295
-
296
- setStateText(_renderDescription(rowData));
297
-
298
- delete rowData.isLoading;
299
- props.onPress(rowData, details);
300
- // }
301
- } else {
302
- _disableRowLoaders();
303
-
304
- if (props.autoFillOnNotFound) {
305
- setStateText(_renderDescription(rowData));
306
- delete rowData.isLoading;
307
- }
308
-
309
- if (!props.onNotFound) {
310
- console.warn(
311
- 'google places autocomplete: ' + responseJSON.status,
312
- );
313
- } else {
314
- props.onNotFound(responseJSON);
315
- }
316
- }
317
- } else {
318
- _disableRowLoaders();
319
-
320
- if (!props.onFail) {
321
- console.warn(
322
- 'google places autocomplete: request could not be completed or has been aborted',
323
- );
324
- } else {
325
- props.onFail('request could not be completed or has been aborted');
326
- }
327
- }
328
- };
329
-
330
- if (props.isNewPlacesAPI) {
331
- request.open(
332
- 'GET',
333
- `${url}/v1/places/${rowData.place_id}?` +
334
- Qs.stringify({
335
- key: props.query.key,
336
- sessionToken,
337
- fields: props.fields,
338
- }),
339
- );
340
- setSessionToken(uuidv4());
341
- } else {
342
- request.open(
343
- 'GET',
344
- `${url}/place/details/json?` +
345
- Qs.stringify({
346
- key: props.query.key,
347
- placeid: rowData.place_id,
348
- language: props.query.language,
349
- ...props.GooglePlacesDetailsQuery,
350
- }),
351
- );
352
- }
353
-
354
- request.withCredentials = requestShouldUseWithCredentials();
355
- setRequestHeaders(request, getRequestHeaders(props.requestUrl));
356
-
357
- request.send();
358
- } else if (rowData.isCurrentLocation === true) {
359
- // display loader
360
- _enableRowLoader(rowData);
361
-
362
- setStateText(_renderDescription(rowData));
363
-
364
- delete rowData.isLoading;
365
- getCurrentLocation();
366
- } else {
367
- setStateText(_renderDescription(rowData));
368
-
369
- _onBlur();
370
- delete rowData.isLoading;
371
- let predefinedPlace = _getPredefinedPlace(rowData);
240
+ requestsRef.current = [];
241
+ }, []);
372
242
 
373
- // sending predefinedPlace as details for predefined places
374
- props.onPress(predefinedPlace, predefinedPlace);
375
- }
376
- };
243
+ // ==========================================================================
244
+ // DATA PROCESSING FUNCTIONS
245
+ // ==========================================================================
377
246
 
378
- const _enableRowLoader = (rowData) => {
379
- let rows = buildRowsFromResults(_results);
380
- for (let i = 0; i < rows.length; i++) {
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;
381
255
  if (
382
- rows[i].place_id === rowData.place_id ||
383
- (rows[i].isCurrentLocation === true &&
384
- rowData.isCurrentLocation === true)
256
+ shouldDisplayPredefinedPlaces ||
257
+ predefinedPlacesAlwaysVisible === true
385
258
  ) {
386
- rows[i].isLoading = true;
387
- setDataSource(rows);
388
- break;
389
- }
390
- }
391
- };
259
+ if (predefinedPlaces.length > 0) {
260
+ res = [
261
+ ...predefinedPlaces.filter((place) => place?.description?.length),
262
+ ];
263
+ }
392
264
 
393
- const _disableRowLoaders = () => {
394
- // if (_isMounted === true) {
395
- for (let i = 0; i < _results.length; i++) {
396
- if (_results[i].isLoading === true) {
397
- _results[i].isLoading = false;
265
+ if (currentLocation === true && hasNavigator()) {
266
+ res.unshift({
267
+ description: currentLocationLabel,
268
+ isCurrentLocation: true,
269
+ });
270
+ }
398
271
  }
399
- }
400
-
401
- setDataSource(buildRowsFromResults(_results));
402
- // }
403
- };
404
272
 
405
- const _getPredefinedPlace = (rowData) => {
406
- if (rowData.isPredefinedPlace !== true) {
407
- return rowData;
408
- }
409
-
410
- for (let i = 0; i < props.predefinedPlaces.length; i++) {
411
- if (props.predefinedPlaces[i].description === rowData.description) {
412
- return props.predefinedPlaces[i];
413
- }
414
- }
273
+ res = res.map((place) => ({
274
+ ...place,
275
+ isPredefinedPlace: true,
276
+ }));
415
277
 
416
- return rowData;
417
- };
278
+ return [...res, ...results];
279
+ },
280
+ [
281
+ predefinedPlacesAlwaysVisible,
282
+ predefinedPlaces,
283
+ currentLocation,
284
+ currentLocationLabel,
285
+ hasNavigator,
286
+ ],
287
+ );
418
288
 
419
- const _filterResultsByTypes = (unfilteredResults, types) => {
420
- if (types.length === 0) return unfilteredResults;
289
+ const _filterResultsByTypes = useCallback((unfilteredResults, types) => {
290
+ if (!types || types.length === 0) return unfilteredResults;
421
291
 
422
292
  const results = [];
423
293
  for (let i = 0; i < unfilteredResults.length; i++) {
424
294
  let found = false;
425
295
 
426
296
  for (let j = 0; j < types.length; j++) {
427
- if (unfilteredResults[i].types.indexOf(types[j]) !== -1) {
297
+ if (unfilteredResults[i].types?.indexOf(types[j]) !== -1) {
428
298
  found = true;
429
299
  break;
430
300
  }
@@ -435,7 +305,7 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
435
305
  }
436
306
  }
437
307
  return results;
438
- };
308
+ }, []);
439
309
 
440
310
  const _filterResultsByPlacePredictions = (unfilteredResults) => {
441
311
  const results = [];
@@ -460,19 +330,150 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
460
330
  return results;
461
331
  };
462
332
 
463
- const _requestNearby = (latitude, longitude) => {
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) => {
464
465
  _abortRequests();
465
466
 
466
- if (
467
- latitude !== undefined &&
468
- longitude !== undefined &&
469
- latitude !== null &&
470
- longitude !== null
471
- ) {
467
+ if (!url) {
468
+ return;
469
+ }
470
+
471
+ if (supportedPlatform() && text && text.length >= minLength) {
472
472
  const request = new XMLHttpRequest();
473
- _requests.push(request);
474
- request.timeout = props.timeout;
475
- request.ontimeout = props.onTimeout;
473
+ requestsRef.current.push(request);
474
+
475
+ request.timeout = timeout;
476
+ request.ontimeout = onTimeout;
476
477
  request.onreadystatechange = () => {
477
478
  if (request.readyState !== 4) {
478
479
  setListLoaderDisplayed(true);
@@ -480,177 +481,294 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
480
481
  }
481
482
 
482
483
  setListLoaderDisplayed(false);
484
+
483
485
  if (request.status === 200) {
484
486
  const responseJSON = JSON.parse(request.responseText);
485
487
 
486
- _disableRowLoaders();
488
+ if (typeof responseJSON.predictions !== 'undefined') {
489
+ const results =
490
+ nearbyPlacesAPI === 'GoogleReverseGeocoding'
491
+ ? _filterResultsByTypes(
492
+ responseJSON.predictions,
493
+ filterReverseGeocodingByTypes,
494
+ )
495
+ : responseJSON.predictions;
487
496
 
488
- if (typeof responseJSON.results !== 'undefined') {
489
- // if (_isMounted === true) {
490
- var results = [];
491
- if (props.nearbyPlacesAPI === 'GoogleReverseGeocoding') {
492
- results = _filterResultsByTypes(
493
- responseJSON.results,
494
- props.filterReverseGeocodingByTypes,
495
- );
496
- } else {
497
- results = responseJSON.results;
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);
498
503
  }
504
+ }
505
+ if (typeof responseJSON.suggestions !== 'undefined') {
506
+ const results = _filterResultsByPlacePredictions(
507
+ responseJSON.suggestions,
508
+ );
499
509
 
500
- setDataSource(buildRowsFromResults(results));
501
- // }
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
+ }
502
517
  }
503
518
  if (typeof responseJSON.error_message !== 'undefined') {
504
- if (!props.onFail)
519
+ if (!onFail) {
505
520
  console.warn(
506
521
  'google places autocomplete: ' + responseJSON.error_message,
507
522
  );
508
- else {
509
- props.onFail(responseJSON.error_message);
523
+ } else {
524
+ onFail(responseJSON.error_message);
510
525
  }
511
526
  }
512
527
  } else {
513
- // console.warn("google places autocomplete: request could not be completed or has been aborted");
528
+ console.warn(
529
+ 'google places autocomplete: request could not be completed or has been aborted',
530
+ );
514
531
  }
515
532
  };
516
533
 
517
- let requestUrl = '';
518
- if (props.nearbyPlacesAPI === 'GoogleReverseGeocoding') {
519
- // your key must be allowed to use Google Maps Geocoding API
520
- requestUrl =
521
- `${url}/geocode/json?` +
522
- Qs.stringify({
523
- latlng: latitude + ',' + longitude,
524
- key: props.query.key,
525
- ...props.GoogleReverseGeocodingQuery,
526
- });
527
- } else {
528
- requestUrl =
529
- `${url}/place/nearbysearch/json?` +
530
- Qs.stringify({
531
- location: latitude + ',' + longitude,
532
- key: props.query.key,
533
- ...props.GooglePlacesSearchQuery,
534
- });
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;
535
664
  }
536
665
 
537
- request.open('GET', requestUrl);
666
+ Keyboard.dismiss();
538
667
 
539
- request.withCredentials = requestShouldUseWithCredentials();
540
- setRequestHeaders(request, getRequestHeaders(props.requestUrl));
668
+ _abortRequests();
541
669
 
542
- request.send();
543
- } else {
544
- _results = [];
545
- setDataSource(buildRowsFromResults([]));
546
- }
547
- };
670
+ // display loader
671
+ _enableRowLoader(rowData);
548
672
 
549
- const _request = (text) => {
550
- _abortRequests();
551
- if (!url) {
552
- return;
553
- }
554
- if (supportedPlatform() && text && text.length >= props.minLength) {
673
+ // fetch details
555
674
  const request = new XMLHttpRequest();
556
- _requests.push(request);
557
- request.timeout = props.timeout;
558
- request.ontimeout = props.onTimeout;
675
+ requestsRef.current.push(request);
676
+ request.timeout = timeout;
677
+ request.ontimeout = onTimeout;
559
678
  request.onreadystatechange = () => {
560
- if (request.readyState !== 4) {
561
- setListLoaderDisplayed(true);
562
- return;
563
- }
679
+ if (request.readyState !== 4) return;
564
680
 
565
- setListLoaderDisplayed(false);
566
681
  if (request.status === 200) {
567
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();
568
690
 
569
- if (typeof responseJSON.predictions !== 'undefined') {
570
- // if (_isMounted === true) {
571
- const results =
572
- props.nearbyPlacesAPI === 'GoogleReverseGeocoding'
573
- ? _filterResultsByTypes(
574
- responseJSON.predictions,
575
- props.filterReverseGeocodingByTypes,
576
- )
577
- : responseJSON.predictions;
691
+ setStateText(_renderDescription(rowData));
578
692
 
579
- _results = results;
580
- setDataSource(buildRowsFromResults(results, text));
581
- // }
582
- }
583
- if (typeof responseJSON.suggestions !== 'undefined') {
584
- const results = _filterResultsByPlacePredictions(
585
- responseJSON.suggestions,
586
- );
693
+ delete rowData.isLoading;
694
+ onPress(rowData, details);
695
+ } else {
696
+ _disableRowLoaders();
587
697
 
588
- _results = results;
589
- setDataSource(buildRowsFromResults(results, text));
590
- }
591
- if (typeof responseJSON.error_message !== 'undefined') {
592
- if (!props.onFail)
698
+ if (autoFillOnNotFound) {
699
+ setStateText(_renderDescription(rowData));
700
+ delete rowData.isLoading;
701
+ }
702
+
703
+ if (!onNotFound) {
593
704
  console.warn(
594
- 'google places autocomplete: ' + responseJSON.error_message,
705
+ 'google places autocomplete: ' + responseJSON.status,
595
706
  );
596
- else {
597
- props.onFail(responseJSON.error_message);
707
+ } else {
708
+ onNotFound(responseJSON);
598
709
  }
599
710
  }
600
711
  } else {
601
- // console.warn("google places autocomplete: request could not be completed or has been aborted");
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
+ }
602
721
  }
603
722
  };
604
723
 
605
- if (props.preProcess) {
606
- setStateText(props.preProcess(text));
607
- }
608
-
609
- if (props.isNewPlacesAPI) {
610
- const keyQueryParam = props.query.key
611
- ? '?' +
724
+ if (isNewPlacesAPI) {
725
+ request.open(
726
+ 'GET',
727
+ `${url}/v1/places/${rowData.place_id}?` +
612
728
  Qs.stringify({
613
- key: props.query.key,
614
- })
615
- : '';
616
- request.open('POST', `${url}/v1/places:autocomplete${keyQueryParam}`);
729
+ key: query.key,
730
+ sessionToken,
731
+ fields,
732
+ }),
733
+ );
734
+ setSessionToken(randomUUID());
617
735
  } else {
618
736
  request.open(
619
737
  'GET',
620
- `${url}/place/autocomplete/json?input=` +
621
- encodeURIComponent(text) +
622
- '&' +
623
- Qs.stringify(props.query),
738
+ `${url}/place/details/json?` +
739
+ Qs.stringify({
740
+ key: query.key,
741
+ placeid: rowData.place_id,
742
+ language: query.language,
743
+ ...GooglePlacesDetailsQuery,
744
+ }),
624
745
  );
625
746
  }
626
747
 
627
748
  request.withCredentials = requestShouldUseWithCredentials();
628
749
  setRequestHeaders(request, getRequestHeaders(props.requestUrl));
629
750
 
630
- if (props.isNewPlacesAPI) {
631
- const { key, locationbias, types, ...rest } = props.query;
632
- request.send(
633
- JSON.stringify({
634
- input: text,
635
- sessionToken,
636
- ...rest,
637
- }),
638
- );
639
- } else {
640
- request.send();
641
- }
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();
642
760
  } else {
643
- _results = [];
644
- setDataSource(buildRowsFromResults([]));
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);
645
769
  }
646
770
  };
647
771
 
648
- // eslint-disable-next-line react-hooks/exhaustive-deps
649
- const debounceData = useMemo(() => debounce(_request, props.debounce), [
650
- props.query,
651
- url,
652
- ]);
653
-
654
772
  const _onChangeText = (text) => {
655
773
  setStateText(text);
656
774
  debounceData(text);
@@ -659,38 +777,46 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
659
777
  const _handleChangeText = (text) => {
660
778
  _onChangeText(text);
661
779
 
662
- const onChangeText = props?.textInputProps?.onChangeText;
780
+ const onChangeText = textInputProps?.onChangeText;
663
781
 
664
782
  if (onChangeText) {
665
783
  onChangeText(text);
666
784
  }
667
785
  };
668
786
 
669
- const _getRowLoader = () => {
670
- return <ActivityIndicator animating={true} size='small' />;
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;
671
801
  };
672
802
 
673
- const _renderRowData = (rowData, index) => {
674
- if (props.renderRow) {
675
- return props.renderRow(rowData, index);
803
+ const _onBlur = (e) => {
804
+ if (e && isNewFocusInAutocompleteResultList(e)) return;
805
+
806
+ if (!keepResultsAfterBlur) {
807
+ setListViewDisplayed(false);
676
808
  }
809
+ inputRef?.current?.blur();
810
+ };
677
811
 
678
- return (
679
- <Text
680
- style={[
681
- props.suppressDefaultStyles ? {} : defaultStyles.description,
682
- props.styles.description,
683
- rowData.isPredefinedPlace
684
- ? props.styles.predefinedPlacesDescription
685
- : {},
686
- ]}
687
- numberOfLines={props.numberOfLines}
688
- >
689
- {_renderDescription(rowData)}
690
- </Text>
691
- );
812
+ const _onFocus = () => {
813
+ setListViewDisplayed(true);
692
814
  };
693
815
 
816
+ // ==========================================================================
817
+ // RENDER FUNCTIONS
818
+ // ==========================================================================
819
+
694
820
  const _renderDescription = (rowData) => {
695
821
  if (props.renderDescription) {
696
822
  return props.renderDescription(rowData);
@@ -699,13 +825,17 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
699
825
  return rowData.description || rowData.formatted_address || rowData.name;
700
826
  };
701
827
 
828
+ const _getRowLoader = () => {
829
+ return <ActivityIndicator animating={true} size='small' />;
830
+ };
831
+
702
832
  const _renderLoader = (rowData) => {
703
833
  if (rowData.isLoading === true) {
704
834
  return (
705
835
  <View
706
836
  style={[
707
- props.suppressDefaultStyles ? {} : defaultStyles.loader,
708
- props.styles.loader,
837
+ suppressDefaultStyles ? {} : defaultStyles.loader,
838
+ styles?.loader,
709
839
  ]}
710
840
  >
711
841
  {_getRowLoader()}
@@ -716,26 +846,45 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
716
846
  return null;
717
847
  };
718
848
 
849
+ const _renderRowData = (rowData, index) => {
850
+ if (props.renderRow) {
851
+ return props.renderRow(rowData, index);
852
+ }
853
+
854
+ return (
855
+ <Text
856
+ style={[
857
+ suppressDefaultStyles ? {} : defaultStyles.description,
858
+ styles?.description,
859
+ rowData.isPredefinedPlace ? styles?.predefinedPlacesDescription : {},
860
+ ]}
861
+ numberOfLines={numberOfLines}
862
+ >
863
+ {_renderDescription(rowData)}
864
+ </Text>
865
+ );
866
+ };
867
+
719
868
  const _renderRow = (rowData = {}, index) => {
720
869
  return (
721
870
  <ScrollView
722
871
  contentContainerStyle={
723
- props.isRowScrollable ? { minWidth: '100%' } : { width: '100%' }
872
+ isRowScrollable ? { minWidth: '100%' } : { width: '100%' }
724
873
  }
725
- scrollEnabled={props.isRowScrollable}
726
- keyboardShouldPersistTaps={props.keyboardShouldPersistTaps}
874
+ scrollEnabled={isRowScrollable}
875
+ keyboardShouldPersistTaps={keyboardShouldPersistTaps}
727
876
  horizontal={true}
728
877
  showsHorizontalScrollIndicator={false}
729
878
  showsVerticalScrollIndicator={false}
730
879
  >
731
880
  <Pressable
732
881
  style={({ hovered, pressed }) => [
733
- props.isRowScrollable ? { minWidth: '100%' } : { width: '100%' },
882
+ isRowScrollable ? { minWidth: '100%' } : { width: '100%' },
734
883
  {
735
884
  backgroundColor: pressed
736
- ? props.listUnderlayColor
885
+ ? listUnderlayColor
737
886
  : hovered
738
- ? props.listHoverColor
887
+ ? listHoverColor
739
888
  : undefined,
740
889
  },
741
890
  ]}
@@ -744,9 +893,9 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
744
893
  >
745
894
  <View
746
895
  style={[
747
- props.suppressDefaultStyles ? {} : defaultStyles.row,
748
- props.styles.row,
749
- rowData.isPredefinedPlace ? props.styles.specialItemRow : {},
896
+ suppressDefaultStyles ? {} : defaultStyles.row,
897
+ styles?.row,
898
+ rowData.isPredefinedPlace ? styles?.specialItemRow : {},
750
899
  ]}
751
900
  >
752
901
  {_renderLoader(rowData)}
@@ -766,40 +915,29 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
766
915
  <View
767
916
  key={`${sectionID}-${rowID}`}
768
917
  style={[
769
- props.suppressDefaultStyles ? {} : defaultStyles.separator,
770
- props.styles.separator,
918
+ suppressDefaultStyles ? {} : defaultStyles.separator,
919
+ styles?.separator,
771
920
  ]}
772
921
  />
773
922
  );
774
923
  };
775
924
 
776
- const isNewFocusInAutocompleteResultList = ({
777
- relatedTarget,
778
- currentTarget,
779
- }) => {
780
- if (!relatedTarget) return false;
925
+ const _shouldShowPoweredLogo = () => {
926
+ if (!enablePoweredByContainer || dataSource.length === 0) {
927
+ return false;
928
+ }
781
929
 
782
- var node = relatedTarget.parentNode;
930
+ for (let i = 0; i < dataSource.length; i++) {
931
+ const row = dataSource[i];
783
932
 
784
- while (node) {
785
- if (node.id === 'result-list-id') return true;
786
- node = node.parentNode;
933
+ if (!('isCurrentLocation' in row) && !('isPredefinedPlace' in row)) {
934
+ return true;
935
+ }
787
936
  }
788
937
 
789
938
  return false;
790
939
  };
791
940
 
792
- const _onBlur = (e) => {
793
- if (e && isNewFocusInAutocompleteResultList(e)) return;
794
-
795
- if (!props.keepResultsAfterBlur) {
796
- setListViewDisplayed(false);
797
- }
798
- inputRef?.current?.blur();
799
- };
800
-
801
- const _onFocus = () => setListViewDisplayed(true);
802
-
803
941
  const _renderPoweredLogo = () => {
804
942
  if (!_shouldShowPoweredLogo()) {
805
943
  return null;
@@ -808,15 +946,15 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
808
946
  return (
809
947
  <View
810
948
  style={[
811
- props.suppressDefaultStyles ? {} : defaultStyles.row,
949
+ suppressDefaultStyles ? {} : defaultStyles.row,
812
950
  defaultStyles.poweredContainer,
813
- props.styles.poweredContainer,
951
+ styles?.poweredContainer,
814
952
  ]}
815
953
  >
816
954
  <Image
817
955
  style={[
818
- props.suppressDefaultStyles ? {} : defaultStyles.powered,
819
- props.styles.powered,
956
+ suppressDefaultStyles ? {} : defaultStyles.powered,
957
+ styles?.powered,
820
958
  ]}
821
959
  resizeMode='contain'
822
960
  source={require('./images/powered_by_google_on_white.png')}
@@ -825,71 +963,73 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
825
963
  );
826
964
  };
827
965
 
828
- const _shouldShowPoweredLogo = () => {
829
- if (!props.enablePoweredByContainer || dataSource.length === 0) {
830
- return false;
831
- }
832
-
833
- for (let i = 0; i < dataSource.length; i++) {
834
- let row = dataSource[i];
835
-
836
- if (
837
- !row.hasOwnProperty('isCurrentLocation') &&
838
- !row.hasOwnProperty('isPredefinedPlace')
839
- ) {
840
- return true;
841
- }
842
- }
843
-
844
- return false;
845
- };
846
-
847
966
  const _renderLeftButton = () => {
848
967
  if (props.renderLeftButton) {
849
968
  return props.renderLeftButton();
850
969
  }
970
+ return null;
851
971
  };
852
972
 
853
973
  const _renderRightButton = () => {
854
974
  if (props.renderRightButton) {
855
975
  return props.renderRightButton();
856
976
  }
977
+ return null;
857
978
  };
858
979
 
859
980
  const _getFlatList = () => {
860
- const keyGenerator = () => Math.random().toString(36).substr(2, 10);
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
+ };
861
995
 
862
- if (
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 =
863
1003
  supportedPlatform() &&
864
- (stateText !== '' ||
865
- props.predefinedPlaces.length > 0 ||
866
- props.currentLocation === true) &&
867
- listViewDisplayed === true
868
- ) {
1004
+ dataSource.length > 0 &&
1005
+ (listViewDisplayed === true || isAutoMode);
1006
+
1007
+ if (shouldShowList) {
869
1008
  return (
870
1009
  <FlatList
871
1010
  nativeID='result-list-id'
872
- scrollEnabled={!props.disableScroll}
1011
+ scrollEnabled={!disableScroll}
1012
+ nestedScrollEnabled={true}
873
1013
  style={[
874
- props.suppressDefaultStyles ? {} : defaultStyles.listView,
875
- props.styles.listView,
1014
+ suppressDefaultStyles ? {} : defaultStyles.listView,
1015
+ styles?.listView,
876
1016
  ]}
877
1017
  data={dataSource}
878
- keyExtractor={keyGenerator}
1018
+ keyExtractor={keyExtractor}
879
1019
  extraData={[dataSource, props]}
880
1020
  ItemSeparatorComponent={_renderSeparator}
881
1021
  renderItem={({ item, index }) => _renderRow(item, index)}
882
1022
  ListEmptyComponent={
883
1023
  listLoaderDisplayed
884
1024
  ? props.listLoaderComponent
885
- : stateText.length > props.minLength && props.listEmptyComponent
1025
+ : stateText.length > minLength && props.listEmptyComponent
886
1026
  }
887
1027
  ListHeaderComponent={
888
1028
  props.renderHeaderComponent &&
889
1029
  props.renderHeaderComponent(stateText)
890
1030
  }
891
1031
  ListFooterComponent={_renderPoweredLogo}
892
- {...props}
1032
+ {...restProps}
893
1033
  />
894
1034
  );
895
1035
  }
@@ -897,52 +1037,141 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
897
1037
  return null;
898
1038
  };
899
1039
 
900
- let {
901
- onFocus,
902
- onBlur,
903
- onChangeText, // destructuring here stops this being set after onChangeText={_handleChangeText}
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}
904
1132
  clearButtonMode,
905
1133
  InputComp,
906
1134
  ...userProps
907
- } = props.textInputProps;
1135
+ } = textInputProps || {};
908
1136
  const TextInputComp = InputComp || TextInput;
1137
+
909
1138
  return (
910
1139
  <View
911
1140
  style={[
912
- props.suppressDefaultStyles ? {} : defaultStyles.container,
913
- props.styles.container,
1141
+ suppressDefaultStyles ? {} : defaultStyles.container,
1142
+ styles?.container,
914
1143
  ]}
915
1144
  pointerEvents='box-none'
916
1145
  >
917
- {!props.textInputHide && (
1146
+ {!textInputHide && (
918
1147
  <View
919
1148
  style={[
920
- props.suppressDefaultStyles ? {} : defaultStyles.textInputContainer,
921
- props.styles.textInputContainer,
1149
+ suppressDefaultStyles ? {} : defaultStyles.textInputContainer,
1150
+ styles?.textInputContainer,
922
1151
  ]}
923
1152
  >
924
1153
  {_renderLeftButton()}
925
1154
  <TextInputComp
926
1155
  ref={inputRef}
927
1156
  style={[
928
- props.suppressDefaultStyles ? {} : defaultStyles.textInput,
929
- props.styles.textInput,
1157
+ suppressDefaultStyles ? {} : defaultStyles.textInput,
1158
+ styles?.textInput,
930
1159
  ]}
931
1160
  value={stateText}
932
- placeholder={props.placeholder}
1161
+ placeholder={placeholder}
933
1162
  onFocus={
934
- onFocus
1163
+ textInputOnFocus
935
1164
  ? (e) => {
936
1165
  _onFocus();
937
- onFocus(e);
1166
+ textInputOnFocus(e);
938
1167
  }
939
1168
  : _onFocus
940
1169
  }
941
1170
  onBlur={
942
- onBlur
1171
+ textInputOnBlur
943
1172
  ? (e) => {
944
1173
  _onBlur(e);
945
- onBlur(e);
1174
+ textInputOnBlur(e);
946
1175
  }
947
1176
  : _onBlur
948
1177
  }
@@ -960,109 +1189,6 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
960
1189
  );
961
1190
  });
962
1191
 
963
- GooglePlacesAutocomplete.propTypes = {
964
- autoFillOnNotFound: PropTypes.bool,
965
- currentLocation: PropTypes.bool,
966
- currentLocationLabel: PropTypes.string,
967
- debounce: PropTypes.number,
968
- disableScroll: PropTypes.bool,
969
- enableHighAccuracyLocation: PropTypes.bool,
970
- enablePoweredByContainer: PropTypes.bool,
971
- fetchDetails: PropTypes.bool,
972
- filterReverseGeocodingByTypes: PropTypes.array,
973
- GooglePlacesDetailsQuery: PropTypes.object,
974
- GooglePlacesSearchQuery: PropTypes.object,
975
- GoogleReverseGeocodingQuery: PropTypes.object,
976
- inbetweenCompo: PropTypes.object,
977
- isRowScrollable: PropTypes.bool,
978
- keyboardShouldPersistTaps: PropTypes.oneOf(['never', 'always', 'handled']),
979
- listEmptyComponent: PropTypes.element,
980
- listLoaderComponent: PropTypes.element,
981
- listHoverColor: PropTypes.string,
982
- listUnderlayColor: PropTypes.string,
983
- // Must write it this way: https://stackoverflow.com/a/54290946/7180620
984
- listViewDisplayed: PropTypes.oneOfType([
985
- PropTypes.bool,
986
- PropTypes.oneOf(['auto']),
987
- ]),
988
- keepResultsAfterBlur: PropTypes.bool,
989
- minLength: PropTypes.number,
990
- nearbyPlacesAPI: PropTypes.string,
991
- numberOfLines: PropTypes.number,
992
- onFail: PropTypes.func,
993
- onNotFound: PropTypes.func,
994
- onPress: PropTypes.func,
995
- onTimeout: PropTypes.func,
996
- placeholder: PropTypes.string,
997
- predefinedPlaces: PropTypes.array,
998
- predefinedPlacesAlwaysVisible: PropTypes.bool,
999
- preProcess: PropTypes.func,
1000
- query: PropTypes.object,
1001
- renderDescription: PropTypes.func,
1002
- renderHeaderComponent: PropTypes.func,
1003
- renderLeftButton: PropTypes.func,
1004
- renderRightButton: PropTypes.func,
1005
- renderRow: PropTypes.func,
1006
- requestUrl: PropTypes.shape({
1007
- url: PropTypes.string,
1008
- useOnPlatform: PropTypes.oneOf(['web', 'all']),
1009
- headers: PropTypes.objectOf(PropTypes.string),
1010
- }),
1011
- styles: PropTypes.object,
1012
- suppressDefaultStyles: PropTypes.bool,
1013
- textInputHide: PropTypes.bool,
1014
- textInputProps: PropTypes.object,
1015
- timeout: PropTypes.number,
1016
- isNewPlacesAPI: PropTypes.bool,
1017
- fields: PropTypes.string,
1018
- };
1019
-
1020
- GooglePlacesAutocomplete.defaultProps = {
1021
- autoFillOnNotFound: false,
1022
- currentLocation: false,
1023
- currentLocationLabel: 'Current location',
1024
- debounce: 0,
1025
- disableScroll: false,
1026
- enableHighAccuracyLocation: true,
1027
- enablePoweredByContainer: true,
1028
- fetchDetails: false,
1029
- filterReverseGeocodingByTypes: [],
1030
- GooglePlacesDetailsQuery: {},
1031
- GooglePlacesSearchQuery: {
1032
- rankby: 'distance',
1033
- type: 'restaurant',
1034
- },
1035
- GoogleReverseGeocodingQuery: {},
1036
- isRowScrollable: true,
1037
- keyboardShouldPersistTaps: 'always',
1038
- listHoverColor: '#ececec',
1039
- listUnderlayColor: '#c8c7cc',
1040
- listViewDisplayed: 'auto',
1041
- keepResultsAfterBlur: false,
1042
- minLength: 0,
1043
- nearbyPlacesAPI: 'GooglePlacesSearch',
1044
- numberOfLines: 1,
1045
- onFail: () => {},
1046
- onNotFound: () => {},
1047
- onPress: () => {},
1048
- onTimeout: () => console.warn('google places autocomplete: request timeout'),
1049
- placeholder: '',
1050
- predefinedPlaces: [],
1051
- predefinedPlacesAlwaysVisible: false,
1052
- query: {
1053
- key: 'missing api key',
1054
- language: 'en',
1055
- types: 'geocode',
1056
- },
1057
- styles: {},
1058
- suppressDefaultStyles: false,
1059
- textInputHide: false,
1060
- textInputProps: {},
1061
- timeout: 20000,
1062
- isNewPlacesAPI: false,
1063
- fields: '*',
1064
- };
1065
-
1066
1192
  GooglePlacesAutocomplete.displayName = 'GooglePlacesAutocomplete';
1067
1193
 
1068
1194
  export default { GooglePlacesAutocomplete };