diff --git a/src/components/AddressSearch/CurrentLocationButton.js b/src/components/AddressSearch/CurrentLocationButton.js
new file mode 100644
index 000000000000..893ec031ab7f
--- /dev/null
+++ b/src/components/AddressSearch/CurrentLocationButton.js
@@ -0,0 +1,52 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import {Text} from 'react-native';
+import colors from '../../styles/colors';
+import styles from '../../styles/styles';
+import Icon from '../Icon';
+import * as Expensicons from '../Icon/Expensicons';
+import PressableWithFeedback from '../Pressable/PressableWithFeedback';
+import getButtonState from '../../libs/getButtonState';
+import * as StyleUtils from '../../styles/StyleUtils';
+import useLocalize from '../../hooks/useLocalize';
+
+const propTypes = {
+ /** Callback that runs when location button is clicked */
+ onPress: PropTypes.func,
+
+ /** Boolean to indicate if the button is clickable */
+ isDisabled: PropTypes.bool,
+};
+
+const defaultProps = {
+ isDisabled: false,
+ onPress: () => {},
+};
+
+function CurrentLocationButton({onPress, isDisabled}) {
+ const {translate} = useLocalize();
+
+ return (
+ e.preventDefault()}
+ onTouchStart={(e) => e.preventDefault()}
+ >
+
+ {translate('location.useCurrent')}
+
+ );
+}
+
+CurrentLocationButton.displayName = 'CurrentLocationButton';
+CurrentLocationButton.propTypes = propTypes;
+CurrentLocationButton.defaultProps = defaultProps;
+
+export default CurrentLocationButton;
diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js
index fe220d442674..3e676b811c16 100644
--- a/src/components/AddressSearch/index.js
+++ b/src/components/AddressSearch/index.js
@@ -1,7 +1,7 @@
import _ from 'underscore';
-import React, {useMemo, useRef, useState} from 'react';
+import React, {useEffect, useMemo, useRef, useState} from 'react';
import PropTypes from 'prop-types';
-import {LogBox, ScrollView, View, Text, ActivityIndicator} from 'react-native';
+import {Keyboard, LogBox, ScrollView, View, Text, ActivityIndicator} from 'react-native';
import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete';
import lodashGet from 'lodash/get';
import compose from '../../libs/compose';
@@ -11,12 +11,16 @@ import themeColors from '../../styles/themes/default';
import TextInput from '../TextInput';
import * as ApiUtils from '../../libs/ApiUtils';
import * as GooglePlacesUtils from '../../libs/GooglePlacesUtils';
+import getCurrentPosition from '../../libs/getCurrentPosition';
import CONST from '../../CONST';
import * as StyleUtils from '../../styles/StyleUtils';
-import resetDisplayListViewBorderOnBlur from './resetDisplayListViewBorderOnBlur';
+import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer';
import variables from '../../styles/variables';
+import FullScreenLoadingIndicator from '../FullscreenLoadingIndicator';
+import LocationErrorMessage from '../LocationErrorMessage';
import {withNetwork} from '../OnyxProvider';
import networkPropTypes from '../networkPropTypes';
+import CurrentLocationButton from './CurrentLocationButton';
// The error that's being thrown below will be ignored until we fork the
// react-native-google-places-autocomplete repo and replace the
@@ -61,6 +65,9 @@ const propTypes = {
/** Should address search be limited to results in the USA */
isLimitedToUSA: PropTypes.bool,
+ /** Shows a current location button in suggestion list */
+ canUseCurrentLocation: PropTypes.bool,
+
/** A list of predefined places that can be shown when the user isn't searching for something */
predefinedPlaces: PropTypes.arrayOf(
PropTypes.shape({
@@ -115,6 +122,7 @@ const defaultProps = {
defaultValue: undefined,
containerStyles: [],
isLimitedToUSA: false,
+ canUseCurrentLocation: false,
renamedInputKeys: {
street: 'addressStreet',
street2: 'addressStreet2',
@@ -135,6 +143,11 @@ const defaultProps = {
function AddressSearch(props) {
const [displayListViewBorder, setDisplayListViewBorder] = useState(false);
const [isTyping, setIsTyping] = useState(false);
+ const [isFocused, setIsFocused] = useState(false);
+ const [searchValue, setSearchValue] = useState(props.value || props.defaultValue || '');
+ const [locationErrorCode, setLocationErrorCode] = useState(null);
+ const [isFetchingCurrentLocation, setIsFetchingCurrentLocation] = useState(false);
+ const shouldTriggerGeolocationCallbacks = useRef(true);
const containerRef = useRef();
const query = useMemo(
() => ({
@@ -144,6 +157,7 @@ function AddressSearch(props) {
}),
[props.preferredLocale, props.resultTypes, props.isLimitedToUSA],
);
+ const shouldShowCurrentLocationButton = props.canUseCurrentLocation && searchValue.trim().length === 0 && isFocused;
const saveLocationDetails = (autocompleteData, details) => {
const addressComponents = details.address_components;
@@ -262,6 +276,72 @@ function AddressSearch(props) {
props.onPress(values);
};
+ /** Gets the user's current location and registers success/error callbacks */
+ const getCurrentLocation = () => {
+ if (isFetchingCurrentLocation) {
+ return;
+ }
+
+ setIsTyping(false);
+ setIsFocused(false);
+ setDisplayListViewBorder(false);
+ setIsFetchingCurrentLocation(true);
+
+ Keyboard.dismiss();
+
+ getCurrentPosition(
+ (successData) => {
+ if (!shouldTriggerGeolocationCallbacks.current) {
+ return;
+ }
+
+ setIsFetchingCurrentLocation(false);
+ setLocationErrorCode(null);
+
+ const location = {
+ lat: successData.coords.latitude,
+ lng: successData.coords.longitude,
+ address: CONST.YOUR_LOCATION_TEXT,
+ };
+ props.onPress(location);
+ },
+ (errorData) => {
+ if (!shouldTriggerGeolocationCallbacks.current) {
+ return;
+ }
+
+ setIsFetchingCurrentLocation(false);
+ setLocationErrorCode(errorData.code);
+ },
+ {
+ maximumAge: 0, // No cache, always get fresh location info
+ timeout: 5000,
+ },
+ );
+ };
+
+ const renderHeaderComponent = () =>
+ props.predefinedPlaces.length > 0 && (
+ <>
+ {/* This will show current location button in list if there are some recent destinations */}
+ {shouldShowCurrentLocationButton && (
+
+ )}
+ {!props.value && {props.translate('common.recentDestinations')}}
+ >
+ );
+
+ // eslint-disable-next-line arrow-body-style
+ useEffect(() => {
+ return () => {
+ // If the component unmounts we don't want any of the callback for geolocation to run.
+ shouldTriggerGeolocationCallbacks.current = false;
+ };
+ }, []);
+
return (
/*
* The GooglePlacesAutocomplete component uses a VirtualizedList internally,
@@ -269,119 +349,149 @@ function AddressSearch(props) {
* To work around this, we wrap the GooglePlacesAutocomplete component with a horizontal ScrollView
* that has scrolling disabled and would otherwise not be needed
*/
-
-
+
- {props.translate('common.noResultsFound')}
- )
- }
- listLoaderComponent={
-
-
-
- }
- renderHeaderComponent={() =>
- !props.value &&
- props.predefinedPlaces && (
- {props.translate('common.recentDestinations')}
- )
- }
- onPress={(data, details) => {
- saveLocationDetails(data, details);
- setIsTyping(false);
-
- // After we select an option, we set displayListViewBorder to false to prevent UI flickering
- setDisplayListViewBorder(false);
- }}
- query={query}
- requestUrl={{
- useOnPlatform: 'all',
- url: props.network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}),
- }}
- textInputProps={{
- InputComp: TextInput,
- ref: (node) => {
- if (!props.innerRef) {
- return;
- }
-
- if (_.isFunction(props.innerRef)) {
- props.innerRef(node);
- return;
- }
-
- // eslint-disable-next-line no-param-reassign
- props.innerRef.current = node;
- },
- label: props.label,
- containerStyles: props.containerStyles,
- errorText: props.errorText,
- hint: displayListViewBorder ? undefined : props.hint,
- value: props.value,
- defaultValue: props.defaultValue,
- inputID: props.inputID,
- shouldSaveDraft: props.shouldSaveDraft,
- onBlur: (event) => {
- resetDisplayListViewBorderOnBlur(setDisplayListViewBorder, event, containerRef);
- props.onBlur();
- },
- autoComplete: 'off',
- onInputChange: (text) => {
- setIsTyping(true);
- if (props.inputID) {
- props.onInputChange(text);
- } else {
- props.onInputChange({street: text});
- }
-
- // If the text is empty and we have no predefined places, we set displayListViewBorder to false to prevent UI flickering
- if (_.isEmpty(text) && _.isEmpty(props.predefinedPlaces)) {
- setDisplayListViewBorder(false);
- }
- },
- maxLength: props.maxInputLength,
- spellCheck: false,
- }}
- styles={{
- textInputContainer: [styles.flexColumn],
- listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.overflowAuto, styles.borderLeft, styles.borderRight],
- row: [styles.pv4, styles.ph3, styles.overflowAuto],
- description: [styles.googleSearchText],
- separator: [styles.googleSearchSeparator],
- }}
- numberOfLines={2}
- isRowScrollable={false}
- listHoverColor={themeColors.border}
- listUnderlayColor={themeColors.buttonPressedBG}
- onLayout={(event) => {
- // We use the height of the element to determine if we should hide the border of the listView dropdown
- // to prevent a lingering border when there are no address suggestions.
- setDisplayListViewBorder(event.nativeEvent.layout.height > variables.googleEmptyListViewHeight);
- }}
- />
-
-
+
+ {props.translate('common.noResultsFound')}
+ )
+ }
+ listLoaderComponent={
+
+
+
+ }
+ renderHeaderComponent={renderHeaderComponent}
+ onPress={(data, details) => {
+ saveLocationDetails(data, details);
+ setIsTyping(false);
+
+ // After we select an option, we set displayListViewBorder to false to prevent UI flickering
+ setDisplayListViewBorder(false);
+ setIsFocused(false);
+
+ // Clear location error code after address is selected
+ setLocationErrorCode(null);
+ }}
+ query={query}
+ requestUrl={{
+ useOnPlatform: 'all',
+ url: props.network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}),
+ }}
+ textInputProps={{
+ InputComp: TextInput,
+ ref: (node) => {
+ if (!props.innerRef) {
+ return;
+ }
+
+ if (_.isFunction(props.innerRef)) {
+ props.innerRef(node);
+ return;
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ props.innerRef.current = node;
+ },
+ label: props.label,
+ containerStyles: props.containerStyles,
+ errorText: props.errorText,
+ hint:
+ displayListViewBorder || (props.predefinedPlaces.length === 0 && shouldShowCurrentLocationButton) || (props.canUseCurrentLocation && isTyping)
+ ? undefined
+ : props.hint,
+ value: props.value,
+ defaultValue: props.defaultValue,
+ inputID: props.inputID,
+ shouldSaveDraft: props.shouldSaveDraft,
+ onFocus: () => {
+ setIsFocused(true);
+ },
+ onBlur: (event) => {
+ if (!isCurrentTargetInsideContainer(event, containerRef)) {
+ setDisplayListViewBorder(false);
+ setIsFocused(false);
+ setIsTyping(false);
+ }
+ props.onBlur();
+ },
+ autoComplete: 'off',
+ onInputChange: (text) => {
+ setSearchValue(text);
+ setIsTyping(true);
+ if (props.inputID) {
+ props.onInputChange(text);
+ } else {
+ props.onInputChange({street: text});
+ }
+
+ // If the text is empty and we have no predefined places, we set displayListViewBorder to false to prevent UI flickering
+ if (_.isEmpty(text) && _.isEmpty(props.predefinedPlaces)) {
+ setDisplayListViewBorder(false);
+ }
+ },
+ maxLength: props.maxInputLength,
+ spellCheck: false,
+ }}
+ styles={{
+ textInputContainer: [styles.flexColumn],
+ listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.overflowAuto, styles.borderLeft, styles.borderRight, !isFocused && {height: 0}],
+ row: [styles.pv4, styles.ph3, styles.overflowAuto],
+ description: [styles.googleSearchText],
+ separator: [styles.googleSearchSeparator],
+ }}
+ numberOfLines={2}
+ isRowScrollable={false}
+ listHoverColor={themeColors.border}
+ listUnderlayColor={themeColors.buttonPressedBG}
+ onLayout={(event) => {
+ // We use the height of the element to determine if we should hide the border of the listView dropdown
+ // to prevent a lingering border when there are no address suggestions.
+ setDisplayListViewBorder(event.nativeEvent.layout.height > variables.googleEmptyListViewHeight);
+ }}
+ inbetweenCompo={
+ // We want to show the current location button even if there are no recent destinations
+ props.predefinedPlaces.length === 0 && shouldShowCurrentLocationButton ? (
+
+
+
+ ) : (
+ <>>
+ )
+ }
+ />
+ setLocationErrorCode(null)}
+ locationErrorCode={locationErrorCode}
+ />
+
+
+ {isFetchingCurrentLocation && }
+ >
);
}
diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.js b/src/components/AddressSearch/isCurrentTargetInsideContainer.js
new file mode 100644
index 000000000000..18bfc10a8dcb
--- /dev/null
+++ b/src/components/AddressSearch/isCurrentTargetInsideContainer.js
@@ -0,0 +1,8 @@
+function isCurrentTargetInsideContainer(event, containerRef) {
+ // The related target check is required here
+ // because without it when we select an option, the onBlur will still trigger setting displayListViewBorder to false
+ // it will make the auto complete component re-render before onPress is called making selecting an option not working.
+ return containerRef.current && event.target && containerRef.current.contains(event.relatedTarget);
+}
+
+export default isCurrentTargetInsideContainer;
diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.native.js b/src/components/AddressSearch/isCurrentTargetInsideContainer.native.js
new file mode 100644
index 000000000000..dbf0004b08d9
--- /dev/null
+++ b/src/components/AddressSearch/isCurrentTargetInsideContainer.native.js
@@ -0,0 +1,6 @@
+function isCurrentTargetInsideContainer() {
+ // The related target check is not required here because in native there is no race condition rendering like on the web
+ return false;
+}
+
+export default isCurrentTargetInsideContainer;
diff --git a/src/components/AddressSearch/resetDisplayListViewBorderOnBlur.js b/src/components/AddressSearch/resetDisplayListViewBorderOnBlur.js
deleted file mode 100644
index def4da13a9a2..000000000000
--- a/src/components/AddressSearch/resetDisplayListViewBorderOnBlur.js
+++ /dev/null
@@ -1,11 +0,0 @@
-function resetDisplayListViewBorderOnBlur(setDisplayListViewBorder, event, containerRef) {
- // The related target check is required here
- // because without it when we select an option, the onBlur will still trigger setting displayListViewBorder to false
- // it will make the auto complete component re-render before onPress is called making selecting an option not working.
- if (containerRef.current && event.target && containerRef.current.contains(event.relatedTarget)) {
- return;
- }
- setDisplayListViewBorder(false);
-}
-
-export default resetDisplayListViewBorderOnBlur;
diff --git a/src/components/AddressSearch/resetDisplayListViewBorderOnBlur.native.js b/src/components/AddressSearch/resetDisplayListViewBorderOnBlur.native.js
deleted file mode 100644
index 7ae5a44cae71..000000000000
--- a/src/components/AddressSearch/resetDisplayListViewBorderOnBlur.native.js
+++ /dev/null
@@ -1,7 +0,0 @@
-function resetDisplayListViewBorderOnBlur(setDisplayListViewBorder) {
- // The related target check is not required here because in native there is no race condition rendering like on the web
- // onPress still called when cliking the option
- setDisplayListViewBorder(false);
-}
-
-export default resetDisplayListViewBorderOnBlur;
diff --git a/src/components/UserCurrentLocationButton.js b/src/components/UserCurrentLocationButton.js
deleted file mode 100644
index fa22eb602886..000000000000
--- a/src/components/UserCurrentLocationButton.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import PropTypes from 'prop-types';
-import React, {useEffect, useRef, useState} from 'react';
-import {Text} from 'react-native';
-import getCurrentPosition from '../libs/getCurrentPosition';
-import styles from '../styles/styles';
-import Icon from './Icon';
-import * as Expensicons from './Icon/Expensicons';
-import LocationErrorMessage from './LocationErrorMessage';
-import withLocalize, {withLocalizePropTypes} from './withLocalize';
-import colors from '../styles/colors';
-import PressableWithFeedback from './Pressable/PressableWithFeedback';
-
-const propTypes = {
- /** Callback that runs when location data is fetched */
- onLocationFetched: PropTypes.func.isRequired,
-
- /** Callback that runs when fetching location has errors */
- onLocationError: PropTypes.func,
-
- /** Callback that runs when location button is clicked */
- onClick: PropTypes.func,
-
- /** Boolean to indicate if the button is clickable */
- isDisabled: PropTypes.bool,
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- isDisabled: false,
- onLocationError: () => {},
- onClick: () => {},
-};
-
-function UserCurrentLocationButton({onLocationFetched, onLocationError, onClick, isDisabled, translate}) {
- const isFetchingLocation = useRef(false);
- const shouldTriggerCallbacks = useRef(true);
- const [locationErrorCode, setLocationErrorCode] = useState(null);
-
- /** Gets the user's current location and registers success/error callbacks */
- const getUserLocation = () => {
- if (isFetchingLocation.current) {
- return;
- }
-
- isFetchingLocation.current = true;
-
- onClick();
-
- getCurrentPosition(
- (successData) => {
- isFetchingLocation.current = false;
- if (!shouldTriggerCallbacks.current) {
- return;
- }
-
- setLocationErrorCode(null);
- onLocationFetched(successData);
- },
- (errorData) => {
- isFetchingLocation.current = false;
- if (!shouldTriggerCallbacks.current) {
- return;
- }
-
- setLocationErrorCode(errorData.code);
- onLocationError(errorData);
- },
- {
- maximumAge: 0, // No cache, always get fresh location info
- timeout: 5000,
- },
- );
- };
-
- // eslint-disable-next-line arrow-body-style
- useEffect(() => {
- return () => {
- // If the component unmounts we don't want any of the callback for geolocation to run.
- shouldTriggerCallbacks.current = false;
- };
- }, []);
-
- return (
- <>
- e.preventDefault()}
- onTouchStart={(e) => e.preventDefault()}
- >
-
- {translate('location.useCurrent')}
-
- setLocationErrorCode(null)}
- locationErrorCode={locationErrorCode}
- />
- >
- );
-}
-
-UserCurrentLocationButton.displayName = 'UserCurrentLocationButton';
-UserCurrentLocationButton.propTypes = propTypes;
-UserCurrentLocationButton.defaultProps = defaultProps;
-
-// This components gets used inside , we are using an HOC (withLocalize) as function components with
-// hooks give hook errors when nested inside .
-export default withLocalize(UserCurrentLocationButton);
diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js
index 13bf1883804c..a123976b326e 100644
--- a/src/pages/iou/WaypointEditor.js
+++ b/src/pages/iou/WaypointEditor.js
@@ -1,7 +1,7 @@
import React, {useMemo, useRef, useState} from 'react';
import _ from 'underscore';
import lodashGet from 'lodash/get';
-import {Keyboard, View} from 'react-native';
+import {View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import {useNavigation} from '@react-navigation/native';
@@ -23,8 +23,6 @@ import * as Transaction from '../../libs/actions/Transaction';
import * as ValidationUtils from '../../libs/ValidationUtils';
import ROUTES from '../../ROUTES';
import transactionPropTypes from '../../components/transactionPropTypes';
-import UserCurrentLocationButton from '../../components/UserCurrentLocationButton';
-import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator';
import * as ErrorUtils from '../../libs/ErrorUtils';
const propTypes = {
@@ -78,7 +76,6 @@ const defaultProps = {
function WaypointEditor({route: {params: {iouType = '', transactionID = '', waypointIndex = '', threadReportID = 0}} = {}, transaction, recentWaypoints}) {
const {windowWidth} = useWindowDimensions();
const [isDeleteStopModalOpen, setIsDeleteStopModalOpen] = useState(false);
- const [isFetchingLocation, setIsFetchingLocation] = useState(false);
const navigation = useNavigation();
const isFocused = navigation.isFocused();
const {translate} = useLocalize();
@@ -176,26 +173,6 @@ function WaypointEditor({route: {params: {iouType = '', transactionID = '', wayp
Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType));
};
- /**
- * Sets user current location as a waypoint
- * @param {Object} geolocationData
- * @param {Object} geolocationData.coords
- * @param {Number} geolocationData.coords.latitude
- * @param {Number} geolocationData.coords.longitude
- * @param {Number} geolocationData.timestamp
- */
- const selectWaypointFromCurrentLocation = (geolocationData) => {
- setIsFetchingLocation(false);
-
- const waypoint = {
- lat: geolocationData.coords.latitude,
- lng: geolocationData.coords.longitude,
- address: CONST.YOUR_LOCATION_TEXT,
- };
-
- selectWaypoint(waypoint);
- };
-
return (
(textInput.current = e)}
hint={!isOffline ? 'distance.errors.selectSuggestedAddress' : ''}
@@ -265,17 +243,6 @@ function WaypointEditor({route: {params: {iouType = '', transactionID = '', wayp
resultTypes=""
/>
- {
- Keyboard.dismiss();
-
- setIsFetchingLocation(true);
- }}
- onLocationError={() => setIsFetchingLocation(false)}
- onLocationFetched={selectWaypointFromCurrentLocation}
- />
- {isFetchingLocation && }