diff --git a/src/components/AddressSearch.js b/src/components/AddressSearch.js index f8ef4e76c186..9d3cacceca6d 100644 --- a/src/components/AddressSearch.js +++ b/src/components/AddressSearch.js @@ -1,5 +1,5 @@ import _ from 'underscore'; -import React, {useEffect, useState, useRef} from 'react'; +import React, {useRef, useState} from 'react'; import PropTypes from 'prop-types'; import {LogBox} from 'react-native'; import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete'; @@ -23,7 +23,7 @@ const propTypes = { value: PropTypes.string, /** A callback function when the value of this field has changed */ - onChangeText: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, /** Customize the ExpensiTextInput container */ containerStyles: PropTypes.arrayOf(PropTypes.object), @@ -40,52 +40,53 @@ const defaultProps = { // Relevant thread: https://expensify.slack.com/archives/C03TQ48KC/p1634088400387400 // Reference: https://github.com/FaridSafi/react-native-google-places-autocomplete/issues/609#issuecomment-886133839 const AddressSearch = (props) => { - const googlePlacesRef = useRef(); const [displayListViewBorder, setDisplayListViewBorder] = useState(false); - useEffect(() => { - if (!googlePlacesRef.current) { - return; - } - googlePlacesRef.current.setAddressText(props.value); - }, []); + // We use `skippedFirstOnChangeTextRef` to work around a feature of the library: + // The library is calling onChangeText with '' at the start and we don't need this + // https://github.com/FaridSafi/react-native-google-places-autocomplete/blob/47d7223dd48f85da97e80a0729a985bbbcee353f/GooglePlacesAutocomplete.js#L148 + const skippedFirstOnChangeTextRef = useRef(false); const saveLocationDetails = (details) => { const addressComponents = details.address_components; - if (GooglePlacesUtils.isAddressValidForVBA(addressComponents)) { - // Gather the values from the Google details - const streetNumber = GooglePlacesUtils.getAddressComponent(addressComponents, 'street_number', 'long_name'); - const streetName = GooglePlacesUtils.getAddressComponent(addressComponents, 'route', 'long_name'); - let city = GooglePlacesUtils.getAddressComponent(addressComponents, 'locality', 'long_name'); - if (!city) { - city = GooglePlacesUtils.getAddressComponent(addressComponents, 'sublocality', 'long_name'); - Log.hmmm('[AddressSearch] Replacing missing locality with sublocality: ', {address: details.formatted_address, sublocality: city}); - } - const state = GooglePlacesUtils.getAddressComponent(addressComponents, 'administrative_area_level_1', 'short_name'); - const zipCode = GooglePlacesUtils.getAddressComponent(addressComponents, 'postal_code', 'long_name'); - - // Trigger text change events for each of the individual fields being saved on the server - props.onChangeText('addressStreet', `${streetNumber} ${streetName}`); - props.onChangeText('addressCity', city); - props.onChangeText('addressState', state); - props.onChangeText('addressZipCode', zipCode); - } else { - // Clear the values associated to the address, so our validations catch the problem - Log.hmmm('[AddressSearch] Search result failed validation: ', { - address: details.formatted_address, - address_components: addressComponents, - place_id: details.place_id, - }); - props.onChangeText('addressStreet', null); - props.onChangeText('addressCity', null); - props.onChangeText('addressState', null); - props.onChangeText('addressZipCode', null); + if (!addressComponents) { + return; + } + + // Gather the values from the Google details + const streetNumber = GooglePlacesUtils.getAddressComponent(addressComponents, 'street_number', 'long_name') || ''; + const streetName = GooglePlacesUtils.getAddressComponent(addressComponents, 'route', 'long_name') || ''; + const street = `${streetNumber} ${streetName}`.trim(); + let city = GooglePlacesUtils.getAddressComponent(addressComponents, 'locality', 'long_name'); + if (!city) { + city = GooglePlacesUtils.getAddressComponent(addressComponents, 'sublocality', 'long_name'); + Log.hmmm('[AddressSearch] Replacing missing locality with sublocality: ', {address: details.formatted_address, sublocality: city}); + } + const zipCode = GooglePlacesUtils.getAddressComponent(addressComponents, 'postal_code', 'long_name'); + const state = GooglePlacesUtils.getAddressComponent(addressComponents, 'administrative_area_level_1', 'short_name'); + + const values = {}; + if (street && street.length > props.value.length) { + // Don't replace if the user has typed something longer. I.e. maybe the user entered the Apt # + values.street = street; } + if (city) { + values.city = city; + } + if (zipCode) { + values.zipCode = zipCode; + } + if (state) { + values.state = state; + } + if (_.size(values) === 0) { + return; + } + props.onChange(values); }; return ( { label: props.label, containerStyles: props.containerStyles, errorText: props.errorText, + value: props.value, onChangeText: (text) => { - const isTextValid = !_.isEmpty(text) && _.isEqual(text, props.value); - - // Ensure whether an address is selected already or has address value initialized. - if (!_.isEmpty(googlePlacesRef.current.getAddressText()) && !isTextValid) { - saveLocationDetails({}); + if (skippedFirstOnChangeTextRef.current) { + props.onChange({street: text}); + } else { + skippedFirstOnChangeTextRef.current = true; } // If the text is empty, we set displayListViewBorder to false to prevent UI flickering diff --git a/src/libs/GooglePlacesUtils.js b/src/libs/GooglePlacesUtils.js index 71c738062d11..5a875e7f102a 100644 --- a/src/libs/GooglePlacesUtils.js +++ b/src/libs/GooglePlacesUtils.js @@ -21,41 +21,7 @@ function getAddressComponent(addressComponents, type, key) { .value(); } -/** - * Validates this contains the minimum address components - * - * @param {Array} addressComponents - * @returns {Boolean} - */ -function isAddressValidForVBA(addressComponents) { - if (!addressComponents) { - return false; - } - if (!_.some(addressComponents, component => _.includes(component.types, 'street_number'))) { - // Missing Street number - return false; - } - if (!_.some(addressComponents, component => _.includes(component.types, 'postal_code'))) { - // Missing zip code - return false; - } - if (!_.some(addressComponents, component => _.includes(component.types, 'administrative_area_level_1'))) { - // Missing state - return false; - } - if (!_.some(addressComponents, component => _.includes(component.types, 'locality')) - && !_.some(addressComponents, component => _.includes(component.types, 'sublocality'))) { - // Missing city - return false; - } - if (_.some(addressComponents, component => _.includes(component.types, 'post_box'))) { - // Reject PO box - return false; - } - return true; -} - export { + // eslint-disable-next-line import/prefer-default-export getAddressComponent, - isAddressValidForVBA, }; diff --git a/src/libs/ReimbursementAccountUtils.js b/src/libs/ReimbursementAccountUtils.js index 5de61144092b..2cbf51c8cc93 100644 --- a/src/libs/ReimbursementAccountUtils.js +++ b/src/libs/ReimbursementAccountUtils.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import lodashGet from 'lodash/get'; import lodashUnset from 'lodash/unset'; import lodashCloneDeep from 'lodash/cloneDeep'; @@ -27,21 +28,30 @@ function getErrors(props) { /** * @param {Object} props - * @param {String} path + * @param {String[]} paths */ -function clearError(props, path) { +function clearErrors(props, paths) { const errors = getErrors(props); - if (!lodashGet(errors, path, false)) { + const pathsWithErrors = _.filter(paths, path => lodashGet(errors, path, false)); + if (_.size(pathsWithErrors) === 0) { // No error found for this path return; } // Clear the existing errors const newErrors = lodashCloneDeep(errors); - lodashUnset(newErrors, path); + _.forEach(pathsWithErrors, path => lodashUnset(newErrors, path)); BankAccounts.setBankAccountFormValidationErrors(newErrors); } +/** + * @param {Object} props + * @param {String} path + */ +function clearError(props, path) { + clearErrors(props, [path]); +} + /** * @param {Object} props * @param {Object} errorTranslationKeys @@ -57,5 +67,6 @@ export { getDefaultStateForField, getErrors, clearError, + clearErrors, getErrorText, }; diff --git a/src/pages/ReimbursementAccount/ACHContractStep.js b/src/pages/ReimbursementAccount/ACHContractStep.js index 74b690591130..110ef0aa9e89 100644 --- a/src/pages/ReimbursementAccount/ACHContractStep.js +++ b/src/pages/ReimbursementAccount/ACHContractStep.js @@ -59,17 +59,12 @@ class ACHContractStep extends React.Component { certifyTrueInformation: 'beneficialOwnersStep.error.certify', }; + this.getErrors = () => ReimbursementAccountUtils.getErrors(this.props); this.clearError = inputKey => ReimbursementAccountUtils.clearError(this.props, inputKey); + this.clearErrors = inputKeys => ReimbursementAccountUtils.clearErrors(this.props, inputKeys); this.getErrorText = inputKey => ReimbursementAccountUtils.getErrorText(this.props, this.errorTranslationKeys, inputKey); } - /** - * @returns {Object} - */ - getErrors() { - return lodashGet(this.props, ['reimbursementAccount', 'errors'], {}); - } - /** * @returns {Boolean} */ @@ -122,31 +117,24 @@ class ACHContractStep extends React.Component { * Clear the error associated to inputKey if found and store the inputKey new value in the state. * * @param {Integer} ownerIndex - * @param {String} inputKey - * @param {String} value + * @param {Object} values */ - clearErrorAndSetBeneficialOwnerValue(ownerIndex, inputKey, value) { + clearErrorAndSetBeneficialOwnerValues(ownerIndex, values) { this.setState((prevState) => { - const renamedFields = { - addressStreet: 'street', - addressCity: 'city', - addressState: 'state', - addressZipCode: 'zipCode', - }; - const renamedInputKey = lodashGet(renamedFields, inputKey, inputKey); const beneficialOwners = [...prevState.beneficialOwners]; - beneficialOwners[ownerIndex] = {...beneficialOwners[ownerIndex], [renamedInputKey]: value}; - if (inputKey !== 'manualAddress') { - BankAccounts.updateReimbursementAccountDraft({beneficialOwners}); - } + beneficialOwners[ownerIndex] = {...beneficialOwners[ownerIndex], ...values}; + BankAccounts.updateReimbursementAccountDraft({beneficialOwners}); return {beneficialOwners}; }); + // Prepare inputKeys for clearing errors + const inputKeys = _.keys(values); + // dob field has multiple validations/errors, we are handling it temporarily like this. - if (inputKey === 'dob') { - this.clearError(`beneficialOwnersErrors.${ownerIndex}.dobAge`); + if (_.contains(inputKeys, 'dob')) { + inputKeys.push('dobAge'); } - this.clearError(`beneficialOwnersErrors.${ownerIndex}.${inputKey}`); + this.clearErrors(_.map(inputKeys, inputKey => `beneficialOwnersErrors.${ownerIndex}.${inputKey}`)); } submit() { @@ -235,7 +223,7 @@ class ACHContractStep extends React.Component { this.clearErrorAndSetBeneficialOwnerValue(index, inputKey, value)} + onFieldChange={values => this.clearErrorAndSetBeneficialOwnerValues(index, values)} values={{ firstName: owner.firstName || '', lastName: owner.lastName || '', @@ -245,7 +233,6 @@ class ACHContractStep extends React.Component { zipCode: owner.zipCode || '', dob: owner.dob || '', ssnLast4: owner.ssnLast4 || '', - manualAddress: owner.manualAddress, }} errors={lodashGet(this.getErrors(), `beneficialOwnersErrors[${index}]`, {})} /> diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js new file mode 100644 index 000000000000..acfcc5d8650f --- /dev/null +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -0,0 +1,90 @@ +import React from 'react'; +import {View} from 'react-native'; +import PropTypes from 'prop-types'; +import ExpensiTextInput from '../../components/ExpensiTextInput'; +import AddressSearch from '../../components/AddressSearch'; +import styles from '../../styles/styles'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import CONST from '../../CONST'; +import StatePicker from '../../components/StatePicker'; +import Text from '../../components/Text'; + +const propTypes = { + /** Callback fired when a field changes. Passes args as {[fieldName]: val} */ + onFieldChange: PropTypes.func.isRequired, + + /** Form values */ + values: PropTypes.shape({ + /** Address street field */ + street: PropTypes.string, + + /** Address city field */ + city: PropTypes.string, + + /** Address state field */ + state: PropTypes.string, + + /** Address zip code field */ + zipCode: PropTypes.string, + }), + + /** Any errors that can arise from form validation */ + errors: PropTypes.objectOf(PropTypes.bool), + + ...withLocalizePropTypes, +}; + +const defaultProps = { + values: { + street: '', + city: '', + state: '', + zipCode: '', + }, + errors: {}, +}; + +const AddressForm = props => ( + <> + + {props.translate('common.noPO')} + + + props.onFieldChange({city: value})} + errorText={props.errors.city ? props.translate('bankAccount.error.addressCity') : ''} + /> + + + props.onFieldChange({state: value})} + errorText={props.errors.state ? props.translate('bankAccount.error.addressState') : ''} + hasError={Boolean(props.errors.state)} + /> + + + props.onFieldChange({zipCode: value})} + errorText={props.errors.zipCode ? props.translate('bankAccount.error.zipCode') : ''} + maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} + /> + +); + +AddressForm.propTypes = propTypes; +AddressForm.defaultProps = defaultProps; +AddressForm.displayName = 'AddressForm'; +export default withLocalize(AddressForm); diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index ae28d375ae66..14379652b4f7 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -24,7 +24,7 @@ import ExpensiPicker from '../../components/ExpensiPicker'; import * as ReimbursementAccountUtils from '../../libs/ReimbursementAccountUtils'; import reimbursementAccountPropTypes from './reimbursementAccountPropTypes'; import ReimbursementAccountForm from './ReimbursementAccountForm'; -import AddressSearch from '../../components/AddressSearch'; +import AddressForm from './AddressForm'; const propTypes = { /** Bank account currently in setup */ @@ -78,10 +78,6 @@ class CompanyStep extends React.Component { // Map a field to the key of the error's translation this.errorTranslationKeys = { companyName: 'bankAccount.error.companyName', - addressStreet: 'bankAccount.error.addressStreet', - addressCity: 'bankAccount.error.addressCity', - addressState: 'bankAccount.error.addressState', - addressZipCode: 'bankAccount.error.zipCode', companyPhone: 'bankAccount.error.phoneNumber', website: 'bankAccount.error.website', companyTaxID: 'bankAccount.error.taxID', @@ -91,29 +87,21 @@ class CompanyStep extends React.Component { hasNoConnectionToCannabis: 'bankAccount.error.restrictedBusiness', }; + // AddressSearch uses different keys for these fields + this.renamedFields = { + street: 'addressStreet', + state: 'addressState', + city: 'addressCity', + zipCode: 'addressZipCode', + }; + this.getErrorText = inputKey => ReimbursementAccountUtils.getErrorText(this.props, this.errorTranslationKeys, inputKey); this.clearError = inputKey => ReimbursementAccountUtils.clearError(this.props, inputKey); + this.clearErrors = inputKeys => ReimbursementAccountUtils.clearErrors(this.props, inputKeys); this.getErrors = () => ReimbursementAccountUtils.getErrors(this.props); this.clearDateErrorsAndSetValue = this.clearDateErrorsAndSetValue.bind(this); } - getFormattedAddressValue() { - let addressString = ''; - if (this.state.addressStreet) { - addressString += `${this.state.addressStreet}, `; - } - if (this.state.addressCity) { - addressString += `${this.state.addressCity}, `; - } - if (this.state.addressState) { - addressString += `${this.state.addressState}, `; - } - if (this.state.addressZipCode) { - addressString += `${this.state.addressZipCode}`; - } - return addressString; - } - /** * @param {String} value */ @@ -150,14 +138,12 @@ class CompanyStep extends React.Component { validate() { const errors = {}; - if (this.state.manualAddress) { - if (!ValidationUtils.isValidAddress(this.state.addressStreet)) { - errors.addressStreet = true; - } + if (!ValidationUtils.isValidAddress(this.state.addressStreet)) { + errors.addressStreet = true; + } - if (!ValidationUtils.isValidZipCode(this.state.addressZipCode)) { - errors.addressZipCode = true; - } + if (!ValidationUtils.isValidZipCode(this.state.addressZipCode)) { + errors.addressZipCode = true; } if (!ValidationUtils.isValidURL(this.state.website)) { @@ -226,66 +212,29 @@ class CompanyStep extends React.Component { disabled={shouldDisableCompanyName} errorText={this.getErrorText('companyName')} /> - {!this.state.manualAddress && ( - <> - this.clearErrorAndSetValue(fieldName, value)} - errorText={this.getErrorText('addressStreet')} - /> - - {this.props.translate('common.cantFindAddress')} - this.setState({manualAddress: true})} - > - {this.props.translate('common.enterManually')} - - - - )} - {this.state.manualAddress && ( - <> - this.clearErrorAndSetValue('addressStreet', value)} - value={this.state.addressStreet} - errorText={this.getErrorText('addressStreet')} - /> - {this.props.translate('common.noPO')} - - - this.clearErrorAndSetValue('addressCity', value)} - value={this.state.addressCity} - errorText={this.getErrorText('addressCity')} - /> - - - this.clearErrorAndSetValue('addressState', value)} - value={this.state.addressState} - hasError={this.getErrors().addressState} - /> - - - this.clearErrorAndSetValue('addressZipCode', value)} - value={this.state.addressZipCode} - errorText={this.getErrorText('addressZipCode')} - /> - - )} - + { + const renamedValues = {}; + _.each(values, (value, inputKey) => { + const renamedInputKey = lodashGet(this.renamedFields, inputKey, inputKey); + renamedValues[renamedInputKey] = value; + }); + this.setValue(renamedValues); + this.clearErrors(_.keys(renamedValues)); + }} + /> { const dobErrorText = (props.errors.dob ? props.translate('bankAccount.error.dob') : '') || (props.errors.dobAge ? props.translate('bankAccount.error.age') : ''); - const getFormattedAddressValue = () => { - let addressString = ''; - if (props.values.street) { - addressString += `${props.values.street}, `; - } - if (props.values.city) { - addressString += `${props.values.city}, `; - } - if (props.values.state) { - addressString += `${props.values.state}, `; - } - if (props.values.zipCode) { - addressString += `${props.values.zipCode}`; - } - return addressString; - }; - return ( @@ -101,7 +77,7 @@ const IdentityForm = (props) => { props.onFieldChange('firstName', value)} + onChangeText={value => props.onFieldChange({firstName: value})} errorText={props.errors.firstName ? props.translate('bankAccount.error.firstName') : ''} /> @@ -109,7 +85,7 @@ const IdentityForm = (props) => { props.onFieldChange('lastName', value)} + onChangeText={value => props.onFieldChange({lastName: value})} errorText={props.errors.lastName ? props.translate('bankAccount.error.lastName') : ''} /> @@ -119,7 +95,7 @@ const IdentityForm = (props) => { containerStyles={[styles.mt4]} placeholder={props.translate('common.dateFormat')} value={props.values.dob} - onChange={value => props.onFieldChange('dob', value)} + onChange={value => props.onFieldChange({dob: value})} errorText={dobErrorText} /> { containerStyles={[styles.mt4]} keyboardType={CONST.KEYBOARD_TYPE.NUMERIC} value={props.values.ssnLast4} - onChangeText={value => props.onFieldChange('ssnLast4', value)} + onChangeText={value => props.onFieldChange({ssnLast4: value})} errorText={props.errors.ssnLast4 ? props.translate('bankAccount.error.ssnLast4') : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.SSN} /> - {props.values.manualAddress ? ( - <> - props.onFieldChange('addressStreet', value)} - errorText={props.errors.street ? props.translate('bankAccount.error.address') : ''} - /> - {props.translate('common.noPO')} - - - props.onFieldChange('addressCity', value)} - errorText={props.errors.city ? props.translate('bankAccount.error.addressCity') : ''} - /> - - - props.onFieldChange('addressState', value)} - errorText={props.errors.state ? props.translate('bankAccount.error.addressState') : ''} - hasError={Boolean(props.errors.state)} - /> - - - props.onFieldChange('addressZipCode', value)} - errorText={props.errors.zipCode ? props.translate('bankAccount.error.zipCode') : ''} - maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} - /> - - ) : ( - <> - props.onFieldChange(fieldName, value)} - errorText={props.errors.street ? props.translate('bankAccount.error.addressStreet') : ''} - /> - - {props.translate('common.cantFindAddress')} - props.onFieldChange('manualAddress', true)} - > - {props.translate('common.enterManually')} - - - - )} + ); }; diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js index 13555bab55a6..7359a15ac6af 100644 --- a/src/pages/ReimbursementAccount/RequestorStep.js +++ b/src/pages/ReimbursementAccount/RequestorStep.js @@ -37,7 +37,7 @@ class RequestorStep extends React.Component { super(props); this.submit = this.submit.bind(this); - this.clearErrorAndSetValue = this.clearErrorAndSetValue.bind(this); + this.clearErrorsAndSetValues = this.clearErrorsAndSetValues.bind(this); this.state = { firstName: ReimbursementAccountUtils.getDefaultStateForField(props, 'firstName'), @@ -55,8 +55,6 @@ class RequestorStep extends React.Component { // Required fields not validated by `validateIdentity` this.requiredFields = [ - 'firstName', - 'lastName', 'isControllingOfficer', ]; @@ -68,38 +66,38 @@ class RequestorStep extends React.Component { }; this.clearError = inputKey => ReimbursementAccountUtils.clearError(this.props, inputKey); + this.clearErrors = inputKeys => ReimbursementAccountUtils.clearErrors(this.props, inputKeys); this.getErrors = () => ReimbursementAccountUtils.getErrors(this.props); } /** - * Clear the error associated to inputKey if found and store the inputKey new value in the state. + * Clear the errors associated to keys in values if found and store the new values in the state. * - * @param {String} inputKey - * @param {String|Boolean} value + * @param {Object} values */ - clearErrorAndSetValue(inputKey, value) { - if (inputKey === 'manualAddress') { - this.setState({ - manualAddress: value, - }); - } else { - const renamedFields = { - addressStreet: 'requestorAddressStreet', - addressCity: 'requestorAddressCity', - addressState: 'requestorAddressState', - addressZipCode: 'requestorAddressZipCode', - }; + clearErrorsAndSetValues(values) { + const renamedFields = { + street: 'requestorAddressStreet', + city: 'requestorAddressCity', + state: 'requestorAddressState', + zipCode: 'requestorAddressZipCode', + }; + const newState = {}; + _.each(values, (value, inputKey) => { const renamedInputKey = lodashGet(renamedFields, inputKey, inputKey); - const newState = {[renamedInputKey]: value}; - this.setState(newState); - BankAccounts.updateReimbursementAccountDraft(newState); + newState[renamedInputKey] = value; + }); + this.setState(newState); + BankAccounts.updateReimbursementAccountDraft(newState); - // dob field has multiple validations/errors, we are handling it temporarily like this. - if (inputKey === 'dob') { - this.clearError('dobAge'); - } - this.clearError(inputKey); + // Prepare inputKeys for clearing errors + const inputKeys = _.keys(values); + + // dob field has multiple validations/errors, we are handling it temporarily like this. + if (_.contains(inputKeys, 'dob')) { + inputKeys.push('dobAge'); } + this.clearErrors(inputKeys); } /** @@ -198,17 +196,16 @@ class RequestorStep extends React.Component { diff --git a/src/pages/settings/Payments/AddDebitCardPage.js b/src/pages/settings/Payments/AddDebitCardPage.js index 4de3352e4983..83dc77abd556 100644 --- a/src/pages/settings/Payments/AddDebitCardPage.js +++ b/src/pages/settings/Payments/AddDebitCardPage.js @@ -18,13 +18,13 @@ import * as PaymentMethods from '../../../libs/actions/PaymentMethods'; import KeyboardAvoidingView from '../../../components/KeyboardAvoidingView'; import * as ValidationUtils from '../../../libs/ValidationUtils'; import CheckboxWithLabel from '../../../components/CheckboxWithLabel'; +import StatePicker from '../../../components/StatePicker'; import ExpensiTextInput from '../../../components/ExpensiTextInput'; import CONST from '../../../CONST'; import FormAlertWithSubmitButton from '../../../components/FormAlertWithSubmitButton'; import ONYXKEYS from '../../../ONYXKEYS'; import compose from '../../../libs/compose'; import AddressSearch from '../../../components/AddressSearch'; -import StatePicker from '../../../components/StatePicker'; const propTypes = { addDebitCardForm: PropTypes.shape({ @@ -131,12 +131,18 @@ class DebitCardPage extends Component { errors.securityCode = true; } - if (!ValidationUtils.isValidAddress(this.state.addressStreet) - || !this.state.addressState - || !ValidationUtils.isValidZipCode(this.state.addressZipCode)) { + if (!ValidationUtils.isValidAddress(this.state.addressStreet)) { errors.addressStreet = true; } + if (!ValidationUtils.isValidZipCode(this.state.addressZipCode)) { + errors.addressZipCode = true; + } + + if (!this.state.addressState) { + errors.addressState = true; + } + if (!this.state.acceptedTerms) { errors.acceptedTerms = true; } @@ -226,64 +232,44 @@ class DebitCardPage extends Component { /> - {!this.state.manualAddress && ( - <> - this.clearErrorAndSetValue(fieldName, value)} - errorText={this.getErrorText('addressStreet')} - /> - - {this.props.translate('common.cantFindAddress')} - this.setState({manualAddress: true})} - > - {this.props.translate('common.enterManually')} - - - - )} - {this.state.manualAddress && ( - <> - this.clearErrorAndSetValue('addressStreet', value)} - value={this.state.addressStreet} - errorText={this.getErrorText('addressStreet')} - /> - - - this.clearErrorAndSetValue('addressCity', value)} - value={this.state.addressCity} - errorText={this.getErrorText('addressCity')} - /> - - - this.clearErrorAndSetValue('addressState', value)} - value={this.state.addressState} - hasError={Boolean(this.getErrorText('addressState'))} - /> - - + { + const renamedFields = { + street: 'addressStreet', + state: 'addressState', + zipCode: 'addressZipCode', + }; + _.each(values, (value, inputKey) => { + if (inputKey === 'city') { + return; + } + const renamedInputKey = lodashGet(renamedFields, inputKey, inputKey); + this.clearErrorAndSetValue(renamedInputKey, value); + }); + }} + errorText={this.getErrorText('addressStreet')} + /> + + this.clearErrorAndSetValue('addressZipCode', value)} value={this.state.addressZipCode} errorText={this.getErrorText('addressZipCode')} /> - - )} + + + this.clearErrorAndSetValue('addressState', value)} + value={this.state.addressState} + hasError={lodashGet(this.state.errors, 'addressState', false)} + /> + + { diff --git a/tests/unit/GooglePlacesUtilsTest.js b/tests/unit/GooglePlacesUtilsTest.js index 9c8552286b1c..2993254a491b 100644 --- a/tests/unit/GooglePlacesUtilsTest.js +++ b/tests/unit/GooglePlacesUtilsTest.js @@ -1,109 +1,6 @@ import * as GooglePlacesUtils from '../../src/libs/GooglePlacesUtils'; describe('GooglePlacesUtilsTest', () => { - describe('isAddressValidForVBA', () => { - it('should reject Google Places result with missing street number', () => { - // This result appears when searching for "25220 Quail Ridge Road, Escondido, CA, 97027" - const googlePlacesRouteResult = { - address_components: [ - { - long_name: 'Quail Ridge Road', - short_name: 'Quail Ridge Rd', - types: ['route'], - }, - { - long_name: 'Escondido', - short_name: 'Escondido', - types: ['locality', 'political'], - }, - { - long_name: 'San Diego County', - short_name: 'San Diego County', - types: ['administrative_area_level_2', 'political'], - }, - { - long_name: 'California', - short_name: 'CA', - types: ['administrative_area_level_1', 'political'], - }, - { - long_name: 'United States', - short_name: 'US', - types: ['country', 'political'], - }, - { - long_name: '92027', - short_name: '92027', - types: ['postal_code'], - }, - ], - formatted_address: 'Quail Ridge Rd, Escondido, CA 92027, USA', - place_id: 'EihRdWFpbCBSaWRnZSBSZCwgRXNjb25kaWRvLCBDQSA5MjAyNywgVVNBIi4qLAoUChIJIQBiT7Pz24ARmaXMgCMhqAUSFAoSCXtDwoFe89uAEd_FlncPyNEB', - types: ['route'], - }; - const isValid = GooglePlacesUtils.isAddressValidForVBA(googlePlacesRouteResult.address_components); - expect(isValid).toStrictEqual(false); - }); - - it('should accept Google Places result with missing locality if sublocality is available', () => { - // This result appears when searching for "64 Noll Street, Brooklyn, NY, USA" - const brooklynAddressResult = { - address_components: [ - { - long_name: '64', - short_name: '64', - types: ['street_number'], - }, - { - long_name: 'Noll Street', - short_name: 'Noll St', - types: ['route'], - }, - { - long_name: 'Bushwick', - short_name: 'Bushwick', - types: ['neighborhood', 'political'], - }, - { - long_name: 'Brooklyn', - short_name: 'Brooklyn', - types: ['sublocality_level_1', 'sublocality', 'political'], - }, - { - long_name: 'Kings County', - short_name: 'Kings County', - types: ['administrative_area_level_2', 'political'], - }, - { - long_name: 'New York', - short_name: 'NY', - types: ['administrative_area_level_1', 'political'], - }, - { - long_name: 'United States', - short_name: 'US', - types: ['country', 'political'], - }, - { - long_name: '11206', - short_name: '11206', - types: ['postal_code'], - }, - { - long_name: '4604', - short_name: '4604', - types: ['postal_code_suffix'], - }, - ], - formatted_address: '64 Noll St, Brooklyn, NY 11206, USA', - // eslint-disable-next-line max-len - place_id: 'EiM2NCBOb2xsIFN0LCBCcm9va2x5biwgTlkgMTEyMDYsIFVTQSJQEk4KNAoyCReOha8HXMKJETjOQzBxX7M3Gh4LEO7B7qEBGhQKEgmJzguI-VvCiRFYR8sAAcN5KAwQQCoUChIJH0FG4AZcwokRvrvwkhWA_6A', - types: ['street_address'], - }; - const isValid = GooglePlacesUtils.isAddressValidForVBA(brooklynAddressResult.address_components); - expect(isValid).toStrictEqual(true); - }); - }); describe('getAddressComponent', () => { it('should find address components by type', () => { const addressComponents = [