diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 78d5f4d54888..5ce59a7be3f0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -85,6 +85,16 @@ export default { SETTINGS_PERSONAL_DETAILS_LEGAL_NAME: 'settings/profile/personal-details/legal-name', SETTINGS_PERSONAL_DETAILS_DATE_OF_BIRTH: 'settings/profile/personal-details/date-of-birth', SETTINGS_PERSONAL_DETAILS_ADDRESS: 'settings/profile/personal-details/address', + SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY: { + route: 'settings/profile/personal-details/address/country', + getRoute: (country: string, backTo?: string) => { + let route = `settings/profile/personal-details/address/country?country=${country}`; + if (backTo) { + route += `&backTo=${encodeURIComponent(backTo)}`; + } + return route; + } + }, SETTINGS_CONTACT_METHODS: 'settings/profile/contact-methods', SETTINGS_CONTACT_METHOD_DETAILS: { route: 'settings/profile/contact-methods/:contactMethod/details', diff --git a/src/components/CountryPicker/CountrySelectorModal.js b/src/components/CountryPicker/CountrySelectorModal.js deleted file mode 100644 index 6c6cd19af0c7..000000000000 --- a/src/components/CountryPicker/CountrySelectorModal.js +++ /dev/null @@ -1,105 +0,0 @@ -import _ from 'underscore'; -import React, {useMemo, useEffect} from 'react'; -import PropTypes from 'prop-types'; -import CONST from '../../CONST'; -import useLocalize from '../../hooks/useLocalize'; -import HeaderWithBackButton from '../HeaderWithBackButton'; -import SelectionList from '../SelectionList'; -import Modal from '../Modal'; -import ScreenWrapper from '../ScreenWrapper'; -import styles from '../../styles/styles'; -import searchCountryOptions from '../../libs/searchCountryOptions'; -import StringUtils from '../../libs/StringUtils'; - -const propTypes = { - /** Whether the modal is visible */ - isVisible: PropTypes.bool.isRequired, - - /** Country value selected */ - currentCountry: PropTypes.string, - - /** 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, - - /** 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 = { - currentCountry: '', - onClose: () => {}, - onCountrySelected: () => {}, -}; - -function CountrySelectorModal({currentCountry, isVisible, onClose, onCountrySelected, setSearchValue, searchValue}) { - const {translate} = useLocalize(); - - useEffect(() => { - if (isVisible) { - return; - } - setSearchValue(''); - }, [isVisible, setSearchValue]); - - const countries = useMemo( - () => - _.map(_.keys(CONST.ALL_COUNTRIES), (countryISO) => { - const countryName = translate(`allCountries.${countryISO}`); - return { - value: countryISO, - keyForList: countryISO, - text: countryName, - isSelected: currentCountry === countryISO, - searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`), - }; - }), - [translate, currentCountry], - ); - - const searchResults = searchCountryOptions(searchValue, countries); - const headerMessage = searchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : ''; - - return ( - - - - - - - ); -} - -CountrySelectorModal.propTypes = propTypes; -CountrySelectorModal.defaultProps = defaultProps; -CountrySelectorModal.displayName = 'CountrySelectorModal'; - -export default CountrySelectorModal; diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js deleted file mode 100644 index 8f5c89b1bce8..000000000000 --- a/src/components/CountryPicker/index.js +++ /dev/null @@ -1,90 +0,0 @@ -import React, {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 CountrySelectorModal from './CountrySelectorModal'; -import FormHelpMessage from '../FormHelpMessage'; -import refPropTypes from '../refPropTypes'; - -const propTypes = { - /** Form Error description */ - errorText: PropTypes.string, - - /** Country to display */ - value: PropTypes.string, - - /** Callback to call when the input changes */ - onInputChange: PropTypes.func, - - /** A ref to forward to MenuItemWithTopDescription */ - forwardedRef: refPropTypes, -}; - -const defaultProps = { - value: undefined, - forwardedRef: undefined, - errorText: '', - onInputChange: () => {}, -}; - -function CountryPicker({value, errorText, onInputChange, forwardedRef}) { - const {translate} = useLocalize(); - const [isPickerVisible, setIsPickerVisible] = useState(false); - const [searchValue, setSearchValue] = useState(''); - - const showPickerModal = () => { - setIsPickerVisible(true); - }; - - const hidePickerModal = () => { - setIsPickerVisible(false); - }; - - const updateCountryInput = (country) => { - if (country.value !== value) { - onInputChange(country.value); - } - hidePickerModal(); - }; - - const title = value ? translate(`allCountries.${value}`) : ''; - const descStyle = title.length === 0 ? styles.textNormal : null; - - return ( - - - - - - - - ); -} - -CountryPicker.propTypes = propTypes; -CountryPicker.defaultProps = defaultProps; -CountryPicker.displayName = 'CountryPicker'; - -export default React.forwardRef((props, ref) => ( - -)); diff --git a/src/components/CountrySelector.js b/src/components/CountrySelector.js new file mode 100644 index 000000000000..2788f3cea8e3 --- /dev/null +++ b/src/components/CountrySelector.js @@ -0,0 +1,77 @@ +import React, {useEffect} from 'react'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import styles from '../styles/styles'; +import Navigation from '../libs/Navigation/Navigation'; +import ROUTES from '../ROUTES'; +import useLocalize from '../hooks/useLocalize'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import FormHelpMessage from './FormHelpMessage'; + +const propTypes = { + /** Form error text. e.g when no country is selected */ + errorText: PropTypes.string, + + /** Callback called when the country changes. */ + onInputChange: PropTypes.func.isRequired, + + /** Current selected country */ + value: PropTypes.string, + + /** inputID used by the Form component */ + // eslint-disable-next-line react/no-unused-prop-types + inputID: PropTypes.string.isRequired, + + /** React ref being forwarded to the MenuItemWithTopDescription */ + forwardedRef: PropTypes.func, +}; + +const defaultProps = { + errorText: '', + value: undefined, + forwardedRef: () => {}, +}; + +function CountrySelector({errorText, value: countryCode, onInputChange, forwardedRef}) { + const {translate} = useLocalize(); + + const title = countryCode ? translate(`allCountries.${countryCode}`) : ''; + const countryTitleDescStyle = title.length === 0 ? styles.textNormal : null; + + useEffect(() => { + // This will cause the form to revalidate and remove any error related to country name + onInputChange(countryCode); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [countryCode]); + + return ( + + { + const activeRoute = Navigation.getActiveRoute().replace(/\?.*/, ''); + Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY.getRoute(countryCode, activeRoute)); + }} + /> + + + + + ); +} + +CountrySelector.propTypes = propTypes; +CountrySelector.defaultProps = defaultProps; +CountrySelector.displayName = 'CountrySelector'; + +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index fc284f566c80..1dfa4d7b707b 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -126,6 +126,7 @@ const SettingsModalStackNavigator = createModalStackNavigator({ Settings_PersonalDetails_LegalName: () => require('../../../pages/settings/Profile/PersonalDetails/LegalNamePage').default, Settings_PersonalDetails_DateOfBirth: () => require('../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default, Settings_PersonalDetails_Address: () => require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default, + Settings_PersonalDetails_Address_Country: () => require('../../../pages/settings/Profile/PersonalDetails/CountrySelectionPage').default, Settings_ContactMethods: () => require('../../../pages/settings/Profile/Contacts/ContactMethodsPage').default, Settings_ContactMethodDetails: () => require('../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default, Settings_NewContactMethod: () => require('../../../pages/settings/Profile/Contacts/NewContactMethodPage').default, diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 116e1b9d55a5..99771fd34558 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -151,6 +151,10 @@ export default { path: ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS, exact: true, }, + Settings_PersonalDetails_Address_Country: { + path: ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY.route, + exact: true, + }, Settings_TwoFactorAuth: { path: ROUTES.SETTINGS_2FA, exact: true, diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.js index 782756024d8f..7dadbb4608d9 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'; @@ -15,13 +15,13 @@ import styles from '../../../../styles/styles'; import * as PersonalDetails from '../../../../libs/actions/PersonalDetails'; import * as ValidationUtils from '../../../../libs/ValidationUtils'; 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 useLocalize from '../../../../hooks/useLocalize'; import usePrivatePersonalDetails from '../../../../hooks/usePrivatePersonalDetails'; import FullscreenLoadingIndicator from '../../../../components/FullscreenLoadingIndicator'; +import CountrySelector from '../../../../components/CountrySelector'; const propTypes = { /* Onyx Props */ @@ -37,6 +37,15 @@ const propTypes = { country: PropTypes.string, }), }), + + /** Route from navigation */ + route: PropTypes.shape({ + /** Params from the route */ + params: PropTypes.shape({ + /** Currently selected country */ + country: PropTypes.string, + }), + }).isRequired, }; const defaultProps = { @@ -59,10 +68,11 @@ 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({privatePersonalDetails}) { +function AddressPage({privatePersonalDetails, route}) { usePrivatePersonalDetails(); const {translate} = useLocalize(); - const [currentCountry, setCurrentCountry] = useState(PersonalDetails.getCountryISO(lodashGet(privatePersonalDetails, 'address.country'))); + const countryFromUrl = lodashGet(route, 'params.country'); + const [currentCountry, setCurrentCountry] = useState(countryFromUrl || PersonalDetails.getCountryISO(lodashGet(privatePersonalDetails, 'address.country'))); const isUSAForm = currentCountry === CONST.COUNTRY.US; const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [currentCountry, 'samples'], ''); const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); @@ -116,7 +126,7 @@ function AddressPage({privatePersonalDetails}) { return errors; }, []); - const handleAddressChange = (value, key) => { + const handleAddressChange = useCallback((value, key) => { if (key !== 'country' && key !== 'state') { return; } @@ -126,7 +136,14 @@ function AddressPage({privatePersonalDetails}) { return; } setState(value); - }; + }, []); + + useEffect(() => { + if (!countryFromUrl || countryFromUrl === currentCountry) { + return; + } + handleAddressChange(countryFromUrl, 'country'); + }, [countryFromUrl, handleAddressChange, currentCountry]); return ( - diff --git a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.js b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.js new file mode 100644 index 000000000000..741974776df1 --- /dev/null +++ b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.js @@ -0,0 +1,107 @@ +import React, {useState, useMemo, useCallback} from 'react'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import lodashGet from 'lodash/get'; +import Navigation from '../../../../libs/Navigation/Navigation'; +import ScreenWrapper from '../../../../components/ScreenWrapper'; +import HeaderWithBackButton from '../../../../components/HeaderWithBackButton'; +import SelectionList from '../../../../components/SelectionList'; +import searchCountryOptions from '../../../../libs/searchCountryOptions'; +import StringUtils from '../../../../libs/StringUtils'; +import CONST from '../../../../CONST'; +import useLocalize from '../../../../hooks/useLocalize'; + +const propTypes = { + /** Route from navigation */ + route: PropTypes.shape({ + /** Params from the route */ + params: PropTypes.shape({ + /** Currently selected country */ + country: PropTypes.string, + + /** Route to navigate back after selecting a currency */ + backTo: PropTypes.string, + }), + }).isRequired, + + /** Navigation from react-navigation */ + navigation: PropTypes.shape({ + /** getState function retrieves the current navigation state from react-navigation's navigation property */ + getState: PropTypes.func.isRequired, + }).isRequired, +}; + +function CountrySelectionPage({route, navigation}) { + const [searchValue, setSearchValue] = useState(''); + const {translate} = useLocalize(); + const currentCountry = lodashGet(route, 'params.country'); + + const countries = useMemo( + () => + _.map(_.keys(CONST.ALL_COUNTRIES), (countryISO) => { + const countryName = translate(`allCountries.${countryISO}`); + return { + value: countryISO, + keyForList: countryISO, + text: countryName, + isSelected: currentCountry === countryISO, + searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`), + }; + }), + [translate, currentCountry], + ); + + const searchResults = searchCountryOptions(searchValue, countries); + const headerMessage = searchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : ''; + + const selectCountry = useCallback( + (option) => { + const backTo = lodashGet(route, 'params.backTo', ''); + + // Check the navigation state and "backTo" parameter to decide navigation behavior + if (navigation.getState().routes.length === 1 && _.isEmpty(backTo)) { + // If there is only one route and "backTo" is empty, go back in navigation + Navigation.goBack(); + } else if (!_.isEmpty(backTo) && navigation.getState().routes.length === 1) { + // If "backTo" is not empty and there is only one route, go back to the specific route defined in "backTo" with a country parameter + Navigation.goBack(`${route.params.backTo}?country=${option.value}`); + } else { + // Otherwise, navigate to the specific route defined in "backTo" with a country parameter + Navigation.navigate(`${route.params.backTo}?country=${option.value}`); + } + }, + [route, navigation], + ); + + return ( + + { + const backTo = lodashGet(route, 'params.backTo', ''); + const backToRoute = backTo ? `${backTo}?country=${currentCountry}` : ''; + Navigation.goBack(backToRoute); + }} + /> + + + + ); +} + +CountrySelectionPage.displayName = 'CountrySelectionPage'; +CountrySelectionPage.propTypes = propTypes; + +export default CountrySelectionPage;