From 6c2c14092a994ee99839db751c576e550879f7a1 Mon Sep 17 00:00:00 2001 From: Edu Date: Fri, 9 Jun 2023 16:42:12 +0200 Subject: [PATCH 01/25] push to page menu item --- src/ROUTES.js | 5 +- src/components/AddressSearch/index.js | 28 +++- src/components/CountryPicker.js | 102 +++++++------- src/components/MenuItemWithTopDescription.js | 19 ++- src/components/OptionsSelectorWithSearch.js | 105 +++++++++++++++ src/components/StatePicker.js | 127 +++++++++++------- src/components/TextInput/BaseTextInput.js | 1 + .../TextInput/baseTextInputPropTypes.js | 5 + .../AppNavigator/ModalStackNavigators.js | 22 +++ .../Navigators/RightModalNavigator.js | 10 ++ src/libs/Navigation/linkingConfig.js | 10 ++ src/libs/actions/PersonalDetails.js | 15 +++ src/pages/CountrySelectorPage.js | 83 ++++++++++++ src/pages/ReimbursementAccount/AddressForm.js | 5 +- src/pages/ReimbursementAccount/CompanyStep.js | 4 +- .../ReimbursementAccount/RequestorStep.js | 2 +- src/pages/StateSelectorPage.js | 83 ++++++++++++ .../Profile/PersonalDetails/AddressPage.js | 111 ++++++++------- src/styles/styles.js | 5 + 19 files changed, 580 insertions(+), 162 deletions(-) create mode 100644 src/components/OptionsSelectorWithSearch.js create mode 100644 src/pages/CountrySelectorPage.js create mode 100644 src/pages/StateSelectorPage.js diff --git a/src/ROUTES.js b/src/ROUTES.js index 966c3d0c5a1a..4cc2632e1bf8 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -70,7 +70,10 @@ export default { getReportShareCodeRoute: (reportID) => `r/${reportID}/details/shareCode`, SELECT_YEAR: 'select-year', getYearSelectionRoute: (minYear, maxYear, currYear, backTo) => `select-year?min=${minYear}&max=${maxYear}&year=${currYear}&backTo=${backTo}`, - + SETTINGS_SELECT_COUNTRY: 'select-country', + getCountrySelectionRoute: (countryISO, backTo) => `select-country?countryISO=${countryISO}&backTo=${backTo}`, + SETTINGS_USA_STATES: 'select-usa-states', + getUsaStateSelectionRoute: (stateISO, backTo) => `select-usa-states?stateISO=${stateISO}&backTo=${backTo}`, /** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */ CONCIERGE: 'concierge', diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 9699eb9aab94..cc1115c964b3 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -48,6 +48,12 @@ const propTypes = { /** A callback function when the value of this field has changed */ onInputChange: PropTypes.func.isRequired, + /** A callback when a new country has changed */ + onCountryChange: PropTypes.func, + + /** A callback when a new state has changed */ + onStateChange: PropTypes.func, + /** Customize the TextInput container */ // eslint-disable-next-line react/forbid-prop-types containerStyles: PropTypes.arrayOf(PropTypes.object), @@ -73,6 +79,8 @@ const defaultProps = { inputID: undefined, shouldSaveDraft: false, onBlur: () => {}, + onCountryChange: () => {}, + onStateChange: () => {}, errorText: '', hint: '', value: undefined, @@ -160,9 +168,18 @@ function AddressSearch(props) { state: state || stateAutoCompleteFallback, }; + const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country); + if (isValidCountryCode) { + values.country = country; + if (props.onCountryChange) { + props.onCountryChange(country); + } + } + // If the address is not in the US, use the full length state name since we're displaying the address's // state / province in a TextInput instead of in a picker. - if (country !== CONST.COUNTRY.US) { + const isUS = country === CONST.COUNTRY.US; + if (isUS) { values.state = longStateName; } @@ -172,17 +189,16 @@ function AddressSearch(props) { values.state = stateFallback; } + if (props.onStateChange) { + props.onStateChange(isUS ? state : longStateName, isUS); + } + // Not all pages define the Address Line 2 field, so in that case we append any additional address details // (e.g. Apt #) to Address Line 1 if (subpremise && typeof props.renamedInputKeys.street2 === 'undefined') { values.street += `, ${subpremise}`; } - const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country); - if (isValidCountryCode) { - values.country = country; - } - if (props.inputID) { _.each(values, (value, key) => { const inputKey = lodashGet(props.renamedInputKeys, key, key); diff --git a/src/components/CountryPicker.js b/src/components/CountryPicker.js index 61bfd26a0e67..8c13ae99d39b 100644 --- a/src/components/CountryPicker.js +++ b/src/components/CountryPicker.js @@ -1,67 +1,79 @@ -import _ from 'underscore'; -import React, {forwardRef} from 'react'; +import React, {useCallback, useRef, useEffect} from 'react'; +import {View} from 'react-native'; import PropTypes from 'prop-types'; -import Picker from './Picker'; +import sizes from '../styles/variables'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import * as PersonalDetails from '../libs/actions/PersonalDetails'; +import Navigation from '../libs/Navigation/Navigation'; +import ROUTES from '../ROUTES'; +import FormHelpMessage from './FormHelpMessage'; const propTypes = { - /** The label for the field */ - label: PropTypes.string, + /** The ISO code of the country */ + countryISO: PropTypes.string, - /** A callback method that is called when the value changes and it receives the selected value as an argument. */ - onInputChange: PropTypes.func.isRequired, + /** The ISO selected from CountrySelector */ + selectedCountryISO: PropTypes.string, - /** The value that needs to be selected */ - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** The ID used to uniquely identify the input in a form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - shouldSaveDraft: PropTypes.bool, - - /** Callback that is called when the text input is blurred */ - onBlur: PropTypes.func, - - /** Error text to display */ + /** Form Error description */ errorText: PropTypes.string, ...withLocalizePropTypes, }; const defaultProps = { - label: '', - value: undefined, + countryISO: '', + selectedCountryISO: undefined, errorText: '', - shouldSaveDraft: false, - inputID: undefined, - onBlur: () => {}, }; -const CountryPicker = forwardRef((props, ref) => { - const COUNTRIES = _.map(props.translate('allCountries'), (countryName, countryISO) => ({ - value: countryISO, - label: countryName, - })); +function BaseCountryPicker(props) { + const countryTitle = useRef({title: '', iso: ''}); + const countryISO = props.countryISO; + const selectedCountryISO = props.selectedCountryISO; + const onInputChange = props.onInputChange; + useEffect(() => { + if (!selectedCountryISO || selectedCountryISO === countryTitle.current.iso) { + return; + } + countryTitle.current = {title: PersonalDetails.getCountryNameBy(selectedCountryISO || countryISO), iso: selectedCountryISO || countryISO}; + + // Needed to call onInputChange, so Form can update the validation and values + onInputChange(countryTitle.current.iso); + }, [countryISO, selectedCountryISO, onInputChange]); + + const navigateToCountrySelector = useCallback(() => { + Navigation.navigate(ROUTES.getCountrySelectionRoute(selectedCountryISO || countryISO, Navigation.getActiveRoute())); + }, [countryISO, selectedCountryISO]); + const descStyle = countryTitle.current.title.length === 0 ? {fontSize: sizes.fontSizeNormal} : null; return ( - + + + + ); -}); +} + +BaseCountryPicker.propTypes = propTypes; +BaseCountryPicker.defaultProps = defaultProps; + +const CountryPicker = React.forwardRef((props, ref) => ( + +)); -CountryPicker.propTypes = propTypes; -CountryPicker.defaultProps = defaultProps; CountryPicker.displayName = 'CountryPicker'; export default withLocalize(CountryPicker); diff --git a/src/components/MenuItemWithTopDescription.js b/src/components/MenuItemWithTopDescription.js index 4bd426d80f0c..31997525d512 100644 --- a/src/components/MenuItemWithTopDescription.js +++ b/src/components/MenuItemWithTopDescription.js @@ -6,16 +6,15 @@ const propTypes = { ...menuItemPropTypes, }; -function MenuItemWithTopDescription(props) { - return ( - - ); -} +const MenuItemWithTopDescription = React.forwardRef((props, ref) => ( + +)); MenuItemWithTopDescription.propTypes = propTypes; MenuItemWithTopDescription.displayName = 'MenuItemWithTopDescription'; diff --git a/src/components/OptionsSelectorWithSearch.js b/src/components/OptionsSelectorWithSearch.js new file mode 100644 index 000000000000..313170f75733 --- /dev/null +++ b/src/components/OptionsSelectorWithSearch.js @@ -0,0 +1,105 @@ +import _ from 'underscore'; +import React, {useState} from 'react'; +import PropTypes from 'prop-types'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import ScreenWrapper from './ScreenWrapper'; +import HeaderWithBackButton from './HeaderWithBackButton'; +import * as Expensicons from './Icon/Expensicons'; +import themeColors from '../styles/themes/default'; +import OptionsSelector from './OptionsSelector'; +import styles from '../styles/styles'; + +const propTypes = { + /** Title of the page */ + title: PropTypes.string.isRequired, + + /** Function to call when the back button is pressed */ + onBackButtonPress: PropTypes.func.isRequired, + + /** Text to display in the search input label */ + textSearchLabel: PropTypes.string.isRequired, + + /** Placeholder text to display in the search input */ + placeholder: PropTypes.string.isRequired, + + /** Function to call when a row is selected */ + onSelectRow: PropTypes.func.isRequired, + + /** Initial option to display as selected */ + initialOption: PropTypes.string, + + data: PropTypes.arrayOf( + PropTypes.shape({ + /** Text to display for the option */ + text: PropTypes.string.isRequired, + + /** Value of the option */ + value: PropTypes.string.isRequired, + + /** Key to use for the option in the list */ + keyForList: PropTypes.string.isRequired, + }), + ).isRequired, + + ...withLocalizePropTypes, +}; + +const defaultProps = { + initialOption: '', +}; + +const greenCheckmark = {src: Expensicons.Checkmark, color: themeColors.success}; + +function filterOptions(searchValue, data) { + const trimmedSearchValue = searchValue.trim(); + if (trimmedSearchValue.length === 0) { + return []; + } + + return _.filter(data, (country) => country.text.toLowerCase().includes(searchValue.toLowerCase())); +} + +function OptionsSelectorWithSearch(props) { + const [searchValue, setSearchValue] = useState(''); + const translate = props.translate; + const initialOption = props.initialOption; + + const filteredData = filterOptions(searchValue, props.data); + const headerMessage = searchValue.trim() && !filteredData.length ? translate('common.noResultsFound') : ''; + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + <> + + + + )} + + ); +} + +OptionsSelectorWithSearch.propTypes = propTypes; +OptionsSelectorWithSearch.defaultProps = defaultProps; +OptionsSelectorWithSearch.displayName = 'OptionsSelectorWithSearch'; + +export {greenCheckmark}; + +export default withLocalize(OptionsSelectorWithSearch); diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js index 1d18652a625f..fd920e6719fa 100644 --- a/src/components/StatePicker.js +++ b/src/components/StatePicker.js @@ -1,67 +1,102 @@ -import _ from 'underscore'; -import React, {forwardRef} from 'react'; +import lodashGet from 'lodash/get'; +import React, {useState, useEffect, useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import {useRoute} from '@react-navigation/native'; import PropTypes from 'prop-types'; -import Picker from './Picker'; +import compose from '../libs/compose'; +import withNavigation from './withNavigation'; +import sizes from '../styles/variables'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import Navigation from '../libs/Navigation/Navigation'; +import ROUTES from '../ROUTES'; +import FormHelpMessage from './FormHelpMessage'; const propTypes = { - /** The label for the field */ - label: PropTypes.string, - - /** A callback method that is called when the value changes and it receives the selected value as an argument. */ - onInputChange: PropTypes.func.isRequired, - - /** The value that needs to be selected */ - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - shouldSaveDraft: PropTypes.bool, - - /** Callback that is called when the text input is blurred */ - onBlur: PropTypes.func, + /** Current State from user address */ + stateISO: PropTypes.string, /** Error text to display */ errorText: PropTypes.string, + /** Default value to display */ + defaultValue: PropTypes.string, + ...withLocalizePropTypes, }; const defaultProps = { - label: '', - value: undefined, + stateISO: '', errorText: '', - shouldSaveDraft: false, - inputID: undefined, - onBlur: () => {}, + defaultValue: '', }; -const StatePicker = forwardRef((props, ref) => { - const STATES = _.chain(props.translate('allStates')) - .sortBy((state) => state.stateName.toLowerCase()) - .map((state) => ({value: state.stateISO, label: state.stateName})) - .value(); +function BaseStatePicker(props) { + const route = useRoute(); + const stateISO = props.stateISO; + const [stateTitle, setStateTitle] = useState(stateISO); + const paramStateISO = lodashGet(route, 'params.stateISO'); + const navigation = props.navigation; + const onInputChange = props.onInputChange; + const defaultValue = props.defaultValue; + const translate = props.translate; + + useEffect(() => { + if (!paramStateISO || paramStateISO === stateTitle) { + return; + } + + setStateTitle(paramStateISO); + + // Needed to call onInputChange, so Form can update the validation and values + onInputChange(paramStateISO); + + navigation.setParams({stateISO: null}); + }, [paramStateISO, stateTitle, onInputChange, navigation]); + + const navigateToCountrySelector = useCallback(() => { + Navigation.navigate(ROUTES.getUsaStateSelectionRoute(stateTitle || stateISO, Navigation.getActiveRoute())); + }, [stateTitle, stateISO]); + + const title = useMemo(() => { + const allStates = translate('allStates'); + if (!stateTitle) { + return defaultValue ? allStates[defaultValue].stateName : ''; + } + if (allStates[stateTitle]) { + return allStates[stateTitle].stateName; + } + + return stateTitle; + }, [translate, stateTitle, defaultValue]); + const descStyle = title.length === 0 ? {fontSize: sizes.fontSizeNormal} : null; return ( - + + + + ); -}); +} + +BaseStatePicker.propTypes = propTypes; +BaseStatePicker.defaultProps = defaultProps; + +const StatePicker = React.forwardRef((props, ref) => ( + +)); -StatePicker.propTypes = propTypes; -StatePicker.defaultProps = defaultProps; StatePicker.displayName = 'StatePicker'; -export default withLocalize(StatePicker); +export default compose(withLocalize, withNavigation)(StatePicker); diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 91ab2162674f..fa5c02c81a68 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -366,6 +366,7 @@ class BaseTextInput extends Component { {!_.isEmpty(inputHelpText) && ( diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js index 2e278bab5d69..a082afbea5c7 100644 --- a/src/components/TextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/baseTextInputPropTypes.js @@ -1,4 +1,5 @@ import PropTypes from 'prop-types'; +import stylePropTypes from '../../styles/stylePropTypes'; const propTypes = { /** Input label */ @@ -58,6 +59,9 @@ const propTypes = { /** Hint text to display below the TextInput */ hint: PropTypes.string, + /** Style the Hint container */ + hintContainerStyle: stylePropTypes, + /** Prefix character */ prefixCharacter: PropTypes.string, @@ -114,6 +118,7 @@ const defaultProps = { shouldSaveDraft: false, maxLength: null, hint: '', + hintContainerStyle: [], prefixCharacter: '', onInputChange: () => {}, shouldDelayFocus: false, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index b983ffd14968..61ff2f16e84c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -729,6 +729,26 @@ const FlagCommentStackNavigator = createModalStackNavigator([ }, ]); +const CountrySelectorStackNavigator = createModalStackNavigator([ + { + getComponent: () => { + const CountrySelectorPage = require('../../../pages/CountrySelectorPage').default; + return CountrySelectorPage; + }, + name: 'CountrySelector_Root', + }, +]); + +const UsaStateSelectorStackNavigator = createModalStackNavigator([ + { + getComponent: () => { + const usaStateSelectorPage = require('../../../pages/StateSelectorPage').default; + return usaStateSelectorPage; + }, + name: 'CountrySelector_Root', + }, +]); + export { IOUBillStackNavigator, IOURequestModalStackNavigator, @@ -752,4 +772,6 @@ export { WalletStatementStackNavigator, YearPickerStackNavigator, FlagCommentStackNavigator, + CountrySelectorStackNavigator, + UsaStateSelectorStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js index cb82795936c2..4b13d9288e75 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js @@ -114,6 +114,16 @@ function RigthModalNavigator() { options={defaultModalScreenOptions} component={ModalStackNavigators.FlagCommentStackNavigator} /> + + ); } diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 37506c0460ce..542daa4367c4 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -342,6 +342,16 @@ export default { FlagComment_Root: ROUTES.FLAG_COMMENT, }, }, + Select_Country: { + screens: { + CountrySelector_Root: ROUTES.SETTINGS_SELECT_COUNTRY, + }, + }, + Select_USA_State: { + screens: { + CountrySelector_Root: ROUTES.SETTINGS_USA_STATES, + }, + }, }, }, }, diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 53680f65a1ec..70fa6173c633 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -95,6 +95,20 @@ function getCountryISO(countryName) { return _.findKey(CONST.ALL_COUNTRIES, (country) => country === countryName) || ''; } +/** + * Returns the name of the country associated with the provided ISO code. + * If the provided code is invalid, an empty string is returned. + * + * @param {string} countryISO The ISO code of the country to look up. + * @returns {string} The name of the country associated with the provided ISO code. + */ +function getCountryNameBy(countryISO) { + if (_.isEmpty(countryISO) || countryISO.length !== 2) { + return countryISO; + } + + return CONST.ALL_COUNTRIES[countryISO] || ''; +} /** * @param {String} pronouns */ @@ -481,4 +495,5 @@ export { updateAutomaticTimezone, updateSelectedTimezone, getCountryISO, + getCountryNameBy, }; diff --git a/src/pages/CountrySelectorPage.js b/src/pages/CountrySelectorPage.js new file mode 100644 index 000000000000..3776052976c3 --- /dev/null +++ b/src/pages/CountrySelectorPage.js @@ -0,0 +1,83 @@ +import _ from 'underscore'; +import lodashGet from 'lodash/get'; +import React, {useCallback, useMemo} from 'react'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import ROUTES from '../ROUTES'; +import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; +import Navigation from '../libs/Navigation/Navigation'; +import compose from '../libs/compose'; +import ONYXKEYS from '../ONYXKEYS'; +import OptionsSelectorWithSearch, {greenCheckmark} from '../components/OptionsSelectorWithSearch'; + +const propTypes = { + route: PropTypes.shape({ + params: PropTypes.shape({ + backTo: PropTypes.string, + }), + }).isRequired, + + /** User's private personal details */ + privatePersonalDetails: PropTypes.shape({ + /** User's home address */ + address: PropTypes.shape({ + country: PropTypes.string, + }), + }), + + ...withLocalizePropTypes, +}; + +const defaultProps = { + privatePersonalDetails: { + address: { + state: '', + }, + }, +}; + +function CountrySelectorPage(props) { + const translate = props.translate; + const route = props.route; + const currentCountry = route.params.countryISO || lodashGet(props.privatePersonalDetails, 'address.country'); + + const countries = useMemo( + () => + _.map(translate('allCountries'), (countryName, countryISO) => ({ + value: countryISO, + keyForList: countryISO, + text: countryName, + customIcon: currentCountry === countryISO ? greenCheckmark : undefined, + })), + [translate, currentCountry], + ); + + const updateCountry = useCallback((selectedCountry) => { + Navigation.goBack(`${ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS}?countryISO=${selectedCountry.value}`, true); + }, []); + + return ( + Navigation.goBack(`${route.params.backTo}`)} + textSearchLabel={translate('common.country')} + placeholder={translate('pronounsPage.placeholderText')} + onSelectRow={updateCountry} + initialOption={currentCountry} + /> + ); +} + +CountrySelectorPage.propTypes = propTypes; +CountrySelectorPage.defaultProps = defaultProps; +CountrySelectorPage.displayName = 'CountrySelectorPage'; + +export default compose( + withLocalize, + withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, + }), +)(CountrySelectorPage); diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 136befbe697c..4f700d7c23a1 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -117,9 +117,10 @@ function AddressForm(props) { defaultValue={props.defaultValues.city} onChangeText={(value) => props.onFieldChange({city: value})} errorText={props.errors.city ? props.translate('bankAccount.error.addressCity') : ''} - containerStyles={[styles.mt4]} + containerStyles={styles.mt4} /> - + + {this.props.translate('companyStep.subtitle')} - + + _.map(translate('allStates'), (state) => ({ + value: state.stateISO, + keyForList: state.stateISO, + text: state.stateName, + customIcon: currentCountryState === state.stateISO ? greenCheckmark : undefined, + })), + [translate, currentCountryState], + ); + + const updateCountryState = useCallback((selectedState) => { + Navigation.goBack(`${ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS}?stateISO=${selectedState.value}`, true); + }, []); + + return ( + Navigation.goBack(`${route.params.backTo}`)} + textSearchLabel={translate('common.state')} + placeholder={translate('pronounsPage.placeholderText')} + onSelectRow={updateCountryState} + initialOption={currentCountryState} + data={countryStates} + /> + ); +} + +StateSelectorPage.propTypes = propTypes; +StateSelectorPage.defaultProps = defaultProps; +StateSelectorPage.displayName = 'StateSelectorPage'; + +export default compose( + withLocalize, + withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, + }), +)(StateSelectorPage); diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index d26960e406f5..9ac74209693d 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import _ from 'underscore'; -import React, {useState, useCallback} from 'react'; +import React, {useState, useCallback, useEffect} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; @@ -60,17 +60,29 @@ function updateAddress(values) { PersonalDetails.updateAddress(values.addressLine1.trim(), values.addressLine2.trim(), values.city.trim(), values.state.trim(), values.zipPostCode.trim().toUpperCase(), values.country); } -function AddressPage(props) { - const {translate} = props; - const [countryISO, setCountryISO] = useState(PersonalDetails.getCountryISO(lodashGet(props.privatePersonalDetails, 'address.country')) || CONST.COUNTRY.US); +function AddressPage({translate, route, navigation, privatePersonalDetails}) { + const [countryISO, setCountryISO] = useState(PersonalDetails.getCountryISO(lodashGet(privatePersonalDetails, 'address.country')) || CONST.COUNTRY.US); const isUSAForm = countryISO === CONST.COUNTRY.US; const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [countryISO, 'samples'], ''); const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); - const address = lodashGet(props.privatePersonalDetails, 'address') || {}; + const address = lodashGet(privatePersonalDetails, 'address') || {}; const [street1, street2] = (address.street || '').split('\n'); + const onCountryUpdate = (newCountry) => { + setCountryISO(newCountry); + }; + const onCountryStateUpdate = (selectedCountryState) => { + navigation.setParams({stateISO: selectedCountryState}); + }; + + useEffect(() => { + const currentCountryISO = lodashGet(route, 'params.countryISO'); + if (currentCountryISO) { + setCountryISO(currentCountryISO); + } + }, [route, navigation]); /** * @param {Function} translate - translate function * @param {Boolean} isUSAForm - selected country ISO code is US @@ -123,24 +135,26 @@ function AddressPage(props) { return ( Navigation.goBack(ROUTES.SETTINGS_PERSONAL_DETAILS)} />
- - - - - + + + + - - {isUSAForm ? ( + {isUSAForm ? ( + - ) : ( - - )} - - + + ) : ( - - - - + )} + + + +
); diff --git a/src/styles/styles.js b/src/styles/styles.js index eb54f1dc5ca0..4b2815639470 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3385,6 +3385,11 @@ const styles = { maxWidth: 375, }, + formSpaceVertical: { + height: 20, + width: 1, + }, + taskCheckbox: { height: 16, width: 16, From c9c320a43a2065397dde9f15013aabae9346f2df Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 19 Jun 2023 12:00:51 +0200 Subject: [PATCH 02/25] Fixed comments --- src/components/AddressSearch/index.js | 2 +- src/components/CountryPicker.js | 5 ++++- src/components/OptionsSelectorWithSearch.js | 6 +++++- src/components/StatePicker.js | 7 ++++--- src/pages/CountrySelectorPage.js | 3 +++ src/pages/StateSelectorPage.js | 3 +++ src/pages/settings/Profile/PersonalDetails/AddressPage.js | 2 +- 7 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index cc1115c964b3..89b05260341a 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -179,7 +179,7 @@ function AddressSearch(props) { // If the address is not in the US, use the full length state name since we're displaying the address's // state / province in a TextInput instead of in a picker. const isUS = country === CONST.COUNTRY.US; - if (isUS) { + if (!isUS) { values.state = longStateName; } diff --git a/src/components/CountryPicker.js b/src/components/CountryPicker.js index 8c13ae99d39b..41e7e2565379 100644 --- a/src/components/CountryPicker.js +++ b/src/components/CountryPicker.js @@ -2,6 +2,7 @@ import React, {useCallback, useRef, useEffect} from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; import sizes from '../styles/variables'; +import styles from '../styles/styles'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import * as PersonalDetails from '../libs/actions/PersonalDetails'; @@ -58,7 +59,9 @@ function BaseCountryPicker(props) { description={props.translate('common.country')} onPress={navigateToCountrySelector} /> - + + +
); } diff --git a/src/components/OptionsSelectorWithSearch.js b/src/components/OptionsSelectorWithSearch.js index 313170f75733..945037cd140e 100644 --- a/src/components/OptionsSelectorWithSearch.js +++ b/src/components/OptionsSelectorWithSearch.js @@ -25,6 +25,9 @@ const propTypes = { /** Function to call when a row is selected */ onSelectRow: PropTypes.func.isRequired, + /** Initial value to display in the search input */ + initialSearchValue: PropTypes.string, + /** Initial option to display as selected */ initialOption: PropTypes.string, @@ -45,6 +48,7 @@ const propTypes = { }; const defaultProps = { + initialSearchValue: '', initialOption: '', }; @@ -60,7 +64,7 @@ function filterOptions(searchValue, data) { } function OptionsSelectorWithSearch(props) { - const [searchValue, setSearchValue] = useState(''); + const [searchValue, setSearchValue] = useState(props.initialSearchValue); const translate = props.translate; const initialOption = props.initialOption; diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js index fd920e6719fa..6e1d81e0afb7 100644 --- a/src/components/StatePicker.js +++ b/src/components/StatePicker.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types'; import compose from '../libs/compose'; import withNavigation from './withNavigation'; import sizes from '../styles/variables'; +import styles from '../styles/styles'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import Navigation from '../libs/Navigation/Navigation'; @@ -50,8 +51,6 @@ function BaseStatePicker(props) { // Needed to call onInputChange, so Form can update the validation and values onInputChange(paramStateISO); - - navigation.setParams({stateISO: null}); }, [paramStateISO, stateTitle, onInputChange, navigation]); const navigateToCountrySelector = useCallback(() => { @@ -81,7 +80,9 @@ function BaseStatePicker(props) { descriptionTextStyle={descStyle} onPress={navigateToCountrySelector} /> - + + +
); } diff --git a/src/pages/CountrySelectorPage.js b/src/pages/CountrySelectorPage.js index 3776052976c3..fc493d0dce63 100644 --- a/src/pages/CountrySelectorPage.js +++ b/src/pages/CountrySelectorPage.js @@ -3,6 +3,7 @@ import lodashGet from 'lodash/get'; import React, {useCallback, useMemo} from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; +import * as PersonalDetails from '../libs/actions/PersonalDetails'; import ROUTES from '../ROUTES'; import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; import Navigation from '../libs/Navigation/Navigation'; @@ -40,6 +41,7 @@ function CountrySelectorPage(props) { const translate = props.translate; const route = props.route; const currentCountry = route.params.countryISO || lodashGet(props.privatePersonalDetails, 'address.country'); + const selectedSearchCountry = PersonalDetails.getCountryNameBy(currentCountry); const countries = useMemo( () => @@ -64,6 +66,7 @@ function CountrySelectorPage(props) { textSearchLabel={translate('common.country')} placeholder={translate('pronounsPage.placeholderText')} onSelectRow={updateCountry} + initialSearchValue={selectedSearchCountry} initialOption={currentCountry} /> ); diff --git a/src/pages/StateSelectorPage.js b/src/pages/StateSelectorPage.js index 4a70d0775b5d..00dcb445899c 100644 --- a/src/pages/StateSelectorPage.js +++ b/src/pages/StateSelectorPage.js @@ -40,6 +40,8 @@ function StateSelectorPage(props) { const translate = props.translate; const route = props.route; const currentCountryState = !_.isEmpty(route.params) ? route.params.stateISO : lodashGet(props.privatePersonalDetails, 'address.state'); + const allStates = translate('allStates'); + const selectedSearchState = !_.isEmpty(currentCountryState) ? allStates[currentCountryState].stateName : ''; const countryStates = useMemo( () => @@ -63,6 +65,7 @@ function StateSelectorPage(props) { textSearchLabel={translate('common.state')} placeholder={translate('pronounsPage.placeholderText')} onSelectRow={updateCountryState} + initialSearchValue={selectedSearchState} initialOption={currentCountryState} data={countryStates} /> diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index 9ac74209693d..29fe43ac46cf 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -147,7 +147,7 @@ function AddressPage({translate, route, navigation, privatePersonalDetails}) { submitButtonText={translate('common.save')} enabledWhenOffline > - + Date: Fri, 9 Jun 2023 16:42:12 +0200 Subject: [PATCH 03/25] push to page menu item --- src/components/OptionsSelectorWithSearch.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/OptionsSelectorWithSearch.js b/src/components/OptionsSelectorWithSearch.js index 945037cd140e..e956dc048705 100644 --- a/src/components/OptionsSelectorWithSearch.js +++ b/src/components/OptionsSelectorWithSearch.js @@ -49,6 +49,7 @@ const propTypes = { const defaultProps = { initialSearchValue: '', + initialOption: '', }; From 062bc8a3987605a271013593a295b52de525d175 Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 19 Jun 2023 12:00:51 +0200 Subject: [PATCH 04/25] Fixed comments --- src/components/OptionsSelectorWithSearch.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/OptionsSelectorWithSearch.js b/src/components/OptionsSelectorWithSearch.js index e956dc048705..945037cd140e 100644 --- a/src/components/OptionsSelectorWithSearch.js +++ b/src/components/OptionsSelectorWithSearch.js @@ -49,7 +49,6 @@ const propTypes = { const defaultProps = { initialSearchValue: '', - initialOption: '', }; From 474125f035d2e49bf6e195d7804ee79c39da9486 Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 26 Jun 2023 17:04:08 +0200 Subject: [PATCH 05/25] Made array containerStyle prop --- src/pages/ReimbursementAccount/AddressForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 4f700d7c23a1..39fa73b97076 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -117,7 +117,7 @@ function AddressForm(props) { defaultValue={props.defaultValues.city} onChangeText={(value) => props.onFieldChange({city: value})} errorText={props.errors.city ? props.translate('bankAccount.error.addressCity') : ''} - containerStyles={styles.mt4} + containerStyles={[styles.mt4]} /> From 74cc95a8b735e31abc2920590320532da3dc1eaa Mon Sep 17 00:00:00 2001 From: Edu Date: Wed, 28 Jun 2023 13:15:55 +0200 Subject: [PATCH 06/25] handling navigation and state update --- src/ROUTES.js | 2 +- src/components/StatePicker.js | 22 ++++++----- src/hooks/useNavigationStorage.js | 37 +++++++++++++++++++ src/pages/ReimbursementAccount/AddressForm.js | 12 +++++- src/pages/ReimbursementAccount/CompanyStep.js | 9 ++++- .../ReimbursementAccountPage.js | 1 + src/pages/StateSelectorPage.js | 16 +++++--- .../Profile/PersonalDetails/AddressPage.js | 6 ++- .../PersonalDetailsInitialPage.js | 5 +++ 9 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 src/hooks/useNavigationStorage.js diff --git a/src/ROUTES.js b/src/ROUTES.js index f784c41db6d8..2ac62585d3eb 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -72,7 +72,7 @@ export default { SETTINGS_SELECT_COUNTRY: 'select-country', getCountrySelectionRoute: (countryISO, backTo) => `select-country?countryISO=${countryISO}&backTo=${backTo}`, SETTINGS_USA_STATES: 'select-usa-states', - getUsaStateSelectionRoute: (stateISO, backTo) => `select-usa-states?stateISO=${stateISO}&backTo=${backTo}`, + getUsaStateSelectionRoute: (stateISO, key, backTo) => `select-usa-states?stateISO=${stateISO}&key=${key}&backTo=${encodeURIComponent(backTo)}`, /** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */ CONCIERGE: 'concierge', diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js index 6e1d81e0afb7..2659eb1600d1 100644 --- a/src/components/StatePicker.js +++ b/src/components/StatePicker.js @@ -1,8 +1,8 @@ -import lodashGet from 'lodash/get'; import React, {useState, useEffect, useCallback, useMemo} from 'react'; import {View} from 'react-native'; import {useRoute} from '@react-navigation/native'; import PropTypes from 'prop-types'; +import useNavigationStorage from '../hooks/useNavigationStorage'; import compose from '../libs/compose'; import withNavigation from './withNavigation'; import sizes from '../styles/variables'; @@ -34,28 +34,30 @@ const defaultProps = { function BaseStatePicker(props) { const route = useRoute(); + const defaultValue = props.defaultValue; + const [collect] = useNavigationStorage(props.inputID, defaultValue); const stateISO = props.stateISO; - const [stateTitle, setStateTitle] = useState(stateISO); - const paramStateISO = lodashGet(route, 'params.stateISO'); - const navigation = props.navigation; + const paramStateISO = collect(); + const [stateTitle, setStateTitle] = useState(stateISO || paramStateISO); const onInputChange = props.onInputChange; - const defaultValue = props.defaultValue; const translate = props.translate; useEffect(() => { - if (!paramStateISO || paramStateISO === stateTitle) { + if (!paramStateISO) { return; } - setStateTitle(paramStateISO); // Needed to call onInputChange, so Form can update the validation and values onInputChange(paramStateISO); - }, [paramStateISO, stateTitle, onInputChange, navigation]); + // onInputChange isn't a stable function, so we can't add it to the dependency array + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [paramStateISO]); const navigateToCountrySelector = useCallback(() => { - Navigation.navigate(ROUTES.getUsaStateSelectionRoute(stateTitle || stateISO, Navigation.getActiveRoute())); - }, [stateTitle, stateISO]); + // Try first using the route.path so I can keep any query params + Navigation.navigate(ROUTES.getUsaStateSelectionRoute(stateTitle || stateISO, props.inputID, route.path || Navigation.getActiveRoute())); + }, [stateTitle, stateISO, route.path, props.inputID]); const title = useMemo(() => { const allStates = translate('allStates'); diff --git a/src/hooks/useNavigationStorage.js b/src/hooks/useNavigationStorage.js new file mode 100644 index 000000000000..8fbf46a6fbfc --- /dev/null +++ b/src/hooks/useNavigationStorage.js @@ -0,0 +1,37 @@ +import {useEffect} from 'react'; + +const storage = new Map(); + +/** + * Clears the navigation storage, call it before to navigate to a new page + */ +function clearNavigationStorage() { + storage.clear(); +} + +/** + * Saves a value into the navigation storage. Use it for class components + * @param {String} key + * @param {any} value + */ +function saveIntoStorage(key, value) { + storage.set(key, value); +} + +export default function useNavigationStorage(key = 'input', initialValue = null) { + useEffect(() => { + if (!initialValue || storage.has(key)) { + return; + } + storage.set(key, initialValue); + }, [key, initialValue]); + + const collect = () => storage.get(key); + const save = (value) => { + storage.set(key, value); + }; + + return [collect, save]; +} + +export {clearNavigationStorage, saveIntoStorage}; diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 39fa73b97076..845609d69d43 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -14,6 +14,12 @@ const propTypes = { /** Callback fired when a field changes. Passes args as {[fieldName]: val} */ onFieldChange: PropTypes.func, + /** Callback fired when a Address search changes the Country. */ + onCountryChange: PropTypes.func, + + /** Callback fired when a Address search changes the State. */ + onStateChange: PropTypes.func, + /** Default values */ defaultValues: PropTypes.shape({ /** Address street field */ @@ -89,6 +95,8 @@ const defaultProps = { }, shouldSaveDraft: false, onFieldChange: () => {}, + onCountryChange: () => {}, + onStateChange: () => {}, }; function AddressForm(props) { @@ -103,6 +111,8 @@ function AddressForm(props) { value={props.values.street} defaultValue={props.defaultValues.street} onInputChange={props.onFieldChange} + onCountryChange={props.onCountryChange} + onStateChange={props.onStateChange} errorText={props.errors.street ? props.translate('bankAccount.error.addressStreet') : ''} hint={props.translate('common.noPO')} renamedInputKeys={props.inputKeys} @@ -125,7 +135,7 @@ function AddressForm(props) { inputID={props.inputKeys.state} shouldSaveDraft={props.shouldSaveDraft} value={props.values.state} - defaultValue={props.defaultValues.state} + defaultValue={props.defaultValues.state || ''} onInputChange={(value) => props.onFieldChange({state: value})} errorText={props.errors.state ? props.translate('bankAccount.error.addressState') : ''} /> diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 65e7a1fda1d2..774a8144a366 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -7,6 +7,7 @@ import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import {parsePhoneNumber} from 'awesome-phonenumber'; import HeaderWithBackButton from '../../components/HeaderWithBackButton'; +import StatePicker from '../../components/StatePicker'; import CONST from '../../CONST'; import * as BankAccounts from '../../libs/actions/BankAccounts'; import Text from '../../components/Text'; @@ -15,7 +16,6 @@ import TextInput from '../../components/TextInput'; import styles from '../../styles/styles'; import CheckboxWithLabel from '../../components/CheckboxWithLabel'; import TextLink from '../../components/TextLink'; -import StatePicker from '../../components/StatePicker'; import withLocalize from '../../components/withLocalize'; import * as ValidationUtils from '../../libs/ValidationUtils'; import compose from '../../libs/compose'; @@ -25,6 +25,7 @@ import AddressForm from './AddressForm'; import Form from '../../components/Form'; import ScreenWrapper from '../../components/ScreenWrapper'; import StepPropTypes from './StepPropTypes'; +import * as NavigationStorage from '../../hooks/useNavigationStorage'; const propTypes = { ...StepPropTypes, @@ -54,6 +55,7 @@ class CompanyStep extends React.Component { super(props); this.submit = this.submit.bind(this); + this.onCountryStateUpdate = this.onCountryStateUpdate.bind(this); this.validate = this.validate.bind(this); this.defaultWebsite = lodashGet(props, 'user.isFromPublicDomain', false) ? 'https://' : `https://www.${Str.extractEmailDomain(props.session.email, '')}`; @@ -63,6 +65,10 @@ class CompanyStep extends React.Component { BankAccounts.resetReimbursementAccount(); } + onCountryStateUpdate(selectedCountryState) { + NavigationStorage.saveIntoStorage('addressState', selectedCountryState); + } + /** * @param {Array} fieldNames * @@ -197,6 +203,7 @@ class CompanyStep extends React.Component { state: 'addressState', zipCode: 'addressZipCode', }} + onStateChange={this.onCountryStateUpdate} shouldSaveDraft streetTranslationKey="common.companyAddress" /> diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js index d12231e85370..a3d567bf82a3 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js @@ -418,6 +418,7 @@ class ReimbursementAccountPage extends React.Component { reimbursementAccountDraft={this.props.reimbursementAccountDraft} onBackButtonPress={this.goBack} getDefaultStateForField={this.getDefaultStateForField} + navigation={this.props.navigation} /> ); } diff --git a/src/pages/StateSelectorPage.js b/src/pages/StateSelectorPage.js index 00dcb445899c..994493c9f5a2 100644 --- a/src/pages/StateSelectorPage.js +++ b/src/pages/StateSelectorPage.js @@ -1,9 +1,8 @@ import _ from 'underscore'; -import lodashGet from 'lodash/get'; import React, {useCallback, useMemo} from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; -import ROUTES from '../ROUTES'; +import useNavigationStorage from '../hooks/useNavigationStorage'; import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; import Navigation from '../libs/Navigation/Navigation'; import compose from '../libs/compose'; @@ -39,7 +38,8 @@ const defaultProps = { function StateSelectorPage(props) { const translate = props.translate; const route = props.route; - const currentCountryState = !_.isEmpty(route.params) ? route.params.stateISO : lodashGet(props.privatePersonalDetails, 'address.state'); + const [collect, dispatch] = useNavigationStorage(route.params.key, null, 'page selector'); + const currentCountryState = collect(); const allStates = translate('allStates'); const selectedSearchState = !_.isEmpty(currentCountryState) ? allStates[currentCountryState].stateName : ''; @@ -54,9 +54,13 @@ function StateSelectorPage(props) { [translate, currentCountryState], ); - const updateCountryState = useCallback((selectedState) => { - Navigation.goBack(`${ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS}?stateISO=${selectedState.value}`, true); - }, []); + const updateCountryState = useCallback( + (selectedState) => { + dispatch(selectedState.value); + Navigation.goBack(`${decodeURIComponent(route.params.backTo)}`, true); + }, + [dispatch, route.params.backTo], + ); return ( { - navigation.setParams({stateISO: selectedCountryState}); + saveIntoStorage(selectedCountryState); }; useEffect(() => { @@ -83,6 +85,7 @@ function AddressPage({translate, route, navigation, privatePersonalDetails}) { setCountryISO(currentCountryISO); } }, [route, navigation]); + /** * @param {Function} translate - translate function * @param {Boolean} isUSAForm - selected country ISO code is US @@ -92,7 +95,6 @@ function AddressPage({translate, route, navigation, privatePersonalDetails}) { const validate = useCallback( (values) => { const errors = {}; - const requiredFields = ['addressLine1', 'city', 'country', 'state']; // Check "State" dropdown is a valid state if selected Country is USA. diff --git a/src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js b/src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js index a466d2091de1..2436b51ba073 100644 --- a/src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js +++ b/src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js @@ -14,6 +14,7 @@ import MenuItemWithTopDescription from '../../../../components/MenuItemWithTopDe import * as PersonalDetails from '../../../../libs/actions/PersonalDetails'; import ONYXKEYS from '../../../../ONYXKEYS'; import {withNetwork} from '../../../../components/OnyxProvider'; +import * as NavigationStorage from '../../../../hooks/useNavigationStorage'; const propTypes = { /* Onyx Props */ @@ -61,6 +62,10 @@ function PersonalDetailsInitialPage(props) { PersonalDetails.openPersonalDetailsPage(); }, [props.network.isOffline]); + useEffect(() => { + NavigationStorage.clearNavigationStorage(); + }, []); + const privateDetails = props.privatePersonalDetails || {}; const address = privateDetails.address || {}; const legalName = `${privateDetails.legalFirstName || ''} ${privateDetails.legalLastName || ''}`.trim(); From f01b1e07dbf831d7d9af850bfc7f60cfa485c878 Mon Sep 17 00:00:00 2001 From: Edu Date: Wed, 28 Jun 2023 13:45:44 +0200 Subject: [PATCH 07/25] improved navigation animation --- src/components/OptionsSelectorWithSearch.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/OptionsSelectorWithSearch.js b/src/components/OptionsSelectorWithSearch.js index 945037cd140e..2c5fe0fa660f 100644 --- a/src/components/OptionsSelectorWithSearch.js +++ b/src/components/OptionsSelectorWithSearch.js @@ -92,6 +92,7 @@ function OptionsSelectorWithSearch(props) { safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} shouldFocusOnSelectRow shouldHaveOptionSeparator + shouldDelayFocus initiallyFocusedOptionKey={initialOption} /> From e48c601b0555983b1f49a92e02b69db85aa4ce99 Mon Sep 17 00:00:00 2001 From: Edu Date: Fri, 7 Jul 2023 10:33:36 +0200 Subject: [PATCH 08/25] Updated navigation and state changes --- src/components/StatePicker.js | 27 ++++++++++--------- src/hooks/useNavigationStorage.js | 13 +++++---- .../Navigators/RightModalNavigator.js | 2 -- src/pages/CountrySelectorPage.js | 2 +- src/pages/StateSelectorPage.js | 2 +- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js index 2659eb1600d1..fe51dce16f5a 100644 --- a/src/components/StatePicker.js +++ b/src/components/StatePicker.js @@ -1,6 +1,6 @@ -import React, {useState, useEffect, useCallback, useMemo} from 'react'; +import React, {useState, useCallback, useMemo} from 'react'; import {View} from 'react-native'; -import {useRoute} from '@react-navigation/native'; +import {useRoute, useFocusEffect} from '@react-navigation/native'; import PropTypes from 'prop-types'; import useNavigationStorage from '../hooks/useNavigationStorage'; import compose from '../libs/compose'; @@ -42,17 +42,18 @@ function BaseStatePicker(props) { const onInputChange = props.onInputChange; const translate = props.translate; - useEffect(() => { - if (!paramStateISO) { - return; - } - setStateTitle(paramStateISO); - - // Needed to call onInputChange, so Form can update the validation and values - onInputChange(paramStateISO); - // onInputChange isn't a stable function, so we can't add it to the dependency array - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [paramStateISO]); + useFocusEffect( + useCallback(() => { + const collectedState = collect(); + if (collectedState && collectedState !== stateTitle) { + setStateTitle(collectedState); + // Needed to call onInputChange, so Form can update the validation and values + onInputChange(paramStateISO); + } + // onInputChange isn't a stable function, so we can't add it to the dependency array + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [collect, stateTitle]), + ); const navigateToCountrySelector = useCallback(() => { // Try first using the route.path so I can keep any query params diff --git a/src/hooks/useNavigationStorage.js b/src/hooks/useNavigationStorage.js index 8fbf46a6fbfc..3fb3bb8b32b8 100644 --- a/src/hooks/useNavigationStorage.js +++ b/src/hooks/useNavigationStorage.js @@ -1,4 +1,4 @@ -import {useEffect} from 'react'; +import {useEffect, useCallback} from 'react'; const storage = new Map(); @@ -26,10 +26,13 @@ export default function useNavigationStorage(key = 'input', initialValue = null) storage.set(key, initialValue); }, [key, initialValue]); - const collect = () => storage.get(key); - const save = (value) => { - storage.set(key, value); - }; + const collect = useCallback(() => storage.get(key), [key]); + const save = useCallback( + (value) => { + storage.set(key, value); + }, + [key], + ); return [collect, save]; } diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js index a3f6c7547ba5..a0c7ec8492bb 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js @@ -95,12 +95,10 @@ function RigthModalNavigator() { /> { - Navigation.goBack(`${ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS}?countryISO=${selectedCountry.value}`, true); + Navigation.navigate(`${ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS}?countryISO=${selectedCountry.value}`, 'NAVIGATE'); }, []); return ( diff --git a/src/pages/StateSelectorPage.js b/src/pages/StateSelectorPage.js index 994493c9f5a2..1650a60aa02f 100644 --- a/src/pages/StateSelectorPage.js +++ b/src/pages/StateSelectorPage.js @@ -57,7 +57,7 @@ function StateSelectorPage(props) { const updateCountryState = useCallback( (selectedState) => { dispatch(selectedState.value); - Navigation.goBack(`${decodeURIComponent(route.params.backTo)}`, true); + Navigation.goBack(`${decodeURIComponent(route.params.backTo)}`); }, [dispatch, route.params.backTo], ); From c2e715e2b746134ef2f1c89d8695589b165c8d09 Mon Sep 17 00:00:00 2001 From: Edu Date: Tue, 11 Jul 2023 15:57:58 +0200 Subject: [PATCH 09/25] Fixed comments --- src/ROUTES.js | 8 +- src/components/AddressSearch/index.js | 24 ++-- src/components/CountryPicker.js | 87 +++++++------- src/components/OptionsSelectorWithSearch.js | 110 ------------------ src/components/StatePicker.js | 94 +++++++-------- .../AppNavigator/ModalStackNavigators.js | 6 +- .../Navigators/RightModalNavigator.js | 2 +- src/libs/Navigation/linkingConfig.js | 6 +- src/libs/actions/PersonalDetails.js | 8 +- src/pages/CountrySelectorPage.js | 86 +++++++++----- src/pages/StateSelectorPage.js | 85 ++++++++------ .../Profile/PersonalDetails/AddressPage.js | 63 +++++----- src/styles/styles.js | 4 + 13 files changed, 253 insertions(+), 330 deletions(-) delete mode 100644 src/components/OptionsSelectorWithSearch.js diff --git a/src/ROUTES.js b/src/ROUTES.js index 41992c4b900c..2bab6d7c5136 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -72,10 +72,10 @@ export default { getReportAttachmentRoute: (reportID, source) => `r/${reportID}/attachment?source=${encodeURI(source)}`, SELECT_YEAR: 'select-year', getYearSelectionRoute: (minYear, maxYear, currYear, backTo) => `select-year?min=${minYear}&max=${maxYear}&year=${currYear}&backTo=${backTo}`, - SETTINGS_SELECT_COUNTRY: 'select-country', - getCountrySelectionRoute: (countryISO, backTo) => `select-country?countryISO=${countryISO}&backTo=${backTo}`, - SETTINGS_USA_STATES: 'select-usa-states', - getUsaStateSelectionRoute: (stateISO, key, backTo) => `select-usa-states?stateISO=${stateISO}&key=${key}&backTo=${encodeURIComponent(backTo)}`, + SELECT_COUNTRY: 'select-country', + getCountrySelectionRoute: (key, backTo) => `select-country?key=${key}&backTo=${backTo}`, + SELECT_STATE: 'select-states', + getUsaStateSelectionRoute: (key, backTo) => `select-states?key=${key}&backTo=${encodeURIComponent(backTo)}`, /** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */ CONCIERGE: 'concierge', diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index bf702aafef23..e8f4549c0c64 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -48,12 +48,6 @@ const propTypes = { /** A callback function when the value of this field has changed */ onInputChange: PropTypes.func.isRequired, - /** A callback when a new country has changed */ - onCountryChange: PropTypes.func, - - /** A callback when a new state has changed */ - onStateChange: PropTypes.func, - /** Customize the TextInput container */ // eslint-disable-next-line react/forbid-prop-types containerStyles: PropTypes.arrayOf(PropTypes.object), @@ -72,6 +66,8 @@ const propTypes = { /** Maximum number of characters allowed in search input */ maxInputLength: PropTypes.number, + onAddressChange: PropTypes.func, + ...withLocalizePropTypes, }; @@ -79,8 +75,6 @@ const defaultProps = { inputID: undefined, shouldSaveDraft: false, onBlur: () => {}, - onCountryChange: () => {}, - onStateChange: () => {}, errorText: '', hint: '', value: undefined, @@ -94,6 +88,7 @@ const defaultProps = { zipCode: 'addressZipCode', }, maxInputLength: undefined, + onAddressChange: () => {}, }; // Do not convert to class component! It's been tried before and presents more challenges than it's worth. @@ -179,9 +174,6 @@ function AddressSearch(props) { const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country); if (isValidCountryCode) { values.country = country; - if (props.onCountryChange) { - props.onCountryChange(country); - } } // If the address is not in the US, use the full length state name since we're displaying the address's @@ -197,10 +189,6 @@ function AddressSearch(props) { values.state = stateFallback; } - if (props.onStateChange) { - props.onStateChange(isUS ? state : longStateName, isUS); - } - // Not all pages define the Address Line 2 field, so in that case we append any additional address details // (e.g. Apt #) to Address Line 1 if (subpremise && typeof props.renamedInputKeys.street2 === 'undefined') { @@ -212,8 +200,14 @@ function AddressSearch(props) { const inputKey = lodashGet(props.renamedInputKeys, key, key); props.onInputChange(value, inputKey); }); + if (props.onAddressChange) { + props.onAddressChange(); + } } else { props.onInputChange(values); + if (props.onAddressChange) { + props.onAddressChange(); + } } }; diff --git a/src/components/CountryPicker.js b/src/components/CountryPicker.js index 41e7e2565379..c67558267d70 100644 --- a/src/components/CountryPicker.js +++ b/src/components/CountryPicker.js @@ -1,12 +1,13 @@ -import React, {useCallback, useRef, useEffect} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; -import sizes from '../styles/variables'; +import {useFocusEffect} from '@react-navigation/native'; import styles from '../styles/styles'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import * as PersonalDetails from '../libs/actions/PersonalDetails'; +import useNavigationStorage from '../hooks/useNavigationStorage'; import Navigation from '../libs/Navigation/Navigation'; +import useLocalize from '../hooks/useLocalize'; import ROUTES from '../ROUTES'; import FormHelpMessage from './FormHelpMessage'; @@ -14,69 +15,77 @@ const propTypes = { /** The ISO code of the country */ countryISO: PropTypes.string, - /** The ISO selected from CountrySelector */ - selectedCountryISO: PropTypes.string, - /** Form Error description */ errorText: PropTypes.string, - ...withLocalizePropTypes, + /** Country to display */ + // eslint-disable-next-line react/require-default-props + value: PropTypes.string, + + /** ID of the input */ + inputID: PropTypes.string.isRequired, + + /** Callback to call when the input changes */ + onInputChange: PropTypes.func, }; const defaultProps = { countryISO: '', - selectedCountryISO: undefined, errorText: '', + onInputChange: () => {}, }; -function BaseCountryPicker(props) { - const countryTitle = useRef({title: '', iso: ''}); - const countryISO = props.countryISO; - const selectedCountryISO = props.selectedCountryISO; - const onInputChange = props.onInputChange; +const CountryPicker = React.forwardRef(({value, countryISO, inputID, errorText, onInputChange}, ref) => { + const {translate} = useLocalize(); + const countryValue = value || countryISO || ''; + const [collect, save] = useNavigationStorage(inputID, countryValue); useEffect(() => { - if (!selectedCountryISO || selectedCountryISO === countryTitle.current.iso) { + const savedCountry = collect(); + if (!countryValue || savedCountry === countryValue) { return; } - countryTitle.current = {title: PersonalDetails.getCountryNameBy(selectedCountryISO || countryISO), iso: selectedCountryISO || countryISO}; + save(countryValue); + }, [countryValue, collect, save]); - // Needed to call onInputChange, so Form can update the validation and values - onInputChange(countryTitle.current.iso); - }, [countryISO, selectedCountryISO, onInputChange]); + useFocusEffect( + useCallback(() => { + const savedCountry = collect(); + if (savedCountry && savedCountry !== countryValue) { + save(savedCountry); + // Needed to call onInputChange, so Form can update the validation and values + onInputChange(savedCountry); + } + // onInputChange isn't a stable function, so we can't add it to the dependency array + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [collect, countryValue]), + ); - const navigateToCountrySelector = useCallback(() => { - Navigation.navigate(ROUTES.getCountrySelectionRoute(selectedCountryISO || countryISO, Navigation.getActiveRoute())); - }, [countryISO, selectedCountryISO]); - const descStyle = countryTitle.current.title.length === 0 ? {fontSize: sizes.fontSizeNormal} : null; + const navigateToCountrySelector = () => { + Navigation.navigate(ROUTES.getCountrySelectionRoute(inputID, Navigation.getActiveRoute())); + }; + + const title = PersonalDetails.getCountryName(countryValue); + const descStyle = title.length === 0 ? styles.addressPickerDescription : null; return ( - + ); -} - -BaseCountryPicker.propTypes = propTypes; -BaseCountryPicker.defaultProps = defaultProps; - -const CountryPicker = React.forwardRef((props, ref) => ( - -)); +}); +CountryPicker.propTypes = propTypes; +CountryPicker.defaultProps = defaultProps; CountryPicker.displayName = 'CountryPicker'; -export default withLocalize(CountryPicker); +export default CountryPicker; diff --git a/src/components/OptionsSelectorWithSearch.js b/src/components/OptionsSelectorWithSearch.js deleted file mode 100644 index 2c5fe0fa660f..000000000000 --- a/src/components/OptionsSelectorWithSearch.js +++ /dev/null @@ -1,110 +0,0 @@ -import _ from 'underscore'; -import React, {useState} from 'react'; -import PropTypes from 'prop-types'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -import ScreenWrapper from './ScreenWrapper'; -import HeaderWithBackButton from './HeaderWithBackButton'; -import * as Expensicons from './Icon/Expensicons'; -import themeColors from '../styles/themes/default'; -import OptionsSelector from './OptionsSelector'; -import styles from '../styles/styles'; - -const propTypes = { - /** Title of the page */ - title: PropTypes.string.isRequired, - - /** Function to call when the back button is pressed */ - onBackButtonPress: PropTypes.func.isRequired, - - /** Text to display in the search input label */ - textSearchLabel: PropTypes.string.isRequired, - - /** Placeholder text to display in the search input */ - placeholder: PropTypes.string.isRequired, - - /** Function to call when a row is selected */ - onSelectRow: PropTypes.func.isRequired, - - /** Initial value to display in the search input */ - initialSearchValue: PropTypes.string, - - /** Initial option to display as selected */ - initialOption: PropTypes.string, - - data: PropTypes.arrayOf( - PropTypes.shape({ - /** Text to display for the option */ - text: PropTypes.string.isRequired, - - /** Value of the option */ - value: PropTypes.string.isRequired, - - /** Key to use for the option in the list */ - keyForList: PropTypes.string.isRequired, - }), - ).isRequired, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - initialSearchValue: '', - initialOption: '', -}; - -const greenCheckmark = {src: Expensicons.Checkmark, color: themeColors.success}; - -function filterOptions(searchValue, data) { - const trimmedSearchValue = searchValue.trim(); - if (trimmedSearchValue.length === 0) { - return []; - } - - return _.filter(data, (country) => country.text.toLowerCase().includes(searchValue.toLowerCase())); -} - -function OptionsSelectorWithSearch(props) { - const [searchValue, setSearchValue] = useState(props.initialSearchValue); - const translate = props.translate; - const initialOption = props.initialOption; - - const filteredData = filterOptions(searchValue, props.data); - const headerMessage = searchValue.trim() && !filteredData.length ? translate('common.noResultsFound') : ''; - - return ( - - {({safeAreaPaddingBottomStyle}) => ( - <> - - - - )} - - ); -} - -OptionsSelectorWithSearch.propTypes = propTypes; -OptionsSelectorWithSearch.defaultProps = defaultProps; -OptionsSelectorWithSearch.displayName = 'OptionsSelectorWithSearch'; - -export {greenCheckmark}; - -export default withLocalize(OptionsSelectorWithSearch); diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js index fe51dce16f5a..ccde1c1fb59a 100644 --- a/src/components/StatePicker.js +++ b/src/components/StatePicker.js @@ -1,106 +1,102 @@ -import React, {useState, useCallback, useMemo} from 'react'; +import React, {useCallback, useMemo, useEffect} from 'react'; import {View} from 'react-native'; import {useRoute, useFocusEffect} from '@react-navigation/native'; import PropTypes from 'prop-types'; import useNavigationStorage from '../hooks/useNavigationStorage'; -import compose from '../libs/compose'; -import withNavigation from './withNavigation'; -import sizes from '../styles/variables'; import styles from '../styles/styles'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import useLocalize from '../hooks/useLocalize'; import Navigation from '../libs/Navigation/Navigation'; import ROUTES from '../ROUTES'; import FormHelpMessage from './FormHelpMessage'; const propTypes = { - /** Current State from user address */ - stateISO: PropTypes.string, - /** Error text to display */ errorText: PropTypes.string, /** Default value to display */ defaultValue: PropTypes.string, - ...withLocalizePropTypes, + /** State to display */ + // eslint-disable-next-line react/require-default-props + value: PropTypes.string, + + /** ID of the input */ + inputID: PropTypes.string.isRequired, + + /** Callback to call when the input changes */ + onInputChange: PropTypes.func, }; const defaultProps = { - stateISO: '', errorText: '', defaultValue: '', + onInputChange: () => {}, }; -function BaseStatePicker(props) { +const StatePicker = React.forwardRef(({value, defaultValue, inputID, errorText, onInputChange}, ref) => { const route = useRoute(); - const defaultValue = props.defaultValue; - const [collect] = useNavigationStorage(props.inputID, defaultValue); - const stateISO = props.stateISO; - const paramStateISO = collect(); - const [stateTitle, setStateTitle] = useState(stateISO || paramStateISO); - const onInputChange = props.onInputChange; - const translate = props.translate; + const {translate} = useLocalize(); + const formValue = value || defaultValue || ''; + const [collect, save] = useNavigationStorage(inputID, formValue); + + useEffect(() => { + const collectedState = collect(); + if (!formValue || collectedState === formValue) { + return; + } + save(formValue); + }, [formValue, collect, save]); useFocusEffect( useCallback(() => { const collectedState = collect(); - if (collectedState && collectedState !== stateTitle) { - setStateTitle(collectedState); + if (collectedState && collectedState !== formValue) { + save(collectedState); // Needed to call onInputChange, so Form can update the validation and values - onInputChange(paramStateISO); + onInputChange(collectedState); } // onInputChange isn't a stable function, so we can't add it to the dependency array // eslint-disable-next-line react-hooks/exhaustive-deps - }, [collect, stateTitle]), + }, [collect, formValue]), ); - const navigateToCountrySelector = useCallback(() => { + const navigateToCountrySelector = () => { // Try first using the route.path so I can keep any query params - Navigation.navigate(ROUTES.getUsaStateSelectionRoute(stateTitle || stateISO, props.inputID, route.path || Navigation.getActiveRoute())); - }, [stateTitle, stateISO, route.path, props.inputID]); + Navigation.navigate(ROUTES.getUsaStateSelectionRoute(inputID, route.path || Navigation.getActiveRoute())); + }; const title = useMemo(() => { const allStates = translate('allStates'); - if (!stateTitle) { - return defaultValue ? allStates[defaultValue].stateName : ''; - } - if (allStates[stateTitle]) { - return allStates[stateTitle].stateName; + + if (allStates[formValue]) { + return allStates[formValue].stateName; } - return stateTitle; - }, [translate, stateTitle, defaultValue]); - const descStyle = title.length === 0 ? {fontSize: sizes.fontSizeNormal} : null; + return ''; + }, [translate, formValue]); + + const descStyle = title.length === 0 ? styles.addressPickerDescription : null; return ( - + ); -} - -BaseStatePicker.propTypes = propTypes; -BaseStatePicker.defaultProps = defaultProps; - -const StatePicker = React.forwardRef((props, ref) => ( - -)); +}); +StatePicker.propTypes = propTypes; +StatePicker.defaultProps = defaultProps; StatePicker.displayName = 'StatePicker'; -export default compose(withLocalize, withNavigation)(StatePicker); +export default StatePicker; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index d627d768a753..271c0fcbe84a 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -729,10 +729,10 @@ const CountrySelectorStackNavigator = createModalStackNavigator([ const UsaStateSelectorStackNavigator = createModalStackNavigator([ { getComponent: () => { - const usaStateSelectorPage = require('../../../pages/StateSelectorPage').default; - return usaStateSelectorPage; + const stateSelectorPage = require('../../../pages/StateSelectorPage').default; + return stateSelectorPage; }, - name: 'CountrySelector_Root', + name: 'StateSelector_Root', }, ]); diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js index a0c7ec8492bb..e4884b4bd185 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js @@ -98,7 +98,7 @@ function RigthModalNavigator() { component={ModalStackNavigators.CountrySelectorStackNavigator} /> country.text.toLowerCase().includes(searchValue.toLowerCase())); +} + function CountrySelectorPage(props) { - const translate = props.translate; + const {translate} = useLocalize(); const route = props.route; - const currentCountry = route.params.countryISO || lodashGet(props.privatePersonalDetails, 'address.country'); - const selectedSearchCountry = PersonalDetails.getCountryNameBy(currentCountry); + const [collect, dispatch] = useNavigationStorage(route.params.key, null); + const currentCountry = collect(); + const selectedSearchCountry = PersonalDetails.getCountryName(currentCountry); + const [searchValue, setSearchValue] = useState(selectedSearchCountry); const countries = useMemo( () => @@ -49,26 +60,40 @@ function CountrySelectorPage(props) { value: countryISO, keyForList: countryISO, text: countryName, - customIcon: currentCountry === countryISO ? greenCheckmark : undefined, + isSelected: currentCountry === countryISO, })), [translate, currentCountry], ); - const updateCountry = useCallback((selectedCountry) => { - Navigation.navigate(`${ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS}?countryISO=${selectedCountry.value}`, 'NAVIGATE'); - }, []); + const filteredData = filterOptions(searchValue, countries); + const headerMessage = searchValue.trim() && !filteredData.length ? translate('common.noResultsFound') : ''; + + const updateCountry = (selectedCountry) => { + dispatch(selectedCountry.value); + Navigation.navigate(`${ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS}`, 'NAVIGATE'); + }; return ( - Navigation.goBack(`${route.params.backTo}`)} - textSearchLabel={translate('common.country')} - placeholder={translate('pronounsPage.placeholderText')} - onSelectRow={updateCountry} - initialSearchValue={selectedSearchCountry} - initialOption={currentCountry} - /> + + Navigation.goBack(`${route.params.backTo}`)} + /> + + ); } @@ -76,11 +101,8 @@ CountrySelectorPage.propTypes = propTypes; CountrySelectorPage.defaultProps = defaultProps; CountrySelectorPage.displayName = 'CountrySelectorPage'; -export default compose( - withLocalize, - withOnyx({ - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, - }), -)(CountrySelectorPage); +export default withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, +})(CountrySelectorPage); diff --git a/src/pages/StateSelectorPage.js b/src/pages/StateSelectorPage.js index 1650a60aa02f..7adc4c25fdce 100644 --- a/src/pages/StateSelectorPage.js +++ b/src/pages/StateSelectorPage.js @@ -1,18 +1,20 @@ import _ from 'underscore'; -import React, {useCallback, useMemo} from 'react'; +import React, {useState, useMemo} from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; +import ScreenWrapper from '../components/ScreenWrapper'; +import HeaderWithBackButton from '../components/HeaderWithBackButton'; +import SelectionListRadio from '../components/SelectionListRadio'; import useNavigationStorage from '../hooks/useNavigationStorage'; -import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; import Navigation from '../libs/Navigation/Navigation'; -import compose from '../libs/compose'; +import useLocalize from '../hooks/useLocalize'; import ONYXKEYS from '../ONYXKEYS'; -import OptionsSelectorWithSearch, {greenCheckmark} from '../components/OptionsSelectorWithSearch'; const propTypes = { route: PropTypes.shape({ params: PropTypes.shape({ backTo: PropTypes.string, + key: PropTypes.string, }), }).isRequired, @@ -23,8 +25,6 @@ const propTypes = { state: PropTypes.string, }), }), - - ...withLocalizePropTypes, }; const defaultProps = { @@ -35,13 +35,23 @@ const defaultProps = { }, }; +function filterOptions(searchValue, data) { + const trimmedSearchValue = searchValue.trim(); + if (trimmedSearchValue.length === 0) { + return []; + } + + return _.filter(data, (country) => country.text.toLowerCase().includes(searchValue.toLowerCase())); +} + function StateSelectorPage(props) { - const translate = props.translate; + const {translate} = useLocalize(); const route = props.route; - const [collect, dispatch] = useNavigationStorage(route.params.key, null, 'page selector'); + const [collect, dispatch] = useNavigationStorage(route.params.key, null); const currentCountryState = collect(); const allStates = translate('allStates'); const selectedSearchState = !_.isEmpty(currentCountryState) ? allStates[currentCountryState].stateName : ''; + const [searchValue, setSearchValue] = useState(selectedSearchState); const countryStates = useMemo( () => @@ -49,30 +59,40 @@ function StateSelectorPage(props) { value: state.stateISO, keyForList: state.stateISO, text: state.stateName, - customIcon: currentCountryState === state.stateISO ? greenCheckmark : undefined, + isSelected: currentCountryState === state.stateISO, })), [translate, currentCountryState], ); - const updateCountryState = useCallback( - (selectedState) => { - dispatch(selectedState.value); - Navigation.goBack(`${decodeURIComponent(route.params.backTo)}`); - }, - [dispatch, route.params.backTo], - ); + const filteredData = filterOptions(searchValue, countryStates); + const headerMessage = searchValue.trim() && !filteredData.length ? translate('common.noResultsFound') : ''; + + const updateCountryState = (selectedState) => { + dispatch(selectedState.value); + Navigation.goBack(`${decodeURIComponent(route.params.backTo)}`); + }; return ( - Navigation.goBack(`${route.params.backTo}`)} - textSearchLabel={translate('common.state')} - placeholder={translate('pronounsPage.placeholderText')} - onSelectRow={updateCountryState} - initialSearchValue={selectedSearchState} - initialOption={currentCountryState} - data={countryStates} - /> + + Navigation.goBack(`${route.params.backTo}`)} + /> + + ); } @@ -80,11 +100,8 @@ StateSelectorPage.propTypes = propTypes; StateSelectorPage.defaultProps = defaultProps; StateSelectorPage.displayName = 'StateSelectorPage'; -export default compose( - withLocalize, - withOnyx({ - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, - }), -)(StateSelectorPage); +export default withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, +})(StateSelectorPage); diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index 9fce5168d9fc..14de92e2936b 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -1,13 +1,13 @@ import lodashGet from 'lodash/get'; import _ from 'underscore'; -import React, {useState, useCallback, useEffect} from 'react'; +import React, {useRef, useState, useCallback} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; import {withOnyx} from 'react-native-onyx'; +import {useFocusEffect} from '@react-navigation/native'; import ScreenWrapper from '../../../../components/ScreenWrapper'; import HeaderWithBackButton from '../../../../components/HeaderWithBackButton'; -import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize'; import Form from '../../../../components/Form'; import ONYXKEYS from '../../../../ONYXKEYS'; import CONST from '../../../../CONST'; @@ -15,13 +15,13 @@ import TextInput from '../../../../components/TextInput'; import styles from '../../../../styles/styles'; import * as PersonalDetails from '../../../../libs/actions/PersonalDetails'; import * as ValidationUtils from '../../../../libs/ValidationUtils'; -import compose from '../../../../libs/compose'; +import useNavigationStorage from '../../../../hooks/useNavigationStorage'; import AddressSearch from '../../../../components/AddressSearch'; import CountryPicker from '../../../../components/CountryPicker'; import StatePicker from '../../../../components/StatePicker'; import Navigation from '../../../../libs/Navigation/Navigation'; import ROUTES from '../../../../ROUTES'; -import useNavigationStorage from '../../../../hooks/useNavigationStorage'; +import useLocalize from '../../../../hooks/useLocalize'; const propTypes = { /* Onyx Props */ @@ -37,8 +37,6 @@ const propTypes = { country: PropTypes.string, }), }), - - ...withLocalizePropTypes, }; const defaultProps = { @@ -61,30 +59,32 @@ function updateAddress(values) { PersonalDetails.updateAddress(values.addressLine1.trim(), values.addressLine2.trim(), values.city.trim(), values.state.trim(), values.zipPostCode.trim().toUpperCase(), values.country); } -function AddressPage({translate, route, navigation, privatePersonalDetails}) { - const [countryISO, setCountryISO] = useState(PersonalDetails.getCountryISO(lodashGet(privatePersonalDetails, 'address.country')) || CONST.COUNTRY.US); - const [, saveIntoStorage] = useNavigationStorage('state'); - const isUSAForm = countryISO === CONST.COUNTRY.US; +function AddressPage({privatePersonalDetails}) { + const countryISO = useRef(PersonalDetails.getCountryISO(lodashGet(privatePersonalDetails, 'address.country')) || CONST.COUNTRY.US).current; + const {translate} = useLocalize(); + const [collect] = useNavigationStorage('country'); + const [currentCountry, setCurrentCountry] = useState(collect() || countryISO); + const isUSAForm = currentCountry === CONST.COUNTRY.US; - const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [countryISO, 'samples'], ''); + const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [currentCountry, 'samples'], ''); const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); const address = lodashGet(privatePersonalDetails, 'address') || {}; const [street1, street2] = (address.street || '').split('\n'); - const onCountryUpdate = (newCountry) => { - setCountryISO(newCountry); - }; - const onCountryStateUpdate = (selectedCountryState) => { - saveIntoStorage(selectedCountryState); + const onAddressChange = () => { + const savedCountry = collect(); + setCurrentCountry(savedCountry); }; - useEffect(() => { - const currentCountryISO = lodashGet(route, 'params.countryISO'); - if (currentCountryISO) { - setCountryISO(currentCountryISO); - } - }, [route, navigation]); + useFocusEffect( + useCallback(() => { + const savedCountry = collect(); + if (savedCountry && savedCountry !== currentCountry) { + setCurrentCountry(savedCountry); + } + }, [collect, currentCountry]), + ); /** * @param {Function} translate - translate function @@ -152,8 +152,7 @@ function AddressPage({translate, route, navigation, privatePersonalDetails}) { label={translate('common.addressLine', {lineNumber: 1})} defaultValue={street1 || ''} isLimitedToUSA={false} - onCountryChange={onCountryUpdate} - onStateChange={onCountryStateUpdate} + onAddressChange={onAddressChange} renamedInputKeys={{ street: 'addressLine1', street2: 'addressLine2', @@ -176,7 +175,6 @@ function AddressPage({translate, route, navigation, privatePersonalDetails}) { @@ -185,7 +183,7 @@ function AddressPage({translate, route, navigation, privatePersonalDetails}) { ) : ( @@ -220,11 +218,8 @@ function AddressPage({translate, route, navigation, privatePersonalDetails}) { AddressPage.propTypes = propTypes; AddressPage.defaultProps = defaultProps; -export default compose( - withLocalize, - withOnyx({ - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, - }), -)(AddressPage); +export default withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, +})(AddressPage); diff --git a/src/styles/styles.js b/src/styles/styles.js index fa0adbcc7d1a..0e8074fae602 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3508,6 +3508,10 @@ const styles = { marginTop: 15, textAlign: 'center', }, + + addressPickerDescription: { + fontSize: variables.fontSizeSmall, + }, }; export default styles; From b13ac10cc0a231c44e3d065eed484ad54354b403 Mon Sep 17 00:00:00 2001 From: Edu Date: Fri, 14 Jul 2023 16:41:16 +0200 Subject: [PATCH 10/25] Removed Navigation fro State and Country pages --- src/ROUTES.js | 4 - src/components/AddressSearch/index.js | 9 -- src/components/CountryPicker.js | 91 --------------- .../CountryPicker/CountrySelectorModal.js} | 83 ++++++-------- src/components/CountryPicker/index.js | 94 +++++++++++++++ src/components/StatePicker.js | 102 ----------------- .../StatePicker/StateSelectorModal.js | 94 +++++++++++++++ src/components/StatePicker/index.js | 92 +++++++++++++++ src/hooks/useNavigationStorage.js | 40 ------- .../AppNavigator/ModalStackNavigators.js | 22 ---- .../Navigators/RightModalNavigator.js | 8 -- src/libs/Navigation/linkingConfig.js | 10 -- src/pages/ReimbursementAccount/CompanyStep.js | 7 -- src/pages/StateSelectorPage.js | 107 ------------------ .../Profile/PersonalDetails/AddressPage.js | 25 +--- .../PersonalDetailsInitialPage.js | 5 - src/styles/styles.js | 2 +- 17 files changed, 318 insertions(+), 477 deletions(-) delete mode 100644 src/components/CountryPicker.js rename src/{pages/CountrySelectorPage.js => components/CountryPicker/CountrySelectorModal.js} (50%) create mode 100644 src/components/CountryPicker/index.js delete mode 100644 src/components/StatePicker.js create mode 100644 src/components/StatePicker/StateSelectorModal.js create mode 100644 src/components/StatePicker/index.js delete mode 100644 src/hooks/useNavigationStorage.js delete mode 100644 src/pages/StateSelectorPage.js diff --git a/src/ROUTES.js b/src/ROUTES.js index 06aa81b32b46..cf11c0b27a93 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -73,10 +73,6 @@ export default { getReportAttachmentRoute: (reportID, source) => `r/${reportID}/attachment?source=${encodeURI(source)}`, SELECT_YEAR: 'select-year', getYearSelectionRoute: (minYear, maxYear, currYear, backTo) => `select-year?min=${minYear}&max=${maxYear}&year=${currYear}&backTo=${backTo}`, - SELECT_COUNTRY: 'select-country', - getCountrySelectionRoute: (key, backTo) => `select-country?key=${key}&backTo=${backTo}`, - SELECT_STATE: 'select-states', - getUsaStateSelectionRoute: (key, backTo) => `select-states?key=${key}&backTo=${encodeURIComponent(backTo)}`, /** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */ CONCIERGE: 'concierge', diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index e8f4549c0c64..afdfd2ff3512 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -66,8 +66,6 @@ const propTypes = { /** Maximum number of characters allowed in search input */ maxInputLength: PropTypes.number, - onAddressChange: PropTypes.func, - ...withLocalizePropTypes, }; @@ -88,7 +86,6 @@ const defaultProps = { zipCode: 'addressZipCode', }, maxInputLength: undefined, - onAddressChange: () => {}, }; // Do not convert to class component! It's been tried before and presents more challenges than it's worth. @@ -200,14 +197,8 @@ function AddressSearch(props) { const inputKey = lodashGet(props.renamedInputKeys, key, key); props.onInputChange(value, inputKey); }); - if (props.onAddressChange) { - props.onAddressChange(); - } } else { props.onInputChange(values); - if (props.onAddressChange) { - props.onAddressChange(); - } } }; diff --git a/src/components/CountryPicker.js b/src/components/CountryPicker.js deleted file mode 100644 index c67558267d70..000000000000 --- a/src/components/CountryPicker.js +++ /dev/null @@ -1,91 +0,0 @@ -import React, {useCallback, useEffect} from 'react'; -import {View} from 'react-native'; -import PropTypes from 'prop-types'; -import {useFocusEffect} from '@react-navigation/native'; -import styles from '../styles/styles'; -import MenuItemWithTopDescription from './MenuItemWithTopDescription'; -import * as PersonalDetails from '../libs/actions/PersonalDetails'; -import useNavigationStorage from '../hooks/useNavigationStorage'; -import Navigation from '../libs/Navigation/Navigation'; -import useLocalize from '../hooks/useLocalize'; -import ROUTES from '../ROUTES'; -import FormHelpMessage from './FormHelpMessage'; - -const propTypes = { - /** The ISO code of the country */ - countryISO: PropTypes.string, - - /** Form Error description */ - errorText: PropTypes.string, - - /** Country to display */ - // eslint-disable-next-line react/require-default-props - value: PropTypes.string, - - /** ID of the input */ - inputID: PropTypes.string.isRequired, - - /** Callback to call when the input changes */ - onInputChange: PropTypes.func, -}; - -const defaultProps = { - countryISO: '', - errorText: '', - onInputChange: () => {}, -}; - -const CountryPicker = React.forwardRef(({value, countryISO, inputID, errorText, onInputChange}, ref) => { - const {translate} = useLocalize(); - const countryValue = value || countryISO || ''; - const [collect, save] = useNavigationStorage(inputID, countryValue); - - useEffect(() => { - const savedCountry = collect(); - if (!countryValue || savedCountry === countryValue) { - return; - } - save(countryValue); - }, [countryValue, collect, save]); - - useFocusEffect( - useCallback(() => { - const savedCountry = collect(); - if (savedCountry && savedCountry !== countryValue) { - save(savedCountry); - // Needed to call onInputChange, so Form can update the validation and values - onInputChange(savedCountry); - } - // onInputChange isn't a stable function, so we can't add it to the dependency array - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [collect, countryValue]), - ); - - const navigateToCountrySelector = () => { - Navigation.navigate(ROUTES.getCountrySelectionRoute(inputID, Navigation.getActiveRoute())); - }; - - const title = PersonalDetails.getCountryName(countryValue); - const descStyle = title.length === 0 ? styles.addressPickerDescription : null; - return ( - - - - - - - ); -}); - -CountryPicker.propTypes = propTypes; -CountryPicker.defaultProps = defaultProps; -CountryPicker.displayName = 'CountryPicker'; - -export default CountryPicker; diff --git a/src/pages/CountrySelectorPage.js b/src/components/CountryPicker/CountrySelectorModal.js similarity index 50% rename from src/pages/CountrySelectorPage.js rename to src/components/CountryPicker/CountrySelectorModal.js index 8c2e40457567..b39408d38da3 100644 --- a/src/pages/CountrySelectorPage.js +++ b/src/components/CountryPicker/CountrySelectorModal.js @@ -1,40 +1,30 @@ import _ from 'underscore'; import React, {useState, useMemo} from 'react'; import PropTypes from 'prop-types'; -import {withOnyx} from 'react-native-onyx'; -import * as PersonalDetails from '../libs/actions/PersonalDetails'; -import ROUTES from '../ROUTES'; -import Navigation from '../libs/Navigation/Navigation'; -import useLocalize from '../hooks/useLocalize'; -import ONYXKEYS from '../ONYXKEYS'; -import ScreenWrapper from '../components/ScreenWrapper'; -import HeaderWithBackButton from '../components/HeaderWithBackButton'; -import SelectionListRadio from '../components/SelectionListRadio'; -import useNavigationStorage from '../hooks/useNavigationStorage'; +import * as PersonalDetails from '../../libs/actions/PersonalDetails'; +import CONST from '../../CONST'; +import useLocalize from '../../hooks/useLocalize'; +import HeaderWithBackButton from '../HeaderWithBackButton'; +import SelectionListRadio from '../SelectionListRadio'; +import Modal from '../Modal'; const propTypes = { - route: PropTypes.shape({ - params: PropTypes.shape({ - backTo: PropTypes.string, - key: PropTypes.string, - }), - }).isRequired, + /** Whether the modal is visible */ + isVisible: PropTypes.bool.isRequired, - /** User's private personal details */ - privatePersonalDetails: PropTypes.shape({ - /** User's home address */ - address: PropTypes.shape({ - country: PropTypes.string, - }), - }), + /** Country value selected */ + currentCountry: PropTypes.string.isRequired, + + /** Function to call when the user selects a Country */ + onCountrySelected: PropTypes.func, + + /** Function to call when the user closes the Country modal */ + onClose: PropTypes.func, }; const defaultProps = { - privatePersonalDetails: { - address: { - state: '', - }, - }, + onClose: () => {}, + onStateSelected: () => {}, }; function filterOptions(searchValue, data) { @@ -46,11 +36,8 @@ function filterOptions(searchValue, data) { return _.filter(data, (country) => country.text.toLowerCase().includes(searchValue.toLowerCase())); } -function CountrySelectorPage(props) { +function CountrySelectorModal({currentCountry, isVisible, onClose, onCountrySelected}) { const {translate} = useLocalize(); - const route = props.route; - const [collect, dispatch] = useNavigationStorage(route.params.key, null); - const currentCountry = collect(); const selectedSearchCountry = PersonalDetails.getCountryName(currentCountry); const [searchValue, setSearchValue] = useState(selectedSearchCountry); @@ -68,17 +55,19 @@ function CountrySelectorPage(props) { const filteredData = filterOptions(searchValue, countries); const headerMessage = searchValue.trim() && !filteredData.length ? translate('common.noResultsFound') : ''; - const updateCountry = (selectedCountry) => { - dispatch(selectedCountry.value); - Navigation.navigate(`${ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS}`, 'NAVIGATE'); - }; - return ( - + Navigation.goBack(`${route.params.backTo}`)} + onBackButtonPress={onClose} /> - + ); } -CountrySelectorPage.propTypes = propTypes; -CountrySelectorPage.defaultProps = defaultProps; -CountrySelectorPage.displayName = 'CountrySelectorPage'; +CountrySelectorModal.propTypes = propTypes; +CountrySelectorModal.defaultProps = defaultProps; +CountrySelectorModal.displayName = 'CountrySelectorModal'; -export default withOnyx({ - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, -})(CountrySelectorPage); +export default CountrySelectorModal; diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js new file mode 100644 index 000000000000..ca34b030b2f4 --- /dev/null +++ b/src/components/CountryPicker/index.js @@ -0,0 +1,94 @@ +import React, {useState, useEffect} from 'react'; +import {View} from 'react-native'; +import PropTypes from 'prop-types'; +import styles from '../../styles/styles'; +import MenuItemWithTopDescription from '../MenuItemWithTopDescription'; +import * as PersonalDetails from '../../libs/actions/PersonalDetails'; +import useLocalize from '../../hooks/useLocalize'; +import CountrySelectorModal from './CountrySelectorModal'; +import FormHelpMessage from '../FormHelpMessage'; + +const propTypes = { + /** The ISO code of the country */ + countryISO: PropTypes.string, + + /** Form Error description */ + errorText: PropTypes.string, + + /** Country to display */ + // eslint-disable-next-line react/require-default-props + value: PropTypes.string, + + /** ID of the input */ + inputID: PropTypes.string.isRequired, + + /** Callback to call when the input changes */ + onInputChange: PropTypes.func, + + /** Callback to call when the Country is updated */ + onCountryUpdated: PropTypes.func, +}; + +const defaultProps = { + countryISO: '', + errorText: '', + onInputChange: () => {}, + onCountryUpdated: () => {}, +}; + +const CountryPicker = React.forwardRef(({value, countryISO, errorText, onCountryUpdated, onInputChange}, ref) => { + const {translate} = useLocalize(); + const [isPickerVisible, setIsPickerVisible] = useState(false); + const countryValue = value || countryISO || ''; + + const showPickerModal = () => { + setIsPickerVisible(true); + }; + + const hidePickerModal = () => { + setIsPickerVisible(false); + }; + + const onStateSelected = (state) => { + onInputChange(state.value); + hidePickerModal(); + }; + + useEffect(() => { + if (onCountryUpdated && countryValue.length > 0) { + onCountryUpdated(countryValue); + } + }, [countryValue]); + + const title = PersonalDetails.getCountryName(countryValue); + const descStyle = title.length === 0 ? styles.addressPickerDescription : null; + + return ( + + + + + + + + ); +}); + +CountryPicker.propTypes = propTypes; +CountryPicker.defaultProps = defaultProps; +CountryPicker.displayName = 'CountryPicker'; + +export default CountryPicker; diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js deleted file mode 100644 index ccde1c1fb59a..000000000000 --- a/src/components/StatePicker.js +++ /dev/null @@ -1,102 +0,0 @@ -import React, {useCallback, useMemo, useEffect} from 'react'; -import {View} from 'react-native'; -import {useRoute, useFocusEffect} from '@react-navigation/native'; -import PropTypes from 'prop-types'; -import useNavigationStorage from '../hooks/useNavigationStorage'; -import styles from '../styles/styles'; -import MenuItemWithTopDescription from './MenuItemWithTopDescription'; -import useLocalize from '../hooks/useLocalize'; -import Navigation from '../libs/Navigation/Navigation'; -import ROUTES from '../ROUTES'; -import FormHelpMessage from './FormHelpMessage'; - -const propTypes = { - /** Error text to display */ - errorText: PropTypes.string, - - /** Default value to display */ - defaultValue: PropTypes.string, - - /** State to display */ - // eslint-disable-next-line react/require-default-props - value: PropTypes.string, - - /** ID of the input */ - inputID: PropTypes.string.isRequired, - - /** Callback to call when the input changes */ - onInputChange: PropTypes.func, -}; - -const defaultProps = { - errorText: '', - defaultValue: '', - onInputChange: () => {}, -}; - -const StatePicker = React.forwardRef(({value, defaultValue, inputID, errorText, onInputChange}, ref) => { - const route = useRoute(); - const {translate} = useLocalize(); - const formValue = value || defaultValue || ''; - const [collect, save] = useNavigationStorage(inputID, formValue); - - useEffect(() => { - const collectedState = collect(); - if (!formValue || collectedState === formValue) { - return; - } - save(formValue); - }, [formValue, collect, save]); - - useFocusEffect( - useCallback(() => { - const collectedState = collect(); - if (collectedState && collectedState !== formValue) { - save(collectedState); - // Needed to call onInputChange, so Form can update the validation and values - onInputChange(collectedState); - } - // onInputChange isn't a stable function, so we can't add it to the dependency array - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [collect, formValue]), - ); - - const navigateToCountrySelector = () => { - // Try first using the route.path so I can keep any query params - Navigation.navigate(ROUTES.getUsaStateSelectionRoute(inputID, route.path || Navigation.getActiveRoute())); - }; - - const title = useMemo(() => { - const allStates = translate('allStates'); - - if (allStates[formValue]) { - return allStates[formValue].stateName; - } - - return ''; - }, [translate, formValue]); - - const descStyle = title.length === 0 ? styles.addressPickerDescription : null; - - return ( - - - - - - - ); -}); - -StatePicker.propTypes = propTypes; -StatePicker.defaultProps = defaultProps; -StatePicker.displayName = 'StatePicker'; - -export default StatePicker; diff --git a/src/components/StatePicker/StateSelectorModal.js b/src/components/StatePicker/StateSelectorModal.js new file mode 100644 index 000000000000..6ce388f7905e --- /dev/null +++ b/src/components/StatePicker/StateSelectorModal.js @@ -0,0 +1,94 @@ +import _ from 'underscore'; +import React, {useState, useMemo} from 'react'; +import PropTypes from 'prop-types'; +import CONST from '../../CONST'; +import Modal from '../Modal'; +import HeaderWithBackButton from '../HeaderWithBackButton'; +import SelectionListRadio from '../SelectionListRadio'; +import useLocalize from '../../hooks/useLocalize'; + +const propTypes = { + /** Whether the modal is visible */ + isVisible: PropTypes.bool.isRequired, + + /** State value selected */ + currentState: PropTypes.string.isRequired, + + /** Function to call when the user selects a State */ + onStateSelected: PropTypes.func, + + /** Function to call when the user closes the State modal */ + onClose: PropTypes.func, +}; + +const defaultProps = { + onClose: () => {}, + onStateSelected: () => {}, +}; + +function filterOptions(searchValue, data) { + const trimmedSearchValue = searchValue.trim(); + if (trimmedSearchValue.length === 0) { + return []; + } + + return _.filter(data, (country) => country.text.toLowerCase().includes(searchValue.toLowerCase())); +} + +function StateSelectorModal({currentState, isVisible, onClose, onStateSelected}) { + const {translate} = useLocalize(); + const allStates = translate('allStates'); + const selectedSearchState = !_.isEmpty(currentState) ? allStates[currentState].stateName : ''; + + const [searchValue, setSearchValue] = useState(selectedSearchState); + + const countryStates = useMemo( + () => + _.map(translate('allStates'), (state) => ({ + value: state.stateISO, + keyForList: state.stateISO, + text: state.stateName, + isSelected: currentState === state.stateISO, + })), + [translate, currentState], + ); + + const filteredData = filterOptions(searchValue, countryStates); + const headerMessage = searchValue.trim() && !filteredData.length ? translate('common.noResultsFound') : ''; + + return ( + + + + + ); +} + +StateSelectorModal.propTypes = propTypes; +StateSelectorModal.defaultProps = defaultProps; +StateSelectorModal.displayName = 'StateSelectorModal'; + +export default StateSelectorModal; diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js new file mode 100644 index 000000000000..edb1845bbebb --- /dev/null +++ b/src/components/StatePicker/index.js @@ -0,0 +1,92 @@ +import React, {useMemo, useState} from 'react'; +import {View} from 'react-native'; +import PropTypes from 'prop-types'; +import styles from '../../styles/styles'; +import MenuItemWithTopDescription from '../MenuItemWithTopDescription'; +import useLocalize from '../../hooks/useLocalize'; +import FormHelpMessage from '../FormHelpMessage'; +import StateSelectorModal from './StateSelectorModal'; + +const propTypes = { + /** Error text to display */ + errorText: PropTypes.string, + + /** Default value to display */ + defaultValue: PropTypes.string, + + /** State to display */ + // eslint-disable-next-line react/require-default-props + value: PropTypes.string, + + /** ID of the input */ + inputID: PropTypes.string.isRequired, + + /** Callback to call when the input changes */ + onInputChange: PropTypes.func, +}; + +const defaultProps = { + errorText: '', + defaultValue: '', + onInputChange: () => {}, +}; + +const StatePicker = React.forwardRef(({value, defaultValue, errorText, onInputChange}, ref) => { + const {translate} = useLocalize(); + const [isPickerVisible, setIsPickerVisible] = useState(false); + const formValue = value || defaultValue || ''; + + const title = useMemo(() => { + const allStates = translate('allStates'); + + if (allStates[formValue]) { + return allStates[formValue].stateName; + } + + return ''; + }, [translate, formValue]); + + const showPickerModal = () => { + setIsPickerVisible(true); + }; + + const hidePickerModal = () => { + setIsPickerVisible(false); + }; + + const onStateSelected = (state) => { + onInputChange(state.value); + hidePickerModal(); + }; + + const descStyle = title.length === 0 ? styles.addressPickerDescription : null; + + return ( + + + + + + + + ); +}); + +StatePicker.propTypes = propTypes; +StatePicker.defaultProps = defaultProps; +StatePicker.displayName = 'StatePicker'; + +export default StatePicker; diff --git a/src/hooks/useNavigationStorage.js b/src/hooks/useNavigationStorage.js deleted file mode 100644 index 3fb3bb8b32b8..000000000000 --- a/src/hooks/useNavigationStorage.js +++ /dev/null @@ -1,40 +0,0 @@ -import {useEffect, useCallback} from 'react'; - -const storage = new Map(); - -/** - * Clears the navigation storage, call it before to navigate to a new page - */ -function clearNavigationStorage() { - storage.clear(); -} - -/** - * Saves a value into the navigation storage. Use it for class components - * @param {String} key - * @param {any} value - */ -function saveIntoStorage(key, value) { - storage.set(key, value); -} - -export default function useNavigationStorage(key = 'input', initialValue = null) { - useEffect(() => { - if (!initialValue || storage.has(key)) { - return; - } - storage.set(key, initialValue); - }, [key, initialValue]); - - const collect = useCallback(() => storage.get(key), [key]); - const save = useCallback( - (value) => { - storage.set(key, value); - }, - [key], - ); - - return [collect, save]; -} - -export {clearNavigationStorage, saveIntoStorage}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index aa4133a61b84..e12995db48ce 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -707,26 +707,6 @@ const FlagCommentStackNavigator = createModalStackNavigator([ }, ]); -const CountrySelectorStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const CountrySelectorPage = require('../../../pages/CountrySelectorPage').default; - return CountrySelectorPage; - }, - name: 'CountrySelector_Root', - }, -]); - -const UsaStateSelectorStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const stateSelectorPage = require('../../../pages/StateSelectorPage').default; - return stateSelectorPage; - }, - name: 'StateSelector_Root', - }, -]); - const EditRequestStackNavigator = createModalStackNavigator([ { getComponent: () => { @@ -757,7 +737,5 @@ export { ReimbursementAccountModalStackNavigator, WalletStatementStackNavigator, FlagCommentStackNavigator, - CountrySelectorStackNavigator, - UsaStateSelectorStackNavigator, EditRequestStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js index 3f4a27c47678..7d6d4cb2709c 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js @@ -89,14 +89,6 @@ function RigthModalNavigator() { name="Flag_Comment" component={ModalStackNavigators.FlagCommentStackNavigator} /> - - diff --git a/src/pages/StateSelectorPage.js b/src/pages/StateSelectorPage.js deleted file mode 100644 index 7adc4c25fdce..000000000000 --- a/src/pages/StateSelectorPage.js +++ /dev/null @@ -1,107 +0,0 @@ -import _ from 'underscore'; -import React, {useState, useMemo} from 'react'; -import PropTypes from 'prop-types'; -import {withOnyx} from 'react-native-onyx'; -import ScreenWrapper from '../components/ScreenWrapper'; -import HeaderWithBackButton from '../components/HeaderWithBackButton'; -import SelectionListRadio from '../components/SelectionListRadio'; -import useNavigationStorage from '../hooks/useNavigationStorage'; -import Navigation from '../libs/Navigation/Navigation'; -import useLocalize from '../hooks/useLocalize'; -import ONYXKEYS from '../ONYXKEYS'; - -const propTypes = { - route: PropTypes.shape({ - params: PropTypes.shape({ - backTo: PropTypes.string, - key: PropTypes.string, - }), - }).isRequired, - - /** User's private personal details */ - privatePersonalDetails: PropTypes.shape({ - /** User's home address */ - address: PropTypes.shape({ - state: PropTypes.string, - }), - }), -}; - -const defaultProps = { - privatePersonalDetails: { - address: { - state: '', - }, - }, -}; - -function filterOptions(searchValue, data) { - const trimmedSearchValue = searchValue.trim(); - if (trimmedSearchValue.length === 0) { - return []; - } - - return _.filter(data, (country) => country.text.toLowerCase().includes(searchValue.toLowerCase())); -} - -function StateSelectorPage(props) { - const {translate} = useLocalize(); - const route = props.route; - const [collect, dispatch] = useNavigationStorage(route.params.key, null); - const currentCountryState = collect(); - const allStates = translate('allStates'); - const selectedSearchState = !_.isEmpty(currentCountryState) ? allStates[currentCountryState].stateName : ''; - const [searchValue, setSearchValue] = useState(selectedSearchState); - - const countryStates = useMemo( - () => - _.map(translate('allStates'), (state) => ({ - value: state.stateISO, - keyForList: state.stateISO, - text: state.stateName, - isSelected: currentCountryState === state.stateISO, - })), - [translate, currentCountryState], - ); - - const filteredData = filterOptions(searchValue, countryStates); - const headerMessage = searchValue.trim() && !filteredData.length ? translate('common.noResultsFound') : ''; - - const updateCountryState = (selectedState) => { - dispatch(selectedState.value); - Navigation.goBack(`${decodeURIComponent(route.params.backTo)}`); - }; - - return ( - - Navigation.goBack(`${route.params.backTo}`)} - /> - - - ); -} - -StateSelectorPage.propTypes = propTypes; -StateSelectorPage.defaultProps = defaultProps; -StateSelectorPage.displayName = 'StateSelectorPage'; - -export default withOnyx({ - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, -})(StateSelectorPage); diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index ce29f33a8965..c9d4147c8da7 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -1,11 +1,10 @@ import lodashGet from 'lodash/get'; import _ from 'underscore'; -import React, {useRef, useState, useCallback} from 'react'; +import React, {useState, useCallback} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; import {withOnyx} from 'react-native-onyx'; -import {useFocusEffect} from '@react-navigation/native'; import ScreenWrapper from '../../../../components/ScreenWrapper'; import HeaderWithBackButton from '../../../../components/HeaderWithBackButton'; import Form from '../../../../components/Form'; @@ -15,7 +14,6 @@ import TextInput from '../../../../components/TextInput'; import styles from '../../../../styles/styles'; import * as PersonalDetails from '../../../../libs/actions/PersonalDetails'; import * as ValidationUtils from '../../../../libs/ValidationUtils'; -import useNavigationStorage from '../../../../hooks/useNavigationStorage'; import AddressSearch from '../../../../components/AddressSearch'; import CountryPicker from '../../../../components/CountryPicker'; import StatePicker from '../../../../components/StatePicker'; @@ -60,32 +58,15 @@ function updateAddress(values) { } function AddressPage({privatePersonalDetails}) { - const countryISO = useRef(PersonalDetails.getCountryISO(lodashGet(privatePersonalDetails, 'address.country')) || CONST.COUNTRY.US).current; const {translate} = useLocalize(); - const [collect] = useNavigationStorage('country'); - const [currentCountry, setCurrentCountry] = useState(collect() || countryISO); + const [currentCountry, setCurrentCountry] = useState(PersonalDetails.getCountryISO(lodashGet(privatePersonalDetails, 'address.country')) || CONST.COUNTRY.US); const isUSAForm = currentCountry === CONST.COUNTRY.US; - const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [currentCountry, 'samples'], ''); const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); const address = lodashGet(privatePersonalDetails, 'address') || {}; const [street1, street2] = (address.street || '').split('\n'); - const onAddressChange = () => { - const savedCountry = collect(); - setCurrentCountry(savedCountry); - }; - - useFocusEffect( - useCallback(() => { - const savedCountry = collect(); - if (savedCountry && savedCountry !== currentCountry) { - setCurrentCountry(savedCountry); - } - }, [collect, currentCountry]), - ); - /** * @param {Function} translate - translate function * @param {Boolean} isUSAForm - selected country ISO code is US @@ -152,7 +133,6 @@ function AddressPage({privatePersonalDetails}) { label={translate('common.addressLine', {lineNumber: 1})} defaultValue={street1 || ''} isLimitedToUSA={false} - onAddressChange={onAddressChange} renamedInputKeys={{ street: 'addressLine1', street2: 'addressLine2', @@ -179,6 +159,7 @@ function AddressPage({privatePersonalDetails}) { inputID="country" countryISO={address.country} defaultValue={PersonalDetails.getCountryISO(address.country)} + onCountryUpdated={setCurrentCountry} /> {isUSAForm ? ( diff --git a/src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js b/src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js index b818f04c9fb7..c155e232295f 100644 --- a/src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js +++ b/src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js @@ -14,7 +14,6 @@ import MenuItemWithTopDescription from '../../../../components/MenuItemWithTopDe import * as PersonalDetails from '../../../../libs/actions/PersonalDetails'; import ONYXKEYS from '../../../../ONYXKEYS'; import {withNetwork} from '../../../../components/OnyxProvider'; -import * as NavigationStorage from '../../../../hooks/useNavigationStorage'; const propTypes = { /* Onyx Props */ @@ -62,10 +61,6 @@ function PersonalDetailsInitialPage(props) { PersonalDetails.openPersonalDetailsPage(); }, [props.network.isOffline]); - useEffect(() => { - NavigationStorage.clearNavigationStorage(); - }, []); - const privateDetails = props.privatePersonalDetails || {}; const address = privateDetails.address || {}; const legalName = `${privateDetails.legalFirstName || ''} ${privateDetails.legalLastName || ''}`.trim(); diff --git a/src/styles/styles.js b/src/styles/styles.js index fb2a47841890..28cfde33f929 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3523,7 +3523,7 @@ const styles = { }, addressPickerDescription: { - fontSize: variables.fontSizeSmall, + fontSize: variables.fontSizeNormal, }, }; From 88982249c7254da3cc9c4e776d9d8c9e21fc9f0f Mon Sep 17 00:00:00 2001 From: Edu Date: Fri, 14 Jul 2023 16:54:49 +0200 Subject: [PATCH 11/25] fixed lint errors --- src/components/CountryPicker/CountrySelectorModal.js | 2 +- src/components/CountryPicker/index.js | 8 +++----- src/components/StatePicker/index.js | 3 --- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/CountryPicker/CountrySelectorModal.js b/src/components/CountryPicker/CountrySelectorModal.js index b39408d38da3..d5d27bda3fcf 100644 --- a/src/components/CountryPicker/CountrySelectorModal.js +++ b/src/components/CountryPicker/CountrySelectorModal.js @@ -24,7 +24,7 @@ const propTypes = { const defaultProps = { onClose: () => {}, - onStateSelected: () => {}, + onCountrySelected: () => {}, }; function filterOptions(searchValue, data) { diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js index ca34b030b2f4..69913336451d 100644 --- a/src/components/CountryPicker/index.js +++ b/src/components/CountryPicker/index.js @@ -19,9 +19,6 @@ const propTypes = { // eslint-disable-next-line react/require-default-props value: PropTypes.string, - /** ID of the input */ - inputID: PropTypes.string.isRequired, - /** Callback to call when the input changes */ onInputChange: PropTypes.func, @@ -55,9 +52,10 @@ const CountryPicker = React.forwardRef(({value, countryISO, errorText, onCountry }; useEffect(() => { - if (onCountryUpdated && countryValue.length > 0) { - onCountryUpdated(countryValue); + if (!onCountryUpdated || countryValue.length === 0) { + return; } + onCountryUpdated(countryValue); }, [countryValue]); const title = PersonalDetails.getCountryName(countryValue); diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js index edb1845bbebb..507897deec52 100644 --- a/src/components/StatePicker/index.js +++ b/src/components/StatePicker/index.js @@ -18,9 +18,6 @@ const propTypes = { // eslint-disable-next-line react/require-default-props value: PropTypes.string, - /** ID of the input */ - inputID: PropTypes.string.isRequired, - /** Callback to call when the input changes */ onInputChange: PropTypes.func, }; From fe46e3cf85e2f9293cc6046da112ee2aaa05fbcf Mon Sep 17 00:00:00 2001 From: Edu Date: Fri, 14 Jul 2023 16:59:19 +0200 Subject: [PATCH 12/25] fixed missing dependency --- src/components/CountryPicker/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js index 69913336451d..132e95efb98a 100644 --- a/src/components/CountryPicker/index.js +++ b/src/components/CountryPicker/index.js @@ -56,7 +56,7 @@ const CountryPicker = React.forwardRef(({value, countryISO, errorText, onCountry return; } onCountryUpdated(countryValue); - }, [countryValue]); + }, [countryValue, onCountryUpdated]); const title = PersonalDetails.getCountryName(countryValue); const descStyle = title.length === 0 ? styles.addressPickerDescription : null; From 761c75af88d77da37cd1bf79b92176c5b4978469 Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 17 Jul 2023 10:55:43 +0200 Subject: [PATCH 13/25] Fixed comments --- src/ROUTES.js | 2 -- src/components/AddressSearch/index.js | 13 +++++----- .../CountryPicker/CountrySelectorModal.js | 3 +-- src/components/CountryPicker/index.js | 25 ++++--------------- src/components/Form.js | 2 +- src/components/StatePicker/index.js | 6 ++--- src/languages/en.js | 3 +++ src/languages/es.js | 3 +++ .../Profile/PersonalDetails/AddressPage.js | 11 ++++++-- 9 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/ROUTES.js b/src/ROUTES.js index cf11c0b27a93..b853c42248c6 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -71,8 +71,6 @@ export default { getReportShareCodeRoute: (reportID) => `r/${reportID}/details/shareCode`, REPORT_ATTACHMENTS: 'r/:reportID/attachment', getReportAttachmentRoute: (reportID, source) => `r/${reportID}/attachment?source=${encodeURI(source)}`, - SELECT_YEAR: 'select-year', - getYearSelectionRoute: (minYear, maxYear, currYear, backTo) => `select-year?min=${minYear}&max=${maxYear}&year=${currYear}&backTo=${backTo}`, /** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */ CONCIERGE: 'concierge', diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index afdfd2ff3512..795e45c6f892 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -168,15 +168,9 @@ function AddressSearch(props) { state: state || stateAutoCompleteFallback, }; - const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country); - if (isValidCountryCode) { - values.country = country; - } - // If the address is not in the US, use the full length state name since we're displaying the address's // state / province in a TextInput instead of in a picker. - const isUS = country === CONST.COUNTRY.US; - if (!isUS) { + if (country !== CONST.COUNTRY.US) { values.state = longStateName; } @@ -192,6 +186,11 @@ function AddressSearch(props) { values.street += `, ${subpremise}`; } + const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country); + if (isValidCountryCode) { + values.country = country; + } + if (props.inputID) { _.each(values, (value, key) => { const inputKey = lodashGet(props.renamedInputKeys, key, key); diff --git a/src/components/CountryPicker/CountrySelectorModal.js b/src/components/CountryPicker/CountrySelectorModal.js index d5d27bda3fcf..d9237a758541 100644 --- a/src/components/CountryPicker/CountrySelectorModal.js +++ b/src/components/CountryPicker/CountrySelectorModal.js @@ -66,13 +66,12 @@ function CountrySelectorModal({currentCountry, isVisible, onClose, onCountrySele > {}, - onCountryUpdated: () => {}, }; -const CountryPicker = React.forwardRef(({value, countryISO, errorText, onCountryUpdated, onInputChange}, ref) => { +function CountryPicker({value, errorText, onInputChange}, ref) { const {translate} = useLocalize(); const [isPickerVisible, setIsPickerVisible] = useState(false); - const countryValue = value || countryISO || ''; + const countryValue = value || ''; const showPickerModal = () => { setIsPickerVisible(true); @@ -51,13 +43,6 @@ const CountryPicker = React.forwardRef(({value, countryISO, errorText, onCountry hidePickerModal(); }; - useEffect(() => { - if (!onCountryUpdated || countryValue.length === 0) { - return; - } - onCountryUpdated(countryValue); - }, [countryValue, onCountryUpdated]); - const title = PersonalDetails.getCountryName(countryValue); const descStyle = title.length === 0 ? styles.addressPickerDescription : null; @@ -83,10 +68,10 @@ const CountryPicker = React.forwardRef(({value, countryISO, errorText, onCountry /> ); -}); +} CountryPicker.propTypes = propTypes; CountryPicker.defaultProps = defaultProps; CountryPicker.displayName = 'CountryPicker'; -export default CountryPicker; +export default React.forwardRef(CountryPicker); diff --git a/src/components/Form.js b/src/components/Form.js index c643ece0ba88..4e1e7897f48c 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -316,7 +316,7 @@ function Form(props) { } if (child.props.onValueChange) { - child.props.onValueChange(value); + child.props.onValueChange(value, inputKey); } }, }); diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js index 507897deec52..bb26cad35452 100644 --- a/src/components/StatePicker/index.js +++ b/src/components/StatePicker/index.js @@ -28,7 +28,7 @@ const defaultProps = { onInputChange: () => {}, }; -const StatePicker = React.forwardRef(({value, defaultValue, errorText, onInputChange}, ref) => { +function StatePicker({value, defaultValue, errorText, onInputChange}, ref) { const {translate} = useLocalize(); const [isPickerVisible, setIsPickerVisible] = useState(false); const formValue = value || defaultValue || ''; @@ -80,10 +80,10 @@ const StatePicker = React.forwardRef(({value, defaultValue, errorText, onInputCh /> ); -}); +} StatePicker.propTypes = propTypes; StatePicker.defaultProps = defaultProps; StatePicker.displayName = 'StatePicker'; -export default StatePicker; +export default React.forwardRef(StatePicker); diff --git a/src/languages/en.js b/src/languages/en.js index 064e4aa08c26..dc22268a15c3 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -1484,4 +1484,7 @@ export default { levelTwoResult: 'Message hidden from channel, plus anonymous warning and message is reported for review.', levelThreeResult: 'Message removed from channel plus anonymous warning and message is reported for review.', }, + countrySelectorModal: { + placeholderText: 'Search to see options', + }, }; diff --git a/src/languages/es.js b/src/languages/es.js index b05c74b26222..7a2abf182c75 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -1955,4 +1955,7 @@ export default { levelTwoResult: 'Mensaje ocultado del canal, más advertencia anónima y mensaje reportado para revisión.', levelThreeResult: 'Mensaje eliminado del canal, más advertencia anónima y mensaje reportado para revisión.', }, + countrySelectorModal: { + placeholderText: 'Buscar para ver opciones', + }, }; diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index c9d4147c8da7..3f2114a02055 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -112,6 +112,13 @@ function AddressPage({privatePersonalDetails}) { return errors; }, []); + const handleAddressChange = (value, key) => { + if (key !== 'country') { + return; + } + setCurrentCountry(value); + }; + return ( {isUSAForm ? ( From e06bc961d7ab53667bc04dae7c803f3e197667d4 Mon Sep 17 00:00:00 2001 From: Edu Date: Wed, 19 Jul 2023 12:36:54 +0200 Subject: [PATCH 14/25] Fixed comments --- src/components/CountryPicker/index.js | 24 ++++++++---- .../StatePicker/StateSelectorModal.js | 5 +-- src/components/StatePicker/index.js | 38 +++++++++++-------- src/pages/ReimbursementAccount/AddressForm.js | 2 - src/styles/styles.js | 4 -- 5 files changed, 41 insertions(+), 32 deletions(-) diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js index e9607700796e..8bfe731b93fb 100644 --- a/src/components/CountryPicker/index.js +++ b/src/components/CountryPicker/index.js @@ -18,14 +18,18 @@ const propTypes = { /** Callback to call when the input changes */ onInputChange: PropTypes.func, + + /** A ref to forward to MenuItemWithTopDescription */ + forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]), }; const defaultProps = { + forwardedRef: undefined, errorText: '', onInputChange: () => {}, }; -function CountryPicker({value, errorText, onInputChange}, ref) { +function CountryPicker({value, errorText, onInputChange, forwardedRef}) { const {translate} = useLocalize(); const [isPickerVisible, setIsPickerVisible] = useState(false); const countryValue = value || ''; @@ -38,18 +42,18 @@ function CountryPicker({value, errorText, onInputChange}, ref) { setIsPickerVisible(false); }; - const onStateSelected = (state) => { - onInputChange(state.value); + const updateCountryInput = (country) => { + onInputChange(country.value); hidePickerModal(); }; const title = PersonalDetails.getCountryName(countryValue); - const descStyle = title.length === 0 ? styles.addressPickerDescription : null; + const descStyle = title.length === 0 ? styles.textNormal : null; return ( ); @@ -74,4 +78,10 @@ CountryPicker.propTypes = propTypes; CountryPicker.defaultProps = defaultProps; CountryPicker.displayName = 'CountryPicker'; -export default React.forwardRef(CountryPicker); +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/components/StatePicker/StateSelectorModal.js b/src/components/StatePicker/StateSelectorModal.js index 6ce388f7905e..289c78857016 100644 --- a/src/components/StatePicker/StateSelectorModal.js +++ b/src/components/StatePicker/StateSelectorModal.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import lodashGet from 'lodash/get'; import React, {useState, useMemo} from 'react'; import PropTypes from 'prop-types'; import CONST from '../../CONST'; @@ -38,9 +39,7 @@ function filterOptions(searchValue, data) { function StateSelectorModal({currentState, isVisible, onClose, onStateSelected}) { const {translate} = useLocalize(); const allStates = translate('allStates'); - const selectedSearchState = !_.isEmpty(currentState) ? allStates[currentState].stateName : ''; - - const [searchValue, setSearchValue] = useState(selectedSearchState); + const [searchValue, setSearchValue] = useState(lodashGet(allStates, `${currentState}.stateName`, '')); const countryStates = useMemo( () => diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js index bb26cad35452..a271042960f2 100644 --- a/src/components/StatePicker/index.js +++ b/src/components/StatePicker/index.js @@ -11,37 +11,37 @@ const propTypes = { /** Error text to display */ errorText: PropTypes.string, - /** Default value to display */ - defaultValue: PropTypes.string, - /** State to display */ // eslint-disable-next-line react/require-default-props value: PropTypes.string, /** Callback to call when the input changes */ onInputChange: PropTypes.func, + + /** A ref to forward to MenuItemWithTopDescription */ + forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]), }; const defaultProps = { + forwardedRef: undefined, errorText: '', - defaultValue: '', onInputChange: () => {}, }; -function StatePicker({value, defaultValue, errorText, onInputChange}, ref) { +function StatePicker({value, errorText, onInputChange, forwardedRef}) { const {translate} = useLocalize(); const [isPickerVisible, setIsPickerVisible] = useState(false); - const formValue = value || defaultValue || ''; + const stateValue = value || ''; const title = useMemo(() => { const allStates = translate('allStates'); - if (allStates[formValue]) { - return allStates[formValue].stateName; + if (allStates[stateValue]) { + return allStates[stateValue].stateName; } return ''; - }, [translate, formValue]); + }, [translate, stateValue]); const showPickerModal = () => { setIsPickerVisible(true); @@ -51,17 +51,17 @@ function StatePicker({value, defaultValue, errorText, onInputChange}, ref) { setIsPickerVisible(false); }; - const onStateSelected = (state) => { + const updateStateInput = (state) => { onInputChange(state.value); hidePickerModal(); }; - const descStyle = title.length === 0 ? styles.addressPickerDescription : null; + const descStyle = title.length === 0 ? styles.textNormal : null; return (
); @@ -86,4 +86,10 @@ StatePicker.propTypes = propTypes; StatePicker.defaultProps = defaultProps; StatePicker.displayName = 'StatePicker'; -export default React.forwardRef(StatePicker); +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 8849bc5642e8..240de641f4f3 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -111,8 +111,6 @@ function AddressForm(props) { value={props.values.street} defaultValue={props.defaultValues.street} onInputChange={props.onFieldChange} - onCountryChange={props.onCountryChange} - onStateChange={props.onStateChange} errorText={props.errors.street ? props.translate('bankAccount.error.addressStreet') : ''} hint={props.translate('common.noPO')} renamedInputKeys={props.inputKeys} diff --git a/src/styles/styles.js b/src/styles/styles.js index 7e4a778cb200..907de6fb920c 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3526,10 +3526,6 @@ const styles = { textAlign: 'center', }, - addressPickerDescription: { - fontSize: variables.fontSizeNormal, - }, - /** * @param {String} backgroundColor * @param {Number} height From 4ee01d6ad4d6403c633afcf661cb311928931209 Mon Sep 17 00:00:00 2001 From: Edu Date: Wed, 19 Jul 2023 16:11:56 +0200 Subject: [PATCH 15/25] Removed fallback --- src/components/CountryPicker/CountrySelectorModal.js | 3 ++- src/components/CountryPicker/index.js | 3 +-- src/components/StatePicker/StateSelectorModal.js | 3 ++- src/components/StatePicker/index.js | 3 +-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/CountryPicker/CountrySelectorModal.js b/src/components/CountryPicker/CountrySelectorModal.js index d9237a758541..c27be8729958 100644 --- a/src/components/CountryPicker/CountrySelectorModal.js +++ b/src/components/CountryPicker/CountrySelectorModal.js @@ -13,7 +13,7 @@ const propTypes = { isVisible: PropTypes.bool.isRequired, /** Country value selected */ - currentCountry: PropTypes.string.isRequired, + currentCountry: PropTypes.string, /** Function to call when the user selects a Country */ onCountrySelected: PropTypes.func, @@ -23,6 +23,7 @@ const propTypes = { }; const defaultProps = { + currentCountry: '', onClose: () => {}, onCountrySelected: () => {}, }; diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js index 8bfe731b93fb..96f33ec94ca3 100644 --- a/src/components/CountryPicker/index.js +++ b/src/components/CountryPicker/index.js @@ -29,10 +29,9 @@ const defaultProps = { onInputChange: () => {}, }; -function CountryPicker({value, errorText, onInputChange, forwardedRef}) { +function CountryPicker({value: countryValue, errorText, onInputChange, forwardedRef}) { const {translate} = useLocalize(); const [isPickerVisible, setIsPickerVisible] = useState(false); - const countryValue = value || ''; const showPickerModal = () => { setIsPickerVisible(true); diff --git a/src/components/StatePicker/StateSelectorModal.js b/src/components/StatePicker/StateSelectorModal.js index 289c78857016..f0c97c93f27e 100644 --- a/src/components/StatePicker/StateSelectorModal.js +++ b/src/components/StatePicker/StateSelectorModal.js @@ -13,7 +13,7 @@ const propTypes = { isVisible: PropTypes.bool.isRequired, /** State value selected */ - currentState: PropTypes.string.isRequired, + currentState: PropTypes.string, /** Function to call when the user selects a State */ onStateSelected: PropTypes.func, @@ -23,6 +23,7 @@ const propTypes = { }; const defaultProps = { + currentState: '', onClose: () => {}, onStateSelected: () => {}, }; diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js index a271042960f2..2c9fa7631a06 100644 --- a/src/components/StatePicker/index.js +++ b/src/components/StatePicker/index.js @@ -28,10 +28,9 @@ const defaultProps = { onInputChange: () => {}, }; -function StatePicker({value, errorText, onInputChange, forwardedRef}) { +function StatePicker({value: stateValue, errorText, onInputChange, forwardedRef}) { const {translate} = useLocalize(); const [isPickerVisible, setIsPickerVisible] = useState(false); - const stateValue = value || ''; const title = useMemo(() => { const allStates = translate('allStates'); From 2cead61d7b05fe89adf28221653587a3c99205f6 Mon Sep 17 00:00:00 2001 From: Edu Date: Thu, 20 Jul 2023 15:13:10 +0200 Subject: [PATCH 16/25] Fixed comments --- .../CountryPicker/CountrySelectorModal.js | 13 ++++++----- src/components/CountryPicker/index.js | 17 +++++++++----- .../StatePicker/StateSelectorModal.js | 15 ++++++++----- src/components/StatePicker/index.js | 22 +++++++++++-------- src/components/TextInput/BaseTextInput.js | 1 - .../TextInput/baseTextInputPropTypes.js | 3 --- src/languages/en.js | 3 +++ src/languages/es.js | 3 +++ src/libs/actions/PersonalDetails.js | 11 ---------- src/pages/ReimbursementAccount/AddressForm.js | 9 -------- .../ReimbursementAccount/RequestorStep.js | 2 +- 11 files changed, 48 insertions(+), 51 deletions(-) diff --git a/src/components/CountryPicker/CountrySelectorModal.js b/src/components/CountryPicker/CountrySelectorModal.js index c27be8729958..d16d97741d7c 100644 --- a/src/components/CountryPicker/CountrySelectorModal.js +++ b/src/components/CountryPicker/CountrySelectorModal.js @@ -1,7 +1,6 @@ import _ from 'underscore'; -import React, {useState, useMemo} from 'react'; +import React, {useMemo} from 'react'; import PropTypes from 'prop-types'; -import * as PersonalDetails from '../../libs/actions/PersonalDetails'; import CONST from '../../CONST'; import useLocalize from '../../hooks/useLocalize'; import HeaderWithBackButton from '../HeaderWithBackButton'; @@ -20,6 +19,12 @@ const propTypes = { /** Function to call when the user closes the Country modal */ onClose: PropTypes.func, + + /** The search value from the selection list */ + searchValue: PropTypes.string.isRequired, + + /** Function to call when the user types in the search input */ + setSearchValue: PropTypes.func.isRequired, }; const defaultProps = { @@ -37,10 +42,8 @@ function filterOptions(searchValue, data) { return _.filter(data, (country) => country.text.toLowerCase().includes(searchValue.toLowerCase())); } -function CountrySelectorModal({currentCountry, isVisible, onClose, onCountrySelected}) { +function CountrySelectorModal({currentCountry, isVisible, onClose, onCountrySelected, setSearchValue, searchValue}) { const {translate} = useLocalize(); - const selectedSearchCountry = PersonalDetails.getCountryName(currentCountry); - const [searchValue, setSearchValue] = useState(selectedSearchCountry); const countries = useMemo( () => diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js index 96f33ec94ca3..7e99fe1eff61 100644 --- a/src/components/CountryPicker/index.js +++ b/src/components/CountryPicker/index.js @@ -1,9 +1,9 @@ -import React, {useState} from 'react'; +import React, {useState, useRef} from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; +import lodashGet from 'lodash/get'; import styles from '../../styles/styles'; import MenuItemWithTopDescription from '../MenuItemWithTopDescription'; -import * as PersonalDetails from '../../libs/actions/PersonalDetails'; import useLocalize from '../../hooks/useLocalize'; import CountrySelectorModal from './CountrySelectorModal'; import FormHelpMessage from '../FormHelpMessage'; @@ -13,6 +13,7 @@ const propTypes = { errorText: PropTypes.string, /** Country to display */ + // Adding a default value overrides the value coming from the Form // eslint-disable-next-line react/require-default-props value: PropTypes.string, @@ -29,9 +30,11 @@ const defaultProps = { onInputChange: () => {}, }; -function CountryPicker({value: countryValue, errorText, onInputChange, forwardedRef}) { +function CountryPicker({value, errorText, onInputChange, forwardedRef}) { const {translate} = useLocalize(); + const allCountries = useRef(translate('allCountries')).current; const [isPickerVisible, setIsPickerVisible] = useState(false); + const [searchValue, setSearchValue] = useState(lodashGet(allCountries, value, '')); const showPickerModal = () => { setIsPickerVisible(true); @@ -43,10 +46,11 @@ function CountryPicker({value: countryValue, errorText, onInputChange, forwarded const updateCountryInput = (country) => { onInputChange(country.value); + setSearchValue(lodashGet(allCountries, country.value, '')); hidePickerModal(); }; - const title = PersonalDetails.getCountryName(countryValue); + const title = allCountries[value] || ''; const descStyle = title.length === 0 ? styles.textNormal : null; return ( @@ -63,9 +67,10 @@ function CountryPicker({value: countryValue, errorText, onInputChange, forwarded diff --git a/src/components/StatePicker/StateSelectorModal.js b/src/components/StatePicker/StateSelectorModal.js index f0c97c93f27e..4497eab72de8 100644 --- a/src/components/StatePicker/StateSelectorModal.js +++ b/src/components/StatePicker/StateSelectorModal.js @@ -1,6 +1,5 @@ import _ from 'underscore'; -import lodashGet from 'lodash/get'; -import React, {useState, useMemo} from 'react'; +import React, {useMemo} from 'react'; import PropTypes from 'prop-types'; import CONST from '../../CONST'; import Modal from '../Modal'; @@ -20,6 +19,12 @@ const propTypes = { /** Function to call when the user closes the State modal */ onClose: PropTypes.func, + + /** The search value from the selection list */ + searchValue: PropTypes.string.isRequired, + + /** Function to call when the user types in the search input */ + setSearchValue: PropTypes.func.isRequired, }; const defaultProps = { @@ -37,10 +42,8 @@ function filterOptions(searchValue, data) { return _.filter(data, (country) => country.text.toLowerCase().includes(searchValue.toLowerCase())); } -function StateSelectorModal({currentState, isVisible, onClose, onStateSelected}) { +function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, searchValue, setSearchValue}) { const {translate} = useLocalize(); - const allStates = translate('allStates'); - const [searchValue, setSearchValue] = useState(lodashGet(allStates, `${currentState}.stateName`, '')); const countryStates = useMemo( () => @@ -73,7 +76,7 @@ function StateSelectorModal({currentState, isVisible, onClose, onStateSelected}) {}, }; -function StatePicker({value: stateValue, errorText, onInputChange, forwardedRef}) { +function StatePicker({value, errorText, onInputChange, forwardedRef}) { const {translate} = useLocalize(); + const allStates = useRef(translate('allStates')).current; const [isPickerVisible, setIsPickerVisible] = useState(false); + const [searchValue, setSearchValue] = useState(lodashGet(allStates, `${value}.stateName`, '')); const title = useMemo(() => { - const allStates = translate('allStates'); - - if (allStates[stateValue]) { - return allStates[stateValue].stateName; + if (allStates[value]) { + return allStates[value].stateName; } return ''; - }, [translate, stateValue]); + }, [value, allStates]); const showPickerModal = () => { setIsPickerVisible(true); @@ -52,6 +54,7 @@ function StatePicker({value: stateValue, errorText, onInputChange, forwardedRef} const updateStateInput = (state) => { onInputChange(state.value); + setSearchValue(lodashGet(allStates, `${state.value}.stateName`, '')); hidePickerModal(); }; @@ -71,11 +74,12 @@ function StatePicker({value: stateValue, errorText, onInputChange, forwardedRef} ); diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index e23fabb65666..d1b67b52e19f 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -371,7 +371,6 @@ function BaseTextInput(props) { {!_.isEmpty(inputHelpText) && ( diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js index e2ab974da75b..58a37c7667eb 100644 --- a/src/components/TextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/baseTextInputPropTypes.js @@ -67,9 +67,6 @@ const propTypes = { /** Hint text to display below the TextInput */ hint: PropTypes.string, - /** Style the Hint container */ - hintContainerStyle: stylePropTypes, - /** Prefix character */ prefixCharacter: PropTypes.string, diff --git a/src/languages/en.js b/src/languages/en.js index e008149ae232..739e552babdd 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -1526,4 +1526,7 @@ export default { countrySelectorModal: { placeholderText: 'Search to see options', }, + stateSelectorModal: { + placeholderText: 'Search to see options', + }, }; diff --git a/src/languages/es.js b/src/languages/es.js index b8c5852b0539..21b568940d54 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -1997,4 +1997,7 @@ export default { countrySelectorModal: { placeholderText: 'Buscar para ver opciones', }, + stateSelectorModal: { + placeholderText: 'Buscar para ver opciones', + }, }; diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 1cd78c749e22..afc6391f3003 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -116,16 +116,6 @@ function getCountryISO(countryName) { return _.findKey(CONST.ALL_COUNTRIES, (country) => country === countryName) || ''; } -/** - * Returns the name of the country associated with the provided ISO code. - * If the provided code is invalid, an empty string is returned. - * - * @param {string} countryISO The ISO code of the country to look up. - * @returns {string} The name of the country associated with the provided ISO code. - */ -function getCountryName(countryISO) { - return CONST.ALL_COUNTRIES[countryISO] || ''; -} /** * @param {String} pronouns */ @@ -505,5 +495,4 @@ export { updateAutomaticTimezone, updateSelectedTimezone, getCountryISO, - getCountryName, }; diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 240de641f4f3..d8fbc0290136 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -14,12 +14,6 @@ const propTypes = { /** Callback fired when a field changes. Passes args as {[fieldName]: val} */ onFieldChange: PropTypes.func, - /** Callback fired when a Address search changes the Country. */ - onCountryChange: PropTypes.func, - - /** Callback fired when a Address search changes the State. */ - onStateChange: PropTypes.func, - /** Default values */ defaultValues: PropTypes.shape({ /** Address street field */ @@ -95,8 +89,6 @@ const defaultProps = { }, shouldSaveDraft: false, onFieldChange: () => {}, - onCountryChange: () => {}, - onStateChange: () => {}, }; function AddressForm(props) { @@ -146,7 +138,6 @@ function AddressForm(props) { label={props.translate('common.zip')} accessibilityLabel={props.translate('common.zip')} accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} - containerStyles={[styles.mt4]} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} value={props.values.zipCode} defaultValue={props.defaultValues.zipCode} diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js index 0a6cd1ee2c16..0fa71833b34c 100644 --- a/src/pages/ReimbursementAccount/RequestorStep.js +++ b/src/pages/ReimbursementAccount/RequestorStep.js @@ -154,7 +154,7 @@ class RequestorStep extends React.Component { lastName: this.props.getDefaultStateForField('lastName'), street: this.props.getDefaultStateForField('requestorAddressStreet'), city: this.props.getDefaultStateForField('requestorAddressCity'), - state: this.props.reimbursementAccountDraft.requestorAddressState || this.props.getDefaultStateForField('requestorAddressState'), + state: this.props.getDefaultStateForField('requestorAddressState'), zipCode: this.props.getDefaultStateForField('requestorAddressZipCode'), dob: this.props.getDefaultStateForField('dob'), ssnLast4: this.props.getDefaultStateForField('ssnLast4'), From 675a7c3c27ba7b04484e986d5e774817775df474 Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 24 Jul 2023 10:15:59 +0200 Subject: [PATCH 17/25] Fixed comments --- src/components/CountryPicker/index.js | 3 +- src/components/MenuItemWithTopDescription.js | 28 ++++++++++++------- src/components/StatePicker/index.js | 3 +- .../TextInput/baseTextInputPropTypes.js | 1 - 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js index 7e99fe1eff61..bae7294d2584 100644 --- a/src/components/CountryPicker/index.js +++ b/src/components/CountryPicker/index.js @@ -13,8 +13,6 @@ const propTypes = { errorText: PropTypes.string, /** Country to display */ - // Adding a default value overrides the value coming from the Form - // eslint-disable-next-line react/require-default-props value: PropTypes.string, /** Callback to call when the input changes */ @@ -25,6 +23,7 @@ const propTypes = { }; const defaultProps = { + value: undefined, forwardedRef: undefined, errorText: '', onInputChange: () => {}, diff --git a/src/components/MenuItemWithTopDescription.js b/src/components/MenuItemWithTopDescription.js index 31997525d512..dcce160ecf3f 100644 --- a/src/components/MenuItemWithTopDescription.js +++ b/src/components/MenuItemWithTopDescription.js @@ -6,17 +6,25 @@ const propTypes = { ...menuItemPropTypes, }; -const MenuItemWithTopDescription = React.forwardRef((props, ref) => ( - -)); +function MenuItemWithTopDescription(props) { + return ( + + ); +} MenuItemWithTopDescription.propTypes = propTypes; MenuItemWithTopDescription.displayName = 'MenuItemWithTopDescription'; -export default MenuItemWithTopDescription; +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js index 0c33dc90c400..e5ed30033d28 100644 --- a/src/components/StatePicker/index.js +++ b/src/components/StatePicker/index.js @@ -13,8 +13,6 @@ const propTypes = { errorText: PropTypes.string, /** State to display */ - // Adding a default value overrides the value coming from the Form - // eslint-disable-next-line react/require-default-props value: PropTypes.string, /** Callback to call when the input changes */ @@ -25,6 +23,7 @@ const propTypes = { }; const defaultProps = { + value: undefined, forwardedRef: undefined, errorText: '', onInputChange: () => {}, diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js index 58a37c7667eb..a51717b6f6d2 100644 --- a/src/components/TextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/baseTextInputPropTypes.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import stylePropTypes from '../../styles/stylePropTypes'; const propTypes = { /** Input label */ From 0779f34ed152f5e9bcea9f7d8eac11d476c0ea96 Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 24 Jul 2023 12:31:26 +0200 Subject: [PATCH 18/25] fixed comments --- src/components/CountryPicker/index.js | 4 ++-- src/components/StatePicker/index.js | 13 +++---------- src/components/TextInput/baseTextInputPropTypes.js | 1 - 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js index bae7294d2584..7f68d6cc2675 100644 --- a/src/components/CountryPicker/index.js +++ b/src/components/CountryPicker/index.js @@ -1,4 +1,4 @@ -import React, {useState, useRef} from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; import lodashGet from 'lodash/get'; @@ -31,7 +31,7 @@ const defaultProps = { function CountryPicker({value, errorText, onInputChange, forwardedRef}) { const {translate} = useLocalize(); - const allCountries = useRef(translate('allCountries')).current; + const allCountries = translate('allCountries'); const [isPickerVisible, setIsPickerVisible] = useState(false); const [searchValue, setSearchValue] = useState(lodashGet(allCountries, value, '')); diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js index e5ed30033d28..a826827b6471 100644 --- a/src/components/StatePicker/index.js +++ b/src/components/StatePicker/index.js @@ -1,4 +1,4 @@ -import React, {useMemo, useState, useRef} from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; @@ -31,18 +31,10 @@ const defaultProps = { function StatePicker({value, errorText, onInputChange, forwardedRef}) { const {translate} = useLocalize(); - const allStates = useRef(translate('allStates')).current; + const allStates = translate('allStates'); const [isPickerVisible, setIsPickerVisible] = useState(false); const [searchValue, setSearchValue] = useState(lodashGet(allStates, `${value}.stateName`, '')); - const title = useMemo(() => { - if (allStates[value]) { - return allStates[value].stateName; - } - - return ''; - }, [value, allStates]); - const showPickerModal = () => { setIsPickerVisible(true); }; @@ -57,6 +49,7 @@ function StatePicker({value, errorText, onInputChange, forwardedRef}) { hidePickerModal(); }; + const title = allStates[value] ? allStates[value].stateName : ''; const descStyle = title.length === 0 ? styles.textNormal : null; return ( diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js index a51717b6f6d2..8a1b05a628c2 100644 --- a/src/components/TextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/baseTextInputPropTypes.js @@ -122,7 +122,6 @@ const defaultProps = { shouldSaveDraft: false, maxLength: null, hint: '', - hintContainerStyle: [], prefixCharacter: '', onInputChange: () => {}, shouldDelayFocus: false, From d063f553ec0cdff83918c5a5b2b2805bbd28f8f5 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 25 Jul 2023 16:25:22 +0200 Subject: [PATCH 19/25] fix: add missing space --- src/pages/settings/Profile/PersonalDetails/AddressPage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index 3f2114a02055..f4c6ea94c690 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.js @@ -169,6 +169,7 @@ function AddressPage({privatePersonalDetails}) { onValueChange={handleAddressChange} /> + {isUSAForm ? ( Date: Tue, 25 Jul 2023 19:07:40 +0200 Subject: [PATCH 20/25] fix: update searchValue when current value changes --- src/components/CountryPicker/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js index 7f68d6cc2675..edbd26d5bcfe 100644 --- a/src/components/CountryPicker/index.js +++ b/src/components/CountryPicker/index.js @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; import lodashGet from 'lodash/get'; @@ -35,6 +35,10 @@ function CountryPicker({value, errorText, onInputChange, forwardedRef}) { const [isPickerVisible, setIsPickerVisible] = useState(false); const [searchValue, setSearchValue] = useState(lodashGet(allCountries, value, '')); + useEffect(() => { + setSearchValue(lodashGet(allCountries, value, '')); + }, [value]); + const showPickerModal = () => { setIsPickerVisible(true); }; From 94bd35a09ac26f6f5e2ea1b15e08e7f120952b3b Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 25 Jul 2023 19:15:49 +0200 Subject: [PATCH 21/25] fix: include allCountries in the deps array --- src/components/CountryPicker/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js index edbd26d5bcfe..60f86f75b546 100644 --- a/src/components/CountryPicker/index.js +++ b/src/components/CountryPicker/index.js @@ -37,7 +37,7 @@ function CountryPicker({value, errorText, onInputChange, forwardedRef}) { useEffect(() => { setSearchValue(lodashGet(allCountries, value, '')); - }, [value]); + }, [value, allCountries]); const showPickerModal = () => { setIsPickerVisible(true); From f750d18a5295b3726a4fcefd4883debd50dc74c1 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 25 Jul 2023 20:28:36 +0200 Subject: [PATCH 22/25] fix: apply requested changes --- src/components/CountryPicker/index.js | 1 - src/components/StatePicker/index.js | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js index 60f86f75b546..20f9a981accb 100644 --- a/src/components/CountryPicker/index.js +++ b/src/components/CountryPicker/index.js @@ -49,7 +49,6 @@ function CountryPicker({value, errorText, onInputChange, forwardedRef}) { const updateCountryInput = (country) => { onInputChange(country.value); - setSearchValue(lodashGet(allCountries, country.value, '')); hidePickerModal(); }; diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js index a826827b6471..7c8fbdae36bb 100644 --- a/src/components/StatePicker/index.js +++ b/src/components/StatePicker/index.js @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; @@ -35,6 +35,10 @@ function StatePicker({value, errorText, onInputChange, forwardedRef}) { const [isPickerVisible, setIsPickerVisible] = useState(false); const [searchValue, setSearchValue] = useState(lodashGet(allStates, `${value}.stateName`, '')); + useEffect(() => { + setSearchValue(lodashGet(allStates, `${value}.stateName`, '')); + }, [value, allStates]); + const showPickerModal = () => { setIsPickerVisible(true); }; @@ -45,7 +49,6 @@ function StatePicker({value, errorText, onInputChange, forwardedRef}) { const updateStateInput = (state) => { onInputChange(state.value); - setSearchValue(lodashGet(allStates, `${state.value}.stateName`, '')); hidePickerModal(); }; From 0e416e014aaab7b9840c6fc8e4fb853a2da75165 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 25 Jul 2023 20:43:28 +0200 Subject: [PATCH 23/25] fix: minor change --- src/components/MenuItemWithTopDescription.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MenuItemWithTopDescription.js b/src/components/MenuItemWithTopDescription.js index dcce160ecf3f..716e832f7d44 100644 --- a/src/components/MenuItemWithTopDescription.js +++ b/src/components/MenuItemWithTopDescription.js @@ -25,6 +25,6 @@ export default React.forwardRef((props, ref) => ( )); From 5c0ef299b18bd4a519b52602a94ff00e1031932b Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 26 Jul 2023 08:52:11 +0200 Subject: [PATCH 24/25] fix: fix prop name problem --- src/components/MenuItemWithTopDescription.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MenuItemWithTopDescription.js b/src/components/MenuItemWithTopDescription.js index 716e832f7d44..c4e5cf6b19cb 100644 --- a/src/components/MenuItemWithTopDescription.js +++ b/src/components/MenuItemWithTopDescription.js @@ -11,7 +11,7 @@ function MenuItemWithTopDescription(props) { From 4852ccbd675e94f9a18413a8d48650fda54229d0 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 26 Jul 2023 10:19:05 +0200 Subject: [PATCH 25/25] fix: change useOnNetworkReconnect to useNetwork --- src/pages/workspace/WorkspaceInvitePage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 9bfcf1871e0b..0810682d34e2 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -21,7 +21,7 @@ import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoun import ROUTES from '../../ROUTES'; import * as Browser from '../../libs/Browser'; import * as PolicyUtils from '../../libs/PolicyUtils'; -import useOnNetworkReconnect from '../../hooks/useOnNetworkReconnect'; +import useNetwork from '../../hooks/useNetwork'; import useLocalize from '../../hooks/useLocalize'; const personalDetailsPropTypes = PropTypes.shape({ @@ -78,7 +78,7 @@ function WorkspaceInvitePage(props) { // eslint-disable-next-line react-hooks/exhaustive-deps -- policyID changes remount the component }, []); - useOnNetworkReconnect(openWorkspaceInvitePage); + useNetwork({onReconnect: openWorkspaceInvitePage}); useEffect(() => { const inviteOptions = OptionsListUtils.getMemberInviteOptions(