diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 97f9eb948262..45a2e6b73bea 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -21,6 +21,7 @@ const includeModules = [ 'react-native-onyx', 'react-native-gesture-handler', 'react-native-flipper', + 'react-native-google-places-autocomplete', ].join('|'); const webpackConfig = { diff --git a/package-lock.json b/package-lock.json index 47109b2c1148..b26a8a933cc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32512,8 +32512,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, "lodash.isequal": { "version": "4.5.0", @@ -38525,6 +38524,23 @@ } } }, + "react-native-google-places-autocomplete": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-native-google-places-autocomplete/-/react-native-google-places-autocomplete-2.4.1.tgz", + "integrity": "sha512-NJrzZ5zsguhTqe0C5tIW9PfxOn2wkWDiGYIBFksHzFOIIURxFPUlO0cJmfOjs5CBIDtMampgNXBdgADExBen5w==", + "requires": { + "lodash.debounce": "^4.0.8", + "prop-types": "^15.7.2", + "qs": "~6.9.1" + }, + "dependencies": { + "qs": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==" + } + } + }, "react-native-image-pan-zoom": { "version": "2.1.12", "resolved": "https://registry.npmjs.org/react-native-image-pan-zoom/-/react-native-image-pan-zoom-2.1.12.tgz", diff --git a/package.json b/package.json index bb86f771c84e..f5c989e21ef3 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "react-native-config": "^1.4.0", "react-native-document-picker": "^5.1.0", "react-native-gesture-handler": "1.9.0", + "react-native-google-places-autocomplete": "^2.4.1", "react-native-image-pan-zoom": "^2.1.12", "react-native-image-picker": "^4.0.3", "react-native-keyboard-spacer": "^0.4.1", diff --git a/src/components/AddressSearch.js b/src/components/AddressSearch.js new file mode 100644 index 000000000000..3769cd2c0cf1 --- /dev/null +++ b/src/components/AddressSearch.js @@ -0,0 +1,121 @@ +import _ from 'underscore'; +import React from 'react'; +import PropTypes from 'prop-types'; +import {LogBox} from 'react-native'; +import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete'; +import CONFIG from '../CONFIG'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import styles from '../styles/styles'; +import ExpensiTextInput from './ExpensiTextInput'; + +// The error that's being thrown below will be ignored until we fork the +// react-native-google-places-autocomplete repo and replace the +// VirtualizedList component with a VirtualizedList-backed instead +LogBox.ignoreLogs(['VirtualizedLists should never be nested']); + +const propTypes = { + /** The label to display for the field */ + label: PropTypes.string.isRequired, + + /** The value to set the field to initially */ + value: PropTypes.string, + + /** A callback function when the value of this field has changed */ + onChangeText: PropTypes.func.isRequired, + + /** Customize the ExpensiTextInput container */ + containerStyles: PropTypes.arrayOf(PropTypes.object), + + ...withLocalizePropTypes, +}; +const defaultProps = { + value: '', + containerStyles: null, +}; + +class AddressSearch extends React.Component { + constructor(props) { + super(props); + this.googlePlacesRef = React.createRef(); + } + + componentDidMount() { + this.googlePlacesRef.current?.setAddressText(this.props.value); + } + + getAddressComponent(object, field, nameType) { + return _.chain(object.address_components) + .find(component => _.contains(component.types, field)) + .get(nameType) + .value(); + } + + /** + * @param {Object} details See https://developers.google.com/maps/documentation/places/web-service/details#PlaceDetailsResponses + */ + saveLocationDetails = (details) => { + if (details.address_components) { + // Gather the values from the Google details + const streetNumber = this.getAddressComponent(details, 'street_number', 'long_name'); + const streetName = this.getAddressComponent(details, 'route', 'long_name'); + const city = this.getAddressComponent(details, 'locality', 'long_name'); + const state = this.getAddressComponent(details, 'administrative_area_level_1', 'short_name'); + const zipCode = this.getAddressComponent(details, 'postal_code', 'long_name'); + + // Trigger text change events for each of the individual fields being saved on the server + this.props.onChangeText('addressStreet', `${streetNumber} ${streetName}`); + this.props.onChangeText('addressCity', city); + this.props.onChangeText('addressState', state); + this.props.onChangeText('addressZipCode', zipCode); + } + } + + render() { + return ( + this.saveLocationDetails(details)} + query={{ + key: 'AIzaSyC4axhhXtpiS-WozJEsmlL3Kg3kXucbZus', + language: this.props.preferredLocale, + }} + 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, + }} + styles={{ + textInputContainer: [styles.flexColumn], + listView: [ + styles.borderTopRounded, + styles.borderBottomRounded, + styles.mt1, + styles.overflowAuto, + styles.borderLeft, + styles.borderRight, + ], + row: [ + styles.pv4, + styles.ph3, + styles.overflowAuto, + ], + description: [styles.googleSearchText], + separator: [styles.googleSearchSeperator], + }} + /> + ); + } +} + +AddressSearch.propTypes = propTypes; +AddressSearch.defaultProps = defaultProps; + +export default withLocalize(AddressSearch); diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index e7e6754fd882..d1ba09725f99 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -24,7 +24,7 @@ import TextLink from '../../components/TextLink'; import StatePicker from '../../components/StatePicker'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import { - isValidAddress, isValidDate, isValidZipCode, isRequiredFulfilled, isValidPhoneWithSpecialChars, isValidURL, + isValidDate, isRequiredFulfilled, isValidURL, isValidPhoneWithSpecialChars, } from '../../libs/ValidationUtils'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; @@ -32,6 +32,7 @@ import ExpensiPicker from '../../components/ExpensiPicker'; import * as ReimbursementAccountUtils from '../../libs/ReimbursementAccountUtils'; import reimbursementAccountPropTypes from './reimbursementAccountPropTypes'; import ReimbursementAccountForm from './ReimbursementAccountForm'; +import AddressSearch from '../../components/AddressSearch'; const propTypes = { /** Bank account currently in setup */ @@ -69,10 +70,6 @@ class CompanyStep extends React.Component { // These fields need to be filled out in order to submit the form this.requiredFields = [ 'companyName', - 'addressStreet', - 'addressCity', - 'addressState', - 'addressZipCode', 'website', 'companyTaxID', 'incorporationDate', @@ -84,9 +81,6 @@ class CompanyStep extends React.Component { // Map a field to the key of the error's translation this.errorTranslationKeys = { - addressStreet: 'bankAccount.error.addressStreet', - addressCity: 'bankAccount.error.addressCity', - addressZipCode: 'bankAccount.error.zipCode', companyName: 'bankAccount.error.companyName', companyPhone: 'bankAccount.error.phoneNumber', website: 'bankAccount.error.website', @@ -125,13 +119,6 @@ class CompanyStep extends React.Component { */ validate() { const errors = {}; - if (!isValidAddress(this.state.addressStreet)) { - errors.addressStreet = true; - } - - if (!isValidZipCode(this.state.addressZipCode)) { - errors.addressZipCode = true; - } if (!isValidURL(this.state.website)) { errors.website = true; @@ -193,40 +180,11 @@ class CompanyStep extends React.Component { disabled={shouldDisableCompanyName} errorText={this.getErrorText('companyName')} /> - this.clearErrorAndSetValue('addressStreet', value)} - value={this.state.addressStreet} - errorText={this.getErrorText('addressStreet')} - /> - {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')} - maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} + value={`${this.state.addressStreet} ${this.state.addressCity} ${this.state.addressState} ${this.state.addressZipCode}`} + onChangeText={(fieldName, value) => this.clearErrorAndSetValue(fieldName, value)} />