diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index 8ce5a6cc4a1d6..5ef8b82f2a554 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -13,7 +13,6 @@ import { clearPlaidBankAccountsAndToken, fetchPlaidLinkToken, getPlaidBankAccounts, - setBankAccountFormValidationErrors, showBankAccountErrorModal, } from '../libs/actions/BankAccounts'; import ONYXKEYS from '../ONYXKEYS'; @@ -101,8 +100,9 @@ class AddPlaidBankAccount extends React.Component { institution: {}, }; - this.getErrors = () => ReimbursementAccountUtils.getErrors(this.props); - this.clearError = inputKey => ReimbursementAccountUtils.clearError(this.props, inputKey); + this.clearError = ReimbursementAccountUtils.clearError.bind(this); + this.getErrors = ReimbursementAccountUtils.getErrors.bind(this); + this.setErrors = ReimbursementAccountUtils.setErrors.bind(this); } componentDidMount() { @@ -127,7 +127,7 @@ class AddPlaidBankAccount extends React.Component { if (_.isUndefined(this.state.selectedIndex)) { errors.selectedBank = true; } - setBankAccountFormValidationErrors(errors); + this.setErrors(errors); return _.size(errors) === 0; } @@ -180,6 +180,7 @@ class AddPlaidBankAccount extends React.Component { {accounts.length > 0 && ( {!_.isEmpty(this.props.text) && ( {this.props.text} diff --git a/src/components/AddressSearch.js b/src/components/AddressSearch.js index 158e3829779b5..99ed83f4a9dce 100644 --- a/src/components/AddressSearch.js +++ b/src/components/AddressSearch.js @@ -1,5 +1,5 @@ +import React, {PureComponent} from 'react'; import _ from 'underscore'; -import React, {useEffect, useState, useRef} from 'react'; import PropTypes from 'prop-types'; import {LogBox} from 'react-native'; import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete'; @@ -8,7 +8,7 @@ import withLocalize, {withLocalizePropTypes} from './withLocalize'; import styles from '../styles/styles'; import ExpensiTextInput from './ExpensiTextInput'; import Log from '../libs/Log'; -import {getAddressComponent, isAddressValidForVBA} from '../libs/GooglePlacesUtils'; +import {getAddressComponent} from '../libs/GooglePlacesUtils'; // The error that's being thrown below will be ignored until we fork the // react-native-google-places-autocomplete repo and replace the @@ -23,134 +23,125 @@ 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), ...withLocalizePropTypes, }; + const defaultProps = { value: '', containerStyles: null, }; -// 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 -const AddressSearch = (props) => { - const googlePlacesRef = useRef(); - const [displayListViewBorder, setDisplayListViewBorder] = useState(false); - useEffect(() => { - if (!googlePlacesRef.current) { - return; - } +class AddressSearch extends PureComponent { + constructor(props) { + super(props); - googlePlacesRef.current.setAddressText(props.value); - }, []); + this.state = { + skippedFirstOnChangeText: false, + displayListViewBorder: false, + }; + } - const saveLocationDetails = (details) => { + saveLocationDetails(details) { const addressComponents = details.address_components; - if (isAddressValidForVBA(addressComponents)) { + if (addressComponents) { // Gather the values from the Google details - const streetNumber = getAddressComponent(addressComponents, 'street_number', 'long_name'); - const streetName = getAddressComponent(addressComponents, 'route', 'long_name'); - let city = getAddressComponent(addressComponents, 'locality', 'long_name'); - if (!city) { - city = getAddressComponent(addressComponents, 'sublocality', 'long_name'); - Log.hmmm('[AddressSearch] Replacing missing locality with sublocality: ', {address: details.formatted_address, sublocality: city}); + const streetNumber = getAddressComponent(addressComponents, 'street_number', 'long_name') || ''; + const streetName = getAddressComponent(addressComponents, 'route', 'long_name') || ''; + const addressStreet = `${streetNumber} ${streetName}`.trim(); + let addressCity = getAddressComponent(addressComponents, 'locality', 'long_name'); + if (!addressCity) { + addressCity = getAddressComponent(addressComponents, 'sublocality', 'long_name'); + Log.hmmm('[AddressSearch] Replacing missing locality with sublocality: ', {address: details.formatted_address, sublocality: addressCity}); } - const state = getAddressComponent(addressComponents, 'administrative_area_level_1', 'short_name'); - const zipCode = 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); + const addressZipCode = getAddressComponent(addressComponents, 'postal_code', 'long_name'); + const addressState = getAddressComponent(addressComponents, 'administrative_area_level_1', 'short_name'); + + this.props.onChange(_.pick({ + addressCity, + addressState, + addressZipCode, + addressStreet, + }, _.identity)); } - }; - - return ( - { - saveLocationDetails(details); - - // After we select an option, we set displayListViewBorder to false to prevent UI flickering - setDisplayListViewBorder(false); - }} - query={{ - key: 'AIzaSyC4axhhXtpiS-WozJEsmlL3Kg3kXucbZus', - language: props.preferredLocale, - types: 'address', - components: 'country:us', - }} - requestUrl={{ - useOnPlatform: 'web', - url: `${CONFIG.EXPENSIFY.URL_EXPENSIFY_COM}api?command=Proxy_GooglePlaces&proxyUrl=`, - }} - textInputProps={{ - InputComp: ExpensiTextInput, - label: props.label, - containerStyles: props.containerStyles, - errorText: props.errorText, - 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 the text is empty, we set displayListViewBorder to false to prevent UI flickering - if (_.isEmpty(text)) { - setDisplayListViewBorder(false); - } - }, - }} - styles={{ - textInputContainer: [styles.flexColumn], - listView: [ - !displayListViewBorder && styles.googleListView, - displayListViewBorder && styles.borderTopRounded, - displayListViewBorder && styles.borderBottomRounded, - displayListViewBorder && styles.mt1, - styles.overflowAuto, - styles.borderLeft, - styles.borderRight, - ], - row: [ - styles.pv4, - styles.ph3, - styles.overflowAuto, - ], - description: [styles.googleSearchText], - separator: [styles.googleSearchSeparator], - }} - 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. - // The height of the empty element is 2px (1px height for each top and bottom borders) - setDisplayListViewBorder(event.nativeEvent.layout.height > 2); - }} - /> - ); -}; + } + + render() { + return ( + { + this.saveLocationDetails(details); + + // After we select an option, we set displayListViewBorder to false to prevent UI flickering + this.setState({displayListViewBorder: false}); + }} + query={{ + key: 'AIzaSyC4axhhXtpiS-WozJEsmlL3Kg3kXucbZus', + language: this.props.preferredLocale, + types: 'address', + components: 'country:us', + }} + requestUrl={{ + useOnPlatform: 'web', + url: `${CONFIG.EXPENSIFY.URL_EXPENSIFY_COM}api?command=Proxy_GooglePlaces&proxyUrl=`, + }} + textInputProps={{ + InputComp: ExpensiTextInput, + label: this.props.label, + containerStyles: this.props.containerStyles, + errorText: this.props.errorText, + value: this.props.value, + onChangeText: (text) => { + // Line of code https://github.com/FaridSafi/react-native-google-places-autocomplete/blob/47d7223dd48f85da97e80a0729a985bbbcee353f/GooglePlacesAutocomplete.js#L148 + // will call onChangeText passing '' after the component renders the first time, clearing its value. Why? who knows, but we have to skip it. + if (this.state.skippedFirstOnChangeText) { + this.props.onChange({addressStreet: text}); + } else { + this.setState({skippedFirstOnChangeText: true}); + } + + // If the text is empty, we set displayListViewBorder to false to prevent UI flickering + if (_.isEmpty(text)) { + this.setState({displayListViewBorder: false}); + } + }, + }} + styles={{ + textInputContainer: [styles.flexColumn], + listView: [ + !this.state.displayListViewBorder && styles.googleListView, + this.state.displayListViewBorder && styles.borderTopRounded, + this.state.displayListViewBorder && styles.borderBottomRounded, + this.state.displayListViewBorder && styles.mt1, + styles.overflowAuto, + styles.borderLeft, + styles.borderRight, + ], + row: [ + styles.pv4, + styles.ph3, + styles.overflowAuto, + ], + description: [styles.googleSearchText], + separator: [styles.googleSearchSeparator], + }} + 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. + // The height of the empty element is 2px (1px height for each top and bottom borders) + this.setState({displayListViewBorder: event.nativeEvent.layout.height > 2}); + }} + /> + ); + } +} AddressSearch.propTypes = propTypes; AddressSearch.defaultProps = defaultProps; diff --git a/src/libs/GooglePlacesUtils.js b/src/libs/GooglePlacesUtils.js index 71c738062d112..5a875e7f102a4 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 e655da4936370..27efdfbc3d2d5 100644 --- a/src/libs/ReimbursementAccountUtils.js +++ b/src/libs/ReimbursementAccountUtils.js @@ -1,7 +1,6 @@ import lodashGet from 'lodash/get'; import lodashUnset from 'lodash/unset'; import lodashCloneDeep from 'lodash/cloneDeep'; -import {setBankAccountFormValidationErrors} from './actions/BankAccounts'; /** * Get the default state for input fields in the VBA flow @@ -18,39 +17,49 @@ function getDefaultStateForField(props, fieldName, defaultValue = '') { } /** - * @param {Object} props + * Use this function binding it to a component's instance * @returns {Object} */ -function getErrors(props) { - return lodashGet(props, ['reimbursementAccount', 'errors'], {}); +function getErrors() { + return this.state.errors || {}; } /** - * @param {Object} props + * Use this function binding it to a component's instance * @param {String} path */ -function clearError(props, path) { - const errors = getErrors(props); +function clearError(path) { + const errors = this.state.errors || {}; if (!lodashGet(errors, path, false)) { // No error found for this path return; } - // Clear the existing errors - const newErrors = lodashCloneDeep(errors); - lodashUnset(newErrors, path); - setBankAccountFormValidationErrors(newErrors); + this.setState((prevState) => { + // Clear the existing errors + const newErrors = lodashCloneDeep(prevState.errors); + lodashUnset(newErrors, path); + return {...prevState, errors: newErrors}; + }); } /** - * @param {Object} props + * Use this function binding it to a component's instance * @param {Object} errorTranslationKeys * @param {String} inputKey * @returns {String} */ -function getErrorText(props, errorTranslationKeys, inputKey) { - const errors = getErrors(props); - return errors[inputKey] ? props.translate(errorTranslationKeys[inputKey]) : ''; +function getErrorText(errorTranslationKeys, inputKey) { + const errors = this.state.errors || {}; + return errors[inputKey] ? this.props.translate(errorTranslationKeys[inputKey]) : ''; +} + +/** + * Use this function binding it to a component's instance + * @param {Object} errors + */ +function setErrors(errors) { + this.setState({errors}); } export { @@ -58,4 +67,5 @@ export { getErrors, clearError, getErrorText, + setErrors, }; diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js index e90d7b2658d64..1cdf003d07a48 100644 --- a/src/libs/ValidationUtils.js +++ b/src/libs/ValidationUtils.js @@ -206,7 +206,7 @@ function isValidURL(url) { * @returns {Object} */ function validateIdentity(identity) { - const requiredFields = ['firstName', 'lastName', 'street', 'city', 'zipCode', 'state', 'ssnLast4', 'dob']; + const requiredFields = ['firstName', 'lastName', 'addressStreet', 'addressCity', 'addressZipCode', 'addressState', 'ssnLast4', 'dob']; const errors = {}; // Check that all required fields are filled @@ -217,12 +217,12 @@ function validateIdentity(identity) { errors[fieldName] = true; }); - if (!isValidAddress(identity.street)) { - errors.street = true; + if (!isValidAddress(identity.addressStreet)) { + errors.addressStreet = true; } - if (!isValidZipCode(identity.zipCode)) { - errors.zipCode = true; + if (!isValidZipCode(identity.addressZipCode)) { + errors.addressZipCode = true; } // dob field has multiple validations/errors, we are handling it temporarily like this. diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 69b452cbab42a..22a6e80b30fed 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -617,17 +617,6 @@ function validateBankAccount(bankAccountID, validateCode) { }); } -/** - * Set the current fields with errors. - * - * @param {String} errors - */ -function setBankAccountFormValidationErrors(errors) { - // We set 'errors' to null first because we don't have a way yet to replace a specific property like 'errors' without merging it - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors: null}); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors}); -} - /** * Set the current error message. * @@ -653,7 +642,7 @@ function setBankAccountSubStep(subStep) { */ function setupWithdrawalAccount(data) { let nextStep; - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: true, errorModalMessage: '', errors: null}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: true, errorModalMessage: ''}); const newACHData = { ...reimbursementAccountInSetup, @@ -691,7 +680,6 @@ function setupWithdrawalAccount(data) { let achData = lodashGet(response, 'achData', {}); let error = lodashGet(achData, CONST.BANK_ACCOUNT.VERIFICATIONS.ERROR_MESSAGE); let isErrorHTML = false; - const errors = {}; if (response.jsonCode === 200 && !error) { // Save an NVP with the bankAccountID for this account. This is temporary since we are not showing lists @@ -781,7 +769,7 @@ function setupWithdrawalAccount(data) { if (response.message === CONST.BANK_ACCOUNT.ERROR.MISSING_ROUTING_NUMBER || response.message === CONST.BANK_ACCOUNT.ERROR.MAX_ROUTING_NUMBER ) { - errors.routingNumber = true; + error = translateLocal('bankAccount.error.routingNumber'); achData.subStep = CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL; } else if (response.message === CONST.BANK_ACCOUNT.ERROR.MISSING_INCORPORATION_STATE) { error = translateLocal('bankAccount.error.incorporationState'); @@ -796,10 +784,6 @@ function setupWithdrawalAccount(data) { // Go to next step goToWithdrawalAccountSetupStep(nextStep, achData); - if (_.size(errors)) { - setBankAccountFormValidationErrors(errors); - showBankAccountErrorModal(); - } if (error) { showBankAccountFormValidationError(error); showBankAccountErrorModal(error, isErrorHTML); @@ -814,7 +798,7 @@ function setupWithdrawalAccount(data) { } function hideBankAccountErrors() { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {error: '', errors: null}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {error: ''}); } function setWorkspaceIDForReimbursementAccount(workspaceID) { @@ -921,7 +905,6 @@ export { hideBankAccountErrors, showBankAccountErrorModal, showBankAccountFormValidationError, - setBankAccountFormValidationErrors, setWorkspaceIDForReimbursementAccount, setBankAccountSubStep, updateReimbursementAccountDraft, diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js index 01acd421f9d36..e4b05218a5679 100644 --- a/src/pages/ReimbursementAccount/BankAccountStep.js +++ b/src/pages/ReimbursementAccount/BankAccountStep.js @@ -20,7 +20,6 @@ import exampleCheckImage from './exampleCheckImage'; import Text from '../../components/Text'; import ExpensiTextInput from '../../components/ExpensiTextInput'; import { - setBankAccountFormValidationErrors, setBankAccountSubStep, setupWithdrawalAccount, showBankAccountErrorModal, @@ -63,9 +62,10 @@ class BankAccountStep extends React.Component { accountNumber: 'bankAccount.error.accountNumber', }; - this.getErrorText = inputKey => ReimbursementAccountUtils.getErrorText(this.props, this.errorTranslationKeys, inputKey); - this.clearError = inputKey => ReimbursementAccountUtils.clearError(this.props, inputKey); - this.getErrors = () => ReimbursementAccountUtils.getErrors(this.props); + this.clearError = ReimbursementAccountUtils.clearError.bind(this); + this.setErrors = ReimbursementAccountUtils.setErrors.bind(this); + this.getErrors = ReimbursementAccountUtils.getErrors.bind(this); + this.getErrorText = ReimbursementAccountUtils.getErrorText.bind(this, this.errorTranslationKeys); } toggleTerms() { @@ -94,7 +94,7 @@ class BankAccountStep extends React.Component { errors.hasAcceptedTerms = true; } - setBankAccountFormValidationErrors(errors); + this.setErrors(errors); return _.size(errors) === 0; } @@ -252,6 +252,7 @@ class BankAccountStep extends React.Component { {subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL && ( {this.props.translate('bankAccount.checkHelpLine')} diff --git a/src/pages/ReimbursementAccount/BeneficialOwnersStep.js b/src/pages/ReimbursementAccount/BeneficialOwnersStep.js index 3319f92d1717b..087b6c7df2984 100644 --- a/src/pages/ReimbursementAccount/BeneficialOwnersStep.js +++ b/src/pages/ReimbursementAccount/BeneficialOwnersStep.js @@ -13,7 +13,6 @@ import IdentityForm from './IdentityForm'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import { goToWithdrawalAccountSetupStep, - setBankAccountFormValidationErrors, setupWithdrawalAccount, updateReimbursementAccountDraft, } from '../../libs/actions/BankAccounts'; @@ -26,6 +25,8 @@ import { getDefaultStateForField, clearError, getErrorText, + getErrors, + setErrors, } from '../../libs/ReimbursementAccountUtils'; import reimbursementAccountPropTypes from './reimbursementAccountPropTypes'; import ReimbursementAccountForm from './ReimbursementAccountForm'; @@ -68,15 +69,10 @@ class BeneficialOwnersStep extends React.Component { certifyTrueInformation: 'beneficialOwnersStep.error.certify', }; - this.clearError = inputKey => clearError(this.props, inputKey); - this.getErrorText = inputKey => getErrorText(this.props, this.errorTranslationKeys, inputKey); - } - - /** - * @returns {Object} - */ - getErrors() { - return lodashGet(this.props, ['reimbursementAccount', 'errors'], {}); + this.clearError = clearError.bind(this); + this.setErrors = setErrors.bind(this); + this.getErrors = getErrors.bind(this); + this.getErrorText = getErrorText.bind(this, this.errorTranslationKeys); } /** @@ -85,7 +81,16 @@ class BeneficialOwnersStep extends React.Component { validate() { let beneficialOwnersErrors = []; if (this.state.hasOtherBeneficialOwners) { - beneficialOwnersErrors = _.map(this.state.beneficialOwners, validateIdentity); + beneficialOwnersErrors = _.map(this.state.beneficialOwners, beneficialOwner => validateIdentity({ + firstName: beneficialOwner.firstName, + lastName: beneficialOwner.lastName, + addressStreet: beneficialOwner.street, + addressState: beneficialOwner.state, + addressCity: beneficialOwner.city, + addressZipCode: beneficialOwner.zipCode, + dob: beneficialOwner.dob, + ssnLast4: beneficialOwner.ssnLast4, + })); } const errors = {}; @@ -96,7 +101,7 @@ class BeneficialOwnersStep extends React.Component { errors[inputKey] = true; }); - setBankAccountFormValidationErrors({...errors, beneficialOwnersErrors}); + this.setErrors({...errors, beneficialOwnersErrors}); return _.every(beneficialOwnersErrors, _.isEmpty) && _.isEmpty(errors); } @@ -109,9 +114,7 @@ class BeneficialOwnersStep extends React.Component { updateReimbursementAccountDraft({beneficialOwners: null}); updateReimbursementAccountDraft({beneficialOwners}); - // Clear errors - setBankAccountFormValidationErrors({}); - return {beneficialOwners}; + return {beneficialOwners, errors: {}}; }); } @@ -195,6 +198,7 @@ class BeneficialOwnersStep extends React.Component { /> {this.props.translate('beneficialOwnersStep.checkAllThatApply')} @@ -246,10 +250,10 @@ class BeneficialOwnersStep extends React.Component { values={{ firstName: owner.firstName || '', lastName: owner.lastName || '', - street: owner.street || '', - city: owner.city || '', - state: owner.state || '', - zipCode: owner.zipCode || '', + addressStreet: owner.street || '', + addressCity: owner.city || '', + addressState: owner.state || '', + addressZipCode: owner.zipCode || '', dob: owner.dob || '', ssnLast4: owner.ssnLast4 || '', }} diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index ac0ee7020c43a..6c6a0ddb0052b 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -11,7 +11,6 @@ import { goToWithdrawalAccountSetupStep, setupWithdrawalAccount, showBankAccountErrorModal, - setBankAccountFormValidationErrors, updateReimbursementAccountDraft, } from '../../libs/actions/BankAccounts'; import Navigation from '../../libs/Navigation/Navigation'; @@ -98,30 +97,13 @@ class CompanyStep extends React.Component { incorporationType: 'bankAccount.error.companyType', hasNoConnectionToCannabis: 'bankAccount.error.restrictedBusiness', }; - - this.getErrorText = inputKey => ReimbursementAccountUtils.getErrorText(this.props, this.errorTranslationKeys, inputKey); - this.clearError = inputKey => ReimbursementAccountUtils.clearError(this.props, inputKey); - this.getErrors = () => ReimbursementAccountUtils.getErrors(this.props); + this.clearError = ReimbursementAccountUtils.clearError.bind(this); + this.getErrors = ReimbursementAccountUtils.getErrors.bind(this); + this.setErrors = ReimbursementAccountUtils.setErrors.bind(this); + this.getErrorText = ReimbursementAccountUtils.getErrorText.bind(this, this.errorTranslationKeys); 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 */ @@ -158,14 +140,12 @@ class CompanyStep extends React.Component { validate() { const errors = {}; - if (this.state.manualAddress) { - if (!isValidAddress(this.state.addressStreet)) { - errors.addressStreet = true; - } + if (!isValidAddress(this.state.addressStreet)) { + errors.addressStreet = true; + } - if (!isValidZipCode(this.state.addressZipCode)) { - errors.addressZipCode = true; - } + if (!isValidZipCode(this.state.addressZipCode)) { + errors.addressZipCode = true; } if (!isValidURL(this.state.website)) { @@ -195,7 +175,7 @@ class CompanyStep extends React.Component { errors[inputKey] = true; }); - setBankAccountFormValidationErrors(errors); + this.setErrors(errors); return _.size(errors) === 0; } @@ -224,6 +204,7 @@ class CompanyStep extends React.Component { /> {this.props.translate('companyStep.subtitle')} - {!this.state.manualAddress && ( - <> - this.clearErrorAndSetValue(fieldName, value)} - errorText={this.getErrorText('addressStreet')} - /> - this.setState({manualAddress: true})} - > - Can't find your address? Enter it manually - - - )} - {this.state.manualAddress && ( - <> + { + _.each(values, (value, key) => { + this.clearErrorAndSetValue(key, value); + }); + }} + errorText={this.getErrorText('addressStreet')} + /> + + this.clearErrorAndSetValue('addressStreet', value)} - value={this.state.addressStreet} - errorText={this.getErrorText('addressStreet')} + label={this.props.translate('common.city')} + onChangeText={value => this.clearErrorAndSetValue('addressCity', value)} + value={this.state.addressCity} + errorText={this.getErrorText('addressCity')} + translateX={-14} /> - {this.props.translate('common.noPO')} - - - this.clearErrorAndSetValue('addressCity', value)} - value={this.state.addressCity} - errorText={this.getErrorText('addressCity')} - translateX={-14} - /> - - - this.clearErrorAndSetValue('addressState', value)} - value={this.state.addressState} - hasError={this.getErrors().addressState} - /> - - - this.clearErrorAndSetValue('addressZipCode', value)} - value={this.state.addressZipCode} - errorText={this.getErrorText('addressZipCode')} + + + 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 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 ( @@ -133,62 +115,44 @@ const IdentityForm = (props) => { 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') : ''} - translateX={-14} - /> - - - props.onFieldChange('addressState', value)} - errorText={props.errors.state ? props.translate('bankAccount.error.addressState') : ''} - hasError={Boolean(props.errors.state)} - /> - - + { + _.each(addressValues, (value, key) => { + props.onFieldChange(key, value); + }); + }} + errorText={props.errors.addressStreet ? props.translate('bankAccount.error.address') : ''} + /> + + props.onFieldChange('addressZipCode', value)} - errorText={props.errors.zipCode ? props.translate('bankAccount.error.zipCode') : ''} - maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} + label={props.translate('common.city')} + value={props.values.addressCity} + onChangeText={value => props.onFieldChange('addressCity', value)} + errorText={props.errors.addressCity ? props.translate('bankAccount.error.addressCity') : ''} + translateX={-14} /> - - ) : ( - <> - props.onFieldChange(fieldName, value)} - errorText={props.errors.street ? props.translate('bankAccount.error.addressStreet') : ''} + + + props.onFieldChange('addressState', value)} + hasError={Boolean(props.errors.addressState)} /> - props.onFieldChange('manualAddress', true)} - > - Can't find your address? Enter it manually - - - )} + + + props.onFieldChange('addressZipCode', value)} + errorText={props.errors.addressZipCode ? props.translate('bankAccount.error.zipCode') : ''} + maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} + /> ); }; diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js index e31669167471d..3a08f833cb8ba 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js @@ -20,27 +20,46 @@ const propTypes = { /** Called when the form is submitted */ onSubmit: PropTypes.func.isRequired, + /** Object containing various errors */ + // eslint-disable-next-line react/no-unused-prop-types + formErrors: PropTypes.objectOf(PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.arrayOf(PropTypes.objectOf(PropTypes.bool)), + ])), + ...withLocalizePropTypes, }; const defaultProps = { reimbursementAccount: {}, + formErrors: {}, }; class ReimbursementAccountForm extends React.Component { - render() { - const isErrorVisible = _.size(lodashGet(this.props, 'reimbursementAccount.errors', {})) > 0 - || lodashGet(this.props, 'reimbursementAccount.errorModalMessage', '').length > 0 + /** + * Checks if we have errors or not + * + * @returns {Boolean} + */ + isErrorVisible() { + if (lodashGet(this.props, 'reimbursementAccount.errorModalMessage', '').length > 0 // @TODO once all validation errors show in multiples we can remove this check - || lodashGet(this.props, 'reimbursementAccount.error', '').length > 0; + || lodashGet(this.props, 'reimbursementAccount.error', '').length > 0) { + return true; + } + + // Check considering that a key may have an array of objects (i.e. beneficial owners) + const formErrors = lodashGet(this.props, 'formErrors', {}); + return _.any(formErrors, value => value === true || (_.isArray(value) && _.any(value, _.size))); + } + render() { const currentStep = lodashGet( this.props, 'reimbursementAccount.achData.currentStep', CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT, ); - return ( { diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js index 32fa9c74c9f03..7605836115b0f 100644 --- a/src/pages/ReimbursementAccount/RequestorStep.js +++ b/src/pages/ReimbursementAccount/RequestorStep.js @@ -15,12 +15,11 @@ import Text from '../../components/Text'; import { showBankAccountErrorModal, goToWithdrawalAccountSetupStep, - setBankAccountFormValidationErrors, setupWithdrawalAccount, updateReimbursementAccountDraft, } from '../../libs/actions/BankAccounts'; import IdentityForm from './IdentityForm'; -import {isRequiredFulfilled, validateIdentity} from '../../libs/ValidationUtils'; +import {validateIdentity} from '../../libs/ValidationUtils'; import Onfido from '../../components/Onfido'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; @@ -28,17 +27,14 @@ import { getDefaultStateForField, clearError, getErrors, + setErrors, } from '../../libs/ReimbursementAccountUtils'; import Log from '../../libs/Log'; import Growl from '../../libs/Growl'; -import reimbursementAccountPropTypes from './reimbursementAccountPropTypes'; import ReimbursementAccountForm from './ReimbursementAccountForm'; import {openExternalLink} from '../../libs/actions/Link'; const propTypes = { - /** Bank account currently in setup */ - reimbursementAccount: reimbursementAccountPropTypes.isRequired, - ...withLocalizePropTypes, }; @@ -63,22 +59,9 @@ class RequestorStep extends React.Component { isOnfidoSetupComplete: lodashGet(props, ['achData', 'isOnfidoSetupComplete'], false), }; - // Required fields not validated by `validateIdentity` - this.requiredFields = [ - 'firstName', - 'lastName', - 'isControllingOfficer', - ]; - - // Map a field to the key of the error's translation - this.errorTranslationKeys = { - firstName: 'bankAccount.error.firstName', - lastName: 'bankAccount.error.lastName', - isControllingOfficer: 'requestorStep.isControllingOfficerError', - }; - - this.clearError = inputKey => clearError(this.props, inputKey); - this.getErrors = () => getErrors(this.props); + this.clearError = clearError.bind(this); + this.setErrors = setErrors.bind(this); + this.getErrors = getErrors.bind(this); } /** @@ -88,28 +71,22 @@ class RequestorStep extends React.Component { * @param {String|Boolean} value */ clearErrorAndSetValue(inputKey, value) { - if (inputKey === 'manualAddress') { - this.setState({ - manualAddress: value, - }); - } else { - const renamedFields = { - addressStreet: 'requestorAddressStreet', - addressCity: 'requestorAddressCity', - addressState: 'requestorAddressState', - addressZipCode: 'requestorAddressZipCode', - }; - const renamedInputKey = lodashGet(renamedFields, inputKey, inputKey); - const newState = {[renamedInputKey]: value}; - this.setState(newState); - updateReimbursementAccountDraft(newState); + const renamedFields = { + addressStreet: 'requestorAddressStreet', + addressCity: 'requestorAddressCity', + addressState: 'requestorAddressState', + addressZipCode: 'requestorAddressZipCode', + }; + const renamedInputKey = lodashGet(renamedFields, inputKey, inputKey); + const newState = {[renamedInputKey]: value}; + this.setState(newState); + updateReimbursementAccountDraft(newState); - // dob field has multiple validations/errors, we are handling it temporarily like this. - if (inputKey === 'dob') { - this.clearError('dobAge'); - } - this.clearError(inputKey); + // dob field has multiple validations/errors, we are handling it temporarily like this. + if (inputKey === 'dob') { + this.clearError('dobAge'); } + this.clearError(inputKey); } /** @@ -119,23 +96,18 @@ class RequestorStep extends React.Component { const errors = validateIdentity({ firstName: this.state.firstName, lastName: this.state.lastName, - street: this.state.requestorAddressStreet, - state: this.state.requestorAddressState, - city: this.state.requestorAddressCity, - zipCode: this.state.requestorAddressZipCode, + addressStreet: this.state.requestorAddressStreet, + addressState: this.state.requestorAddressState, + addressCity: this.state.requestorAddressCity, + addressZipCode: this.state.requestorAddressZipCode, dob: this.state.dob, ssnLast4: this.state.ssnLast4, }); - - _.each(this.requiredFields, (inputKey) => { - if (isRequiredFulfilled(this.state[inputKey])) { - return; - } - - errors[inputKey] = true; - }); + if (!this.state.isControllingOfficer) { + errors.isControllingOfficer = true; + } if (_.size(errors)) { - setBankAccountFormValidationErrors(errors); + this.setErrors(errors); showBankAccountErrorModal(); return false; } @@ -188,6 +160,7 @@ class RequestorStep extends React.Component { ) : ( {this.props.translate('requestorStep.subtitle')} @@ -212,15 +185,14 @@ class RequestorStep extends React.Component { values={{ firstName: this.state.firstName, lastName: this.state.lastName, - street: this.state.requestorAddressStreet, - city: this.state.requestorAddressCity, - state: this.state.requestorAddressState, - zipCode: this.state.requestorAddressZipCode, + addressStreet: this.state.requestorAddressStreet, + addressCity: this.state.requestorAddressCity, + addressState: this.state.requestorAddressState, + addressZipCode: this.state.requestorAddressZipCode, dob: this.state.dob, ssnLast4: this.state.ssnLast4, - manualAddress: this.state.manualAddress, }} - errors={this.props.reimbursementAccount.errors} + errors={this.getErrors()} /> ReimbursementAccountUtils.getErrors(this.props); - this.getErrorText = inputKey => ReimbursementAccountUtils.getErrorText(this.props, this.errorTranslationKeys, inputKey); - this.clearError = inputKey => ReimbursementAccountUtils.clearError(this.props, inputKey); + this.clearError = ReimbursementAccountUtils.clearError.bind(this); + this.setErrors = ReimbursementAccountUtils.setErrors.bind(this); + this.getErrors = ReimbursementAccountUtils.getErrors.bind(this); + this.getErrorText = ReimbursementAccountUtils.getErrorText.bind(this, this.errorTranslationKeys); } /** @@ -107,7 +107,7 @@ class ValidationStep extends React.Component { errors[inputKey] = true; }); - setBankAccountFormValidationErrors(errors); + this.setErrors(errors); return _.size(errors) === 0; } @@ -189,6 +189,7 @@ class ValidationStep extends React.Component { {!maxAttemptsReached && state === BankAccount.STATE.PENDING && ( diff --git a/src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js b/src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js index a620be3412763..c765360740c58 100644 --- a/src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js +++ b/src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js @@ -29,10 +29,4 @@ export default PropTypes.shape({ /** Error set when handling the API response */ error: PropTypes.string, - - /** Object containing various errors */ - errors: PropTypes.objectOf(PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.arrayOf(PropTypes.objectOf(PropTypes.bool)), - ])), }); diff --git a/tests/unit/GooglePlacesUtilsTest.js b/tests/unit/GooglePlacesUtilsTest.js index 2ec7445623d13..274aec7cad6d6 100644 --- a/tests/unit/GooglePlacesUtilsTest.js +++ b/tests/unit/GooglePlacesUtilsTest.js @@ -1,109 +1,6 @@ -import {getAddressComponent, isAddressValidForVBA} from '../../src/libs/GooglePlacesUtils'; +import {getAddressComponent} 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 = 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 = isAddressValidForVBA(brooklynAddressResult.address_components); - expect(isValid).toStrictEqual(true); - }); - }); describe('getAddressComponent', () => { it('should find address components by type', () => { const addressComponents = [