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
47 changes: 18 additions & 29 deletions src/components/AddressSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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, isAddressValidForVBA} 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 Down Expand Up @@ -43,37 +45,19 @@ const AddressSearch = (props) => {
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 (isAddressValidForVBA(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('[AddressSearch] Replacing missing locality with sublocality: ', {address: details.formatted_address, sublocality: city});
}
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 @@ -82,6 +66,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
61 changes: 61 additions & 0 deletions src/libs/GooglePlacesUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import _ from 'underscore';

/**
* Finds an address component by type, and returns the value associated to key. Each address component object
* inside the addressComponents array has the following structure:
* {
* long_name: "New York",
* short_name: "New York",
* types: [ "locality", "political" ]
* }
*
* @param {Array} addressComponents
* @param {String} type
* @param {String} key
* @returns {String|undefined}
*/
function getAddressComponent(addressComponents, type, key) {
return _.chain(addressComponents)
.find(component => _.contains(component.types, type))
.get(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 {
getAddressComponent,
isAddressValidForVBA,
};
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, isAddressValidForVBA} 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',
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 = 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 = [
{
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);
});
});
});