Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Address search - show all inputs #6569

Merged
merged 24 commits into from
Dec 23, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2865a78
Add clearErrors to clear multiple errors on one call
aldo-expensify Dec 2, 2021
8f8d148
Replace onChangeText with onChange.
aldo-expensify Dec 2, 2021
32a9eb9
Update CompanyStep with new AddressSearch
aldo-expensify Dec 2, 2021
91920d6
Add two-way binding for AddressSearch text input
aldo-expensify Dec 2, 2021
bf63468
Update requestor step with new address fields
aldo-expensify Dec 2, 2021
0ea709a
Update ACHContract step handling of IdentityForm
aldo-expensify Dec 2, 2021
9295c47
Update AddDebitCardPage with new AddressSearch
aldo-expensify Dec 2, 2021
27b0d84
Remove unused validation
aldo-expensify Dec 2, 2021
3f73b08
Don't replace street if user typed something longer
aldo-expensify Dec 2, 2021
5adf0c2
Update variable name
aldo-expensify Dec 2, 2021
7d44cd8
Fix CompanyStep not clearing errors
aldo-expensify Dec 3, 2021
d7ac198
Resolve conclifts
aldo-expensify Dec 3, 2021
93642a8
Resolve conflicts
aldo-expensify Dec 3, 2021
0865523
Move comment near state declaration
aldo-expensify Dec 3, 2021
b0fe744
Merge branch 'main' of github.com:Expensify/App into aldo_vba-address…
aldo-expensify Dec 3, 2021
f8377ad
Merge branch 'main' of github.com:Expensify/App into aldo_vba-address…
aldo-expensify Dec 23, 2021
aed7b8c
Use ref instead of state to skip first onChangeText('')
aldo-expensify Dec 23, 2021
e7ff88a
Remove unnecesary new line
aldo-expensify Dec 23, 2021
fd5316e
Remove unnecesary empty placeholder
aldo-expensify Dec 23, 2021
3fd626a
Remove "address" prefix from address related input
aldo-expensify Dec 23, 2021
c30f08a
IdentityForm calls onChange always passing an object
aldo-expensify Dec 23, 2021
ec758cc
Cleanup contract step
aldo-expensify Dec 23, 2021
8466121
Remove unused function
aldo-expensify Dec 23, 2021
3cddfcf
Refactored address related fields into new AddressForm component
aldo-expensify Dec 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 43 additions & 43 deletions src/components/AddressSearch.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import _ from 'underscore';
import React, {useEffect, useState, useRef} from 'react';
import React, {useState} from 'react';
import PropTypes from 'prop-types';
import {LogBox} from 'react-native';
import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete';
Expand All @@ -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),
Expand All @@ -39,55 +39,52 @@ 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);
}, []);
const [skippedFirstOnChangeText, setSkippedFirstOnChangeText] = useState(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not saying this is likely but example of how this could go wrong in the future...

  • Someone updates this library and the bug gets fixed
  • They are now stuck having to figure out why the first onChangeText never fires
  • Maybe they find this code eventually 😂

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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');
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 addressStreet = `${streetNumber} ${streetName}`.trim();
let addressCity = GooglePlacesUtils.getAddressComponent(addressComponents, 'locality', 'long_name');
if (!addressCity) {
addressCity = GooglePlacesUtils.getAddressComponent(addressComponents, 'sublocality', 'long_name');
Log.hmmm('[AddressSearch] Replacing missing locality with sublocality: ', {address: details.formatted_address, sublocality: addressCity});
}
const addressZipCode = GooglePlacesUtils.getAddressComponent(addressComponents, 'postal_code', 'long_name');
const addressState = GooglePlacesUtils.getAddressComponent(addressComponents, 'administrative_area_level_1', 'short_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 values = {};
if (addressStreet) {
values.addressStreet = addressStreet;
}
if (addressCity) {
values.addressCity = addressCity;
}
if (addressZipCode) {
values.addressZipCode = addressZipCode;
}
if (addressState) {
values.addressState = addressState;
}
if (_.size(values) === 0) {
return;
}
props.onChange(values);
};

return (
<GooglePlacesAutocomplete
ref={googlePlacesRef}
fetchDetails
suppressDefaultStyles
enablePoweredByContainer={false}
placeholder=""
aldo-expensify marked this conversation as resolved.
Show resolved Hide resolved
onPress={(data, details) => {
saveLocationDetails(details);

Expand All @@ -109,12 +106,15 @@ const AddressSearch = (props) => {
label: props.label,
containerStyles: props.containerStyles,
errorText: props.errorText,
value: props.value,
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
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({});
// We use `skippedFirstOnChangeText` 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
aldo-expensify marked this conversation as resolved.
Show resolved Hide resolved
if (skippedFirstOnChangeText) {
props.onChange({addressStreet: text});
} else {
setSkippedFirstOnChangeText(true);
aldo-expensify marked this conversation as resolved.
Show resolved Hide resolved
}

// If the text is empty, we set displayListViewBorder to false to prevent UI flickering
Expand Down
20 changes: 16 additions & 4 deletions src/libs/ReimbursementAccountUtils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import _ from 'underscore';
import lodashGet from 'lodash/get';
import lodashUnset from 'lodash/unset';
import lodashCloneDeep from 'lodash/cloneDeep';
Expand Down Expand Up @@ -25,23 +26,33 @@ function getErrors(props) {
return lodashGet(props, ['reimbursementAccount', 'errors'], {});
}


aldo-expensify marked this conversation as resolved.
Show resolved Hide resolved
/**
* @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
Expand All @@ -57,5 +68,6 @@ export {
getDefaultStateForField,
getErrors,
clearError,
clearErrors,
getErrorText,
};
10 changes: 5 additions & 5 deletions src/libs/ValidationUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is because I changed these names in the IdentityForm.js for consistency, see lines:

value={props.values.state}
onChange={value => props.onFieldChange('addressState', value)}

IdentityForm is reading values using one key (i.e. state) and then calling onChangeText with a different key (i.e. addressState). This just makes it harder to follow and update the logic where we already have some key renaming (i.e. requestor step, beneficial owners).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just makes it harder to follow and update the logic where we already have some key renaming (i.e. requestor step, beneficial owners).

Unsure what is hard to follow or update, but won't we have to "rename" again if something else decides to use this method which is meant to be generic? Could be better to just leave it generic and then pass in fields we want to validate regardless of what they are named.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, looking at it again, this function could really stay as it is. The thing that was confusing is the IdentityForm reading an input with one key and then calling "onFieldChange" with another key.

const errors = {};

// Check that all required fields are filled
Expand All @@ -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.
Expand Down
67 changes: 46 additions & 21 deletions src/pages/ReimbursementAccount/ACHContractStep.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,28 @@ 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 = values => ReimbursementAccountUtils.clearErrors(this.props, values);
this.getErrorText = inputKey => ReimbursementAccountUtils.getErrorText(this.props, this.errorTranslationKeys, inputKey);
}

/**
* @returns {Object}
*/
getErrors() {
return lodashGet(this.props, ['reimbursementAccount', 'errors'], {});
}

/**
* @returns {Boolean}
*/
validate() {
let beneficialOwnersErrors = [];
if (this.state.hasOtherBeneficialOwners) {
beneficialOwnersErrors = _.map(this.state.beneficialOwners, ValidationUtils.validateIdentity);
beneficialOwnersErrors = _.map(this.state.beneficialOwners, beneficialOwner => ValidationUtils.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,
aldo-expensify marked this conversation as resolved.
Show resolved Hide resolved
}));
}

const errors = {};
Expand Down Expand Up @@ -122,29 +126,44 @@ 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};
_.each(values, (value, inputKey) => {
const renamedInputKey = lodashGet(renamedFields, inputKey, inputKey);
beneficialOwners[ownerIndex] = {...beneficialOwners[ownerIndex], [renamedInputKey]: value};
});
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}`));
}

/**
* 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
*/
clearErrorAndSetBeneficialOwnerValue(ownerIndex, inputKey, value) {
this.clearErrorAndSetBeneficialOwnerValues(ownerIndex, {[inputKey]: value});
}

submit() {
Expand Down Expand Up @@ -233,14 +252,20 @@ class ACHContractStep extends React.Component {
</Text>
<IdentityForm
style={[styles.mb2]}
onFieldChange={(inputKey, value) => this.clearErrorAndSetBeneficialOwnerValue(index, inputKey, value)}
onFieldChange={(inputKeyOrValues, value) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having onFieldChange return one of two possible things is a bit strange.

What if we just return an object and have the validation and error clearing handled all at the same time for the various fields?

Sorry to sound like a broken record, but the way we validate these will be changing soon in pretty drastic ways so maybe we should not spend much time perfecting this.

Copy link
Contributor Author

@aldo-expensify aldo-expensify Dec 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having onFieldChange return one of two possible things is a bit strange.

Agreed, didn't liked it, but it was what I came up with to solve the clearing multiple errors in a batch.

What if we just return an object and have the validation and error clearing handled all at the same time for the various fields?

the idea is to ALWAYS pass an object to the onFieldChange callback, even if we are just changing a single field like firstName, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: onFieldChange gets called with an object every time.

if (_.isString(inputKeyOrValues)) {
this.clearErrorAndSetBeneficialOwnerValue(index, inputKeyOrValues, value);
} else {
this.clearErrorAndSetBeneficialOwnerValues(index, inputKeyOrValues);
}
}}
values={{
firstName: owner.firstName || '',
lastName: owner.lastName || '',
street: owner.street || '',
city: owner.city || '',
state: owner.state || '',
zipCode: owner.zipCode || '',
addressStreet: owner.street || '',
addressState: owner.state || '',
addressCity: owner.city || '',
addressZipCode: owner.zipCode || '',
aldo-expensify marked this conversation as resolved.
Show resolved Hide resolved
dob: owner.dob || '',
ssnLast4: owner.ssnLast4 || '',
}}
Expand Down
Loading