Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
9 changes: 5 additions & 4 deletions src/components/AddPlaidBankAccount.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
clearPlaidBankAccountsAndToken,
fetchPlaidLinkToken,
getPlaidBankAccounts,
setBankAccountFormValidationErrors,
showBankAccountErrorModal,
} from '../libs/actions/BankAccounts';
import ONYXKEYS from '../ONYXKEYS';
Expand Down Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB, personally, I think it's fine to explicitly pass the props rather than implicitly bind to this. Perhaps it would be better to not spend time improving things we know we are going to soon replace and only change what we need to accomplish the task at hand.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, but I changed this because I started using this.state and this.setState in clearError. Didn't check this properly, but I thought that passing this.setState would already require a binding so I thought it was just better to bind clearError. (#6200 (comment))

}

componentDidMount() {
Expand All @@ -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;
}

Expand Down Expand Up @@ -180,6 +180,7 @@ class AddPlaidBankAccount extends React.Component {
{accounts.length > 0 && (
<ReimbursementAccountForm
onSubmit={this.selectAccount}
formErrors={this.getErrors()}
>
{!_.isEmpty(this.props.text) && (
<Text style={[styles.mb5]}>{this.props.text}</Text>
Expand Down
219 changes: 106 additions & 113 deletions src/components/AddressSearch.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -23,134 +23,127 @@ 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,
Copy link
Contributor

Choose a reason for hiding this comment

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

Having the ref was more intuitive than this and would recommend we go back to that if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In my opinion, I don't think it makes it more intuitive, but I'm ok with changing it. If I change it to ref, this would mean that this component should be a functional component again, right?

displayListViewBorder: false,
};
}

const saveLocationDetails = (details) => {
saveLocationDetails(details) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs a doc

const addressComponents = details.address_components;
if (isAddressValidForVBA(addressComponents)) {
if (addressComponents) {
Copy link
Contributor

Choose a reason for hiding this comment

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

if (!addressComponents) {
    return;
}

// 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));
Copy link
Contributor

Choose a reason for hiding this comment

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

We are trying to pass the key and value for all of these properties to props.onChange. What's the value of doing it this way?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The object passed to props.onChange will optionally have the following keys: addressCity, addressState, addressZipCode and addressStreet. The _pick(..., _.identity) will remove any of they keys that don't have a truthy value associated. Why? because if the clicked result is missing some part of the address (i.e. city), we are not going to overwrite what the user may have typed.

Now that you mentioned, the intention is not obvious by looking at the code.

I could add a comment to make it more clear, what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

Adding a comment would be very helpful in this case, I missed the overwriting part but that makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

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

NAB, maybe would be better without the underscore identity trick and check for each value

const values = {};
if (addressCity) {
    values.addressCity = addressCity;
}

// etc

if (_.size(values) === 0) {
    return;
}

this.props.onChange(values);

}
};

return (
<GooglePlacesAutocomplete
ref={googlePlacesRef}
fetchDetails
suppressDefaultStyles
enablePoweredByContainer={false}
onPress={(data, details) => {
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 (
<GooglePlacesAutocomplete
fetchDetails
Comment on lines +75 to +76
Copy link
Contributor

Choose a reason for hiding this comment

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

nab: my IDE is complaining about no placeholder required attribute set

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right, there is a mandatory prop "placeholder" in GooglePlacesAutocomplete (see here). We haven't been using it.

If I add placeholder="PLACEHOLDER", it looks like this:

Screen.Recording.2021-11-22.at.11.00.36.AM.mov

We could get rid of the error by just doing placeholder="" if we don't want to have a placeholder text.

Copy link
Contributor

Choose a reason for hiding this comment

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

Was going to suggest just doing placeholder="", I think that'd be best

suppressDefaultStyles
enablePoweredByContainer={false}
onPress={(data, details) => {
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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe just me, but I think we should either figure out why it happens or just say we are working around a feature of the library.

Copy link
Contributor

Choose a reason for hiding this comment

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

But ideally we don't do this at all and there is no explanation necessary 😄

Copy link
Contributor Author

@aldo-expensify aldo-expensify Nov 26, 2021

Choose a reason for hiding this comment

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

Yeah, I guess it is not the best comment. The point is, I know why it happens (like what code in the library is creating the problem), but I don't know the reason why they decided to do that (doesn't make sense to me).

we are working around a feature of the library.

This ^

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;
Expand Down
36 changes: 1 addition & 35 deletions src/libs/GooglePlacesUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Loading