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

Use sublocality when locality is missing - AddressSearch #5950

Merged
merged 12 commits into from
Oct 23, 2021
Merged
56 changes: 22 additions & 34 deletions src/components/AddressSearch.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import _ from 'underscore';
import React, {useEffect, useRef} from 'react';
import PropTypes from 'prop-types';
import {LogBox} from 'react-native';
Expand All @@ -7,6 +6,8 @@ import CONFIG from '../CONFIG';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import styles from '../styles/styles';
import ExpensiTextInput from './ExpensiTextInput';
import Log from '../libs/Log';
import {getAddressComponent, validateAddressComponents} 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 @@ -33,43 +34,25 @@ const defaultProps = {
containerStyles: null,
};

const AddressSearch = (props) => {
export const AddressSearchComponent = (props) => {
aldo-expensify marked this conversation as resolved.
Show resolved Hide resolved
const googlePlacesRef = useRef();
useEffect(() => {
googlePlacesRef.current?.setAddressText(props.value);
}, []);

// eslint-disable-next-line
const getAddressComponent = (object, field, nameType) => {
return _.chain(object.address_components)
.find(component => _.contains(component.types, field))
.get(nameType)
.value();
};

const validateAddressComponents = (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, 'post_box'))) {
// Reject PO box
return false;
}
return true;
};

const saveLocationDetails = (details) => {
if (validateAddressComponents(details.address_components)) {
const addressComponents = details.address_components;
if (validateAddressComponents(addressComponents)) {
// Gather the values from the Google details
const streetNumber = getAddressComponent(details, 'street_number', 'long_name');
const streetName = getAddressComponent(details, 'route', 'long_name');
const city = getAddressComponent(details, 'locality', 'long_name');
const state = getAddressComponent(details, 'administrative_area_level_1', 'short_name');
const zipCode = getAddressComponent(details, 'postal_code', 'long_name');
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('Replacing missing locality with sublocality: ', {address: details.formatted_address, sublocality: city});
aldo-expensify marked this conversation as resolved.
Show resolved Hide resolved
}
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}`);
Expand All @@ -78,6 +61,11 @@ const AddressSearch = (props) => {
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);
Expand Down Expand Up @@ -130,7 +118,7 @@ const AddressSearch = (props) => {
);
};

AddressSearch.propTypes = propTypes;
AddressSearch.defaultProps = defaultProps;
AddressSearchComponent.propTypes = propTypes;
AddressSearchComponent.defaultProps = defaultProps;

export default withLocalize(AddressSearch);
export default withLocalize(AddressSearchComponent);
36 changes: 36 additions & 0 deletions src/libs/GooglePlacesUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import _ from 'underscore';

export const getAddressComponent = (addressComponents, type, field) => (
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
_.chain(addressComponents)
.find(component => _.contains(component.types, type))
.get(field)
.value()
);

export const validateAddressComponents = (addressComponents) => {
aldo-expensify marked this conversation as resolved.
Show resolved Hide resolved
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;
};
142 changes: 142 additions & 0 deletions tests/unit/GooglePlacesUtilsTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {getAddressComponent, validateAddressComponents} from '../../src/libs/GooglePlacesUtils';

describe('GooglePlacesUtilsTest', () => {
describe('validateAddressComponents', () => {
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',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I left this here even if it not used in the tests just as a way to remember these addresses that gave up problems.

place_id: 'EihRdWFpbCBSaWRnZSBSZCwgRXNjb25kaWRvLCBDQSA5MjAyNywgVVNBIi4qLAoUChIJIQBiT7Pz24ARmaXMgCMhqAUSFAoSCXtDwoFe89uAEd_FlncPyNEB',
types: ['route'],
};
const isValid = validateAddressComponents(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 = validateAddressComponents(brooklynAddressResult.address_components);
expect(isValid).toStrictEqual(true);
});
});
describe('getAddressComponent', () => {
it('should find address components by type', () => {
const addressComponents = [
{
long_name: 'Bushwick',
short_name: 'Bushwick',
types: ['neighborhood', 'political'],
},
{
long_name: 'Brooklyn',
short_name: 'Brooklyn',
types: ['sublocality_level_1', 'sublocality', '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'],
},
];
expect(getAddressComponent(addressComponents, 'sublocality', 'long_name')).toStrictEqual('Brooklyn');
expect(getAddressComponent(addressComponents, 'administrative_area_level_1', 'short_name')).toStrictEqual('NY');
expect(getAddressComponent(addressComponents, 'postal_code', 'long_name')).toStrictEqual('11206');
expect(getAddressComponent(addressComponents, 'doesn-exist', 'long_name')).toStrictEqual(undefined);
});
});
});