diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index f8219c028853..e1077a0199cd 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, Keyboard, LogBox, ScrollView, Text, View} from 'react-native'; import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete'; import _ from 'underscore'; @@ -140,46 +140,27 @@ const defaultProps = { resultTypes: 'address', }; -function AddressSearch({ - canUseCurrentLocation, - containerStyles, - defaultValue, - errorText, - hint, - innerRef, - inputID, - isLimitedToUSA, - label, - maxInputLength, - network, - onBlur, - onInputChange, - onPress, - predefinedPlaces, - preferredLocale, - renamedInputKeys, - resultTypes, - shouldSaveDraft, - translate, - value, -}) { +// Do not convert to class component! It's been tried before and presents more challenges than it's worth. +// Relevant thread: https://expensify.slack.com/archives/C03TQ48KC/p1634088400387400 +// Reference: https://github.com/FaridSafi/react-native-google-places-autocomplete/issues/609#issuecomment-886133839 +function AddressSearch(props) { const [displayListViewBorder, setDisplayListViewBorder] = useState(false); const [isTyping, setIsTyping] = useState(false); const [isFocused, setIsFocused] = useState(false); - const [searchValue, setSearchValue] = useState(value || defaultValue || ''); + 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( () => ({ - language: preferredLocale, - types: resultTypes, - components: isLimitedToUSA ? 'country:us' : undefined, + language: props.preferredLocale, + types: props.resultTypes, + components: props.isLimitedToUSA ? 'country:us' : undefined, }), - [preferredLocale, resultTypes, isLimitedToUSA], + [props.preferredLocale, props.resultTypes, props.isLimitedToUSA], ); - const shouldShowCurrentLocationButton = canUseCurrentLocation && searchValue.trim().length === 0 && isFocused; + const shouldShowCurrentLocationButton = props.canUseCurrentLocation && searchValue.trim().length === 0 && isFocused; const saveLocationDetails = (autocompleteData, details) => { const addressComponents = details.address_components; @@ -188,7 +169,7 @@ function AddressSearch({ // to this component which don't match the usual properties coming from auto-complete. In that case, only a limited // amount of data massaging needs to happen for what the parent expects to get from this function. if (_.size(details)) { - onPress({ + props.onPress({ address: lodashGet(details, 'description'), lat: lodashGet(details, 'geometry.location.lat', 0), lng: lodashGet(details, 'geometry.location.lng', 0), @@ -275,7 +256,7 @@ function AddressSearch({ // Not all pages define the Address Line 2 field, so in that case we append any additional address details // (e.g. Apt #) to Address Line 1 - if (subpremise && typeof renamedInputKeys.street2 === 'undefined') { + if (subpremise && typeof props.renamedInputKeys.street2 === 'undefined') { values.street += `, ${subpremise}`; } @@ -284,19 +265,19 @@ function AddressSearch({ values.country = country; } - if (inputID) { - _.each(values, (inputValue, key) => { - const inputKey = lodashGet(renamedInputKeys, key, key); + if (props.inputID) { + _.each(values, (value, key) => { + const inputKey = lodashGet(props.renamedInputKeys, key, key); if (!inputKey) { return; } - onInputChange(inputValue, inputKey); + props.onInputChange(value, inputKey); }); } else { - onInputChange(values); + props.onInputChange(values); } - onPress(values); + props.onPress(values); }; /** Gets the user's current location and registers success/error callbacks */ @@ -344,16 +325,16 @@ function AddressSearch({ }; const renderHeaderComponent = () => - predefinedPlaces.length > 0 && ( + props.predefinedPlaces.length > 0 && ( <> {/* This will show current location button in list if there are some recent destinations */} {shouldShowCurrentLocationButton && ( )} - {!value && {translate('common.recentDestinations')}} + {!props.value && {props.translate('common.recentDestinations')}} ); @@ -365,26 +346,6 @@ function AddressSearch({ }; }, []); - const listEmptyComponent = useCallback( - () => - network.isOffline || !isTyping ? null : ( - {translate('common.noResultsFound')} - ), - [isTyping, translate, network.isOffline], - ); - - const listLoader = useCallback( - () => ( - - - - ), - [], - ); - return ( /* * The GooglePlacesAutocomplete component uses a VirtualizedList internally, @@ -411,10 +372,20 @@ function AddressSearch({ fetchDetails suppressDefaultStyles enablePoweredByContainer={false} - predefinedPlaces={predefinedPlaces} - listEmptyComponent={listEmptyComponent} - listLoaderComponent={listLoader} - renderHeaderComponent={renderHeaderComponent} + predefinedPlaces={props.predefinedPlaces} + listEmptyComponent={ + props.network.isOffline || !isTyping ? null : ( + {props.translate('common.noResultsFound')} + ) + } + listLoaderComponent={ + + + + } renderRow={(data) => { const title = data.isPredefinedPlace ? data.name : data.structured_formatting.main_text; const subtitle = data.isPredefinedPlace ? data.description : data.structured_formatting.secondary_text; @@ -425,6 +396,7 @@ function AddressSearch({ ); }} + renderHeaderComponent={renderHeaderComponent} onPress={(data, details) => { saveLocationDetails(data, details); setIsTyping(false); @@ -439,31 +411,34 @@ function AddressSearch({ query={query} requestUrl={{ useOnPlatform: 'all', - url: network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}), + url: props.network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}), }} textInputProps={{ InputComp: TextInput, ref: (node) => { - if (!innerRef) { + if (!props.innerRef) { return; } - if (_.isFunction(innerRef)) { - innerRef(node); + if (_.isFunction(props.innerRef)) { + props.innerRef(node); return; } // eslint-disable-next-line no-param-reassign - innerRef.current = node; + props.innerRef.current = node; }, - label, - containerStyles, - errorText, - hint: displayListViewBorder || (predefinedPlaces.length === 0 && shouldShowCurrentLocationButton) || (canUseCurrentLocation && isTyping) ? undefined : hint, - value, - defaultValue, - inputID, - shouldSaveDraft, + 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); }, @@ -473,24 +448,24 @@ function AddressSearch({ setIsFocused(false); setIsTyping(false); } - onBlur(); + props.onBlur(); }, autoComplete: 'off', onInputChange: (text) => { setSearchValue(text); setIsTyping(true); - if (inputID) { - onInputChange(text); + if (props.inputID) { + props.onInputChange(text); } else { - onInputChange({street: text}); + 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(predefinedPlaces)) { + if (_.isEmpty(text) && _.isEmpty(props.predefinedPlaces)) { setDisplayListViewBorder(false); } }, - maxLength: maxInputLength, + maxLength: props.maxInputLength, spellCheck: false, }} styles={{ @@ -511,18 +486,17 @@ function AddressSearch({ }} inbetweenCompo={ // We want to show the current location button even if there are no recent destinations - predefinedPlaces.length === 0 && shouldShowCurrentLocationButton ? ( + props.predefinedPlaces.length === 0 && shouldShowCurrentLocationButton ? ( ) : ( <> ) } - placeholder="" /> setLocationErrorCode(null)} diff --git a/src/components/CheckboxWithLabel.js b/src/components/CheckboxWithLabel.js index f18ec346dfa2..4bffadecb733 100644 --- a/src/components/CheckboxWithLabel.js +++ b/src/components/CheckboxWithLabel.js @@ -7,7 +7,6 @@ import variables from '@styles/variables'; import Checkbox from './Checkbox'; import FormHelpMessage from './FormHelpMessage'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; -import refPropTypes from './refPropTypes'; import Text from './Text'; /** @@ -55,7 +54,7 @@ const propTypes = { defaultValue: PropTypes.bool, /** React ref being forwarded to the Checkbox input */ - forwardedRef: refPropTypes, + forwardedRef: PropTypes.func, /** The ID used to uniquely identify the input in a Form */ /* eslint-disable-next-line react/no-unused-prop-types */ diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 85408323c9f2..92baa9727832 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -71,8 +71,6 @@ const propTypes = { shouldValidateOnChange: PropTypes.bool, }; -const VALIDATE_DELAY = 200; - const defaultProps = { isSubmitButtonVisible: true, formState: { @@ -248,28 +246,19 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC // as this is already happening by the value prop. defaultValue: undefined, onTouched: (event) => { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); + setTouchedInput(inputID); if (_.isFunction(propsToParse.onTouched)) { propsToParse.onTouched(event); } }, onPress: (event) => { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); + setTouchedInput(inputID); if (_.isFunction(propsToParse.onPress)) { propsToParse.onPress(event); } }, - onPressOut: (event) => { - // To prevent validating just pressed inputs, we need to set the touched input right after - // onValidate and to do so, we need to delays setTouchedInput of the same amount of time - // as the onValidate is delayed - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); + onPressIn: (event) => { + setTouchedInput(inputID); if (_.isFunction(propsToParse.onPressIn)) { propsToParse.onPressIn(event); } @@ -285,7 +274,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC if (shouldValidateOnBlur) { onValidate(inputValues, !hasServerError); } - }, VALIDATE_DELAY); + }, 200); } if (_.isFunction(propsToParse.onBlur)) { diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index b2e6f4477e89..99237fd8db43 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -1,13 +1,12 @@ import PropTypes from 'prop-types'; import React, {forwardRef, useContext} from 'react'; -import refPropTypes from '@components/refPropTypes'; import FormContext from './FormContext'; const propTypes = { InputComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.elementType]).isRequired, inputID: PropTypes.string.isRequired, valueType: PropTypes.string, - forwardedRef: refPropTypes, + forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]), }; const defaultProps = { diff --git a/src/pages/settings/Wallet/AddDebitCardPage.js b/src/pages/settings/Wallet/AddDebitCardPage.js index 5b99bd49812c..ab948dcdc589 100644 --- a/src/pages/settings/Wallet/AddDebitCardPage.js +++ b/src/pages/settings/Wallet/AddDebitCardPage.js @@ -4,8 +4,7 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import AddressSearch from '@components/AddressSearch'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; +import Form from '@components/Form'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import StatePicker from '@components/StatePicker'; @@ -118,7 +117,7 @@ function DebitCardPage(props) { title={translate('addDebitCardPage.addADebitCard')} onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET)} /> - - (nameOnCardRef.current = ref)} spellCheck={false} /> - - - - - - + - ( {`${translate('common.iAcceptThe')}`} @@ -210,7 +198,7 @@ function DebitCardPage(props) { )} style={[styles.mt4]} /> - + ); }