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