diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Tax.md b/docs/articles/expensify-classic/policy-and-domain-settings/Tax.md deleted file mode 100644 index 3ee1c8656b4b..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Tax.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/tax-tracking.md b/docs/articles/expensify-classic/policy-and-domain-settings/tax-tracking.md new file mode 100644 index 000000000000..7b859c5101b1 --- /dev/null +++ b/docs/articles/expensify-classic/policy-and-domain-settings/tax-tracking.md @@ -0,0 +1,19 @@ +--- +title: Tax +description: How to track expense taxes +--- +# Overview +Expensify’s tax tracking feature allows you to: +- Add tax names, rates, and codes whether you’re connected to an accounting system or not. +- Enable/disable taxes you’d like to make available to users. +- Set a default tax for Workspace currency expenses and, optionally, another default tax (including exempt) for foreign currency expenses which - will automatically apply to all new expenses. + +# How to Enable Tax Tracking +Tax tracking can be enabled in the Tax section of the Workspace settings of any Workspace, whether group or individual. +## If Connected to an Accounting Integration +If your group Workspace is connected to Xero, QuickBooks Online, Sage Intacct, or NetSuite, make sure to first enable tax via the connection configuration page (Settings > Policies > Group > [Workspace Name] > Connections > Configure) and then sync the connection. Your tax rates will be imported from the accounting system and indicated by its logo. +## Not Connected to an Accounting Integration +If your Workspace is not connected to an accounting system, go to Settings > Policies > Group > [Workspace Name] > Tax to enable tax. + +# Tracking Tax by Expense Category +To set a different tax rate for a specific expense type in the Workspace currency, go to Settings > Workspaces > Group > [Workspace Name] > Categories page. Click "Edit Rules" next to the desired category and set the "Category default tax". This will be applied to new expenses, overriding the default Workspace currency tax rate. diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 00f3a4012664..b2dafa643b22 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -84,6 +84,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/components/OptionRow.js b/src/components/OptionRow.js index 8bc016faa6b5..70415ab03a13 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -212,7 +212,7 @@ class OptionRow extends Component { accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} hoverDimmingValue={1} hoverStyle={this.props.hoverStyle} - needsOffscreenAlphaCompositing={this.props.option.icons.length >= 2} + needsOffscreenAlphaCompositing={lodashGet(this.props.option, 'icons.length', 0) >= 2} > diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index c0dde899ad8e..35215cadd15d 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -233,7 +233,12 @@ function MoneyRequestPreview(props) { errorRowStyles={[styles.mbn1]} needsOffscreenAlphaCompositing > - + {hasReceipt && ( { if (isTaskCompleted) { - Task.reopenTask(props.taskReport, taskTitle); + Task.reopenTask(props.taskReport); } else { - Task.completeTask(props.taskReport, taskTitle); + Task.completeTask(props.taskReport); } })} accessibilityLabel={props.translate('task.task')} diff --git a/src/components/ReportActionItem/TaskView.js b/src/components/ReportActionItem/TaskView.js index 39807ab037d5..c52427ae1e8d 100644 --- a/src/components/ReportActionItem/TaskView.js +++ b/src/components/ReportActionItem/TaskView.js @@ -91,9 +91,9 @@ function TaskView(props) { { if (isCompleted) { - Task.reopenTask(props.report, taskTitle); + Task.reopenTask(props.report); } else { - Task.completeTask(props.report, taskTitle); + Task.completeTask(props.report); } })} isChecked={isCompleted} diff --git a/src/components/TaskHeaderActionButton.js b/src/components/TaskHeaderActionButton.js index aa7694095f5b..b22b5c92bf70 100644 --- a/src/components/TaskHeaderActionButton.js +++ b/src/components/TaskHeaderActionButton.js @@ -36,10 +36,8 @@ function TaskHeaderActionButton(props) { success isDisabled={ReportUtils.isCanceledTaskReport(props.report) || !Task.canModifyTask(props.report, props.session.accountID)} medium - text={props.translate(ReportUtils.isCompletedTaskReport(props.report) ? 'task.markAsIncomplete' : 'task.markAsDone')} - onPress={() => - ReportUtils.isCompletedTaskReport(props.report) ? Task.reopenTask(props.report, props.report.reportName) : Task.completeTask(props.report, props.report.reportName) - } + text={props.translate(ReportUtils.isCompletedTaskReport(props.report) ? 'task.markAsIncomplete' : 'task.markAsComplete')} + onPress={() => (ReportUtils.isCompletedTaskReport(props.report) ? Task.reopenTask(props.report) : Task.completeTask(props.report))} style={[styles.flex1]} /> diff --git a/src/languages/en.ts b/src/languages/en.ts index bd40b6ba391f..9c49e2907702 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1535,12 +1535,12 @@ export default { assignee: 'Assignee', completed: 'Completed', messages: { - completed: 'completed task', + completed: 'marked as complete', canceled: 'deleted task', - reopened: 'reopened task', + reopened: 'marked as incomplete', error: 'You do not have the permission to do the requested action.', }, - markAsDone: 'Mark as done', + markAsComplete: 'Mark as complete', markAsIncomplete: 'Mark as incomplete', assigneeError: 'There was an error assigning this task, please try another assignee.', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index bf6a05097961..2be3f9e96265 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1557,12 +1557,12 @@ export default { assignee: 'Usuario asignado', completed: 'Completada', messages: { - completed: 'tarea completada', + completed: 'marcada como completa', canceled: 'tarea eliminado', - reopened: 'tarea reabrir', + reopened: 'marcada como incompleta', error: 'No tiene permiso para realizar la acción solicitada.', }, - markAsDone: 'Marcar como completada', + markAsComplete: 'Marcar como completada', markAsIncomplete: 'Marcar como incompleta', assigneeError: 'Hubo un error al asignar esta tarea, inténtalo con otro usuario.', }, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 12424bf1e1f2..6636702592c0 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 533dbf51633a..bf069aba314e 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/libs/ReportUtils.js b/src/libs/ReportUtils.js index b7ad8476ae5f..f41ad0b75b42 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2551,7 +2551,7 @@ function buildOptimisticCreatedReportAction(emailCreatingAction) { { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: emailCreatingAction === currentUserEmail ? 'You' : emailCreatingAction, + text: emailCreatingAction, }, { type: CONST.REPORT.MESSAGE.TYPE.TEXT, @@ -2590,7 +2590,7 @@ function buildOptimisticEditedTaskReportAction(emailEditingTask) { { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: emailEditingTask === currentUserEmail ? 'You' : emailEditingTask, + text: emailEditingTask, }, { type: CONST.REPORT.MESSAGE.TYPE.TEXT, @@ -2631,7 +2631,7 @@ function buildOptimisticClosedReportAction(emailClosingReport, policyName, reaso { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: emailClosingReport === currentUserEmail ? 'You' : emailClosingReport, + text: emailClosingReport, }, { type: CONST.REPORT.MESSAGE.TYPE.TEXT, @@ -3579,8 +3579,8 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeEmail, assigneeAccountID // If you're choosing to share the task in the same DM as the assignee then we don't need to create another reportAction indicating that you've been assigned if (assigneeChatReportID !== parentReportID) { - optimisticAssigneeAddComment = buildOptimisticTaskCommentReportAction(taskReportID, title, assigneeEmail, assigneeAccountID, `Assigned a task to you: ${title}`, parentReportID); - + const displayname = lodashGet(allPersonalDetails, [assigneeAccountID, 'displayName']) || lodashGet(allPersonalDetails, [assigneeAccountID, 'login'], ''); + optimisticAssigneeAddComment = buildOptimisticTaskCommentReportAction(taskReportID, title, assigneeEmail, assigneeAccountID, `assigned to ${displayname}`, parentReportID); const lastAssigneeCommentText = formatReportLastMessageText(optimisticAssigneeAddComment.reportAction.message[0].text); const optimisticAssigneeReport = { lastVisibleActionCreated: currentTime, diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index a280947a97b5..7a32db660021 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -351,9 +351,9 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, const newName = lodashGet(lastAction, 'originalMessage.newName', ''); result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName}); } else if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED) { - result.alternateText = `${Localize.translate(preferredLocale, 'task.messages.reopened')}: ${report.reportName}`; + result.alternateText = `${Localize.translate(preferredLocale, 'task.messages.reopened')}`; } else if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED) { - result.alternateText = `${Localize.translate(preferredLocale, 'task.messages.completed')}: ${report.reportName}`; + result.alternateText = `${Localize.translate(preferredLocale, 'task.messages.completed')}`; } else if (lodashGet(lastAction, 'actionName', '') !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) { result.alternateText = `${lastActorDisplayName}: ${lastMessageText}`; } else { diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 963bfebb7eb2..91267b9b1053 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -70,7 +70,7 @@ function createTaskAndNavigate(parentReportID, title, description, assigneeEmail // Parent ReportAction indicating that a task has been created const optimisticTaskCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmail); - const optimisticAddCommentReport = ReportUtils.buildOptimisticTaskCommentReportAction(taskReportID, title, assigneeEmail, assigneeAccountID, `Created a task: ${title}`, parentReportID); + const optimisticAddCommentReport = ReportUtils.buildOptimisticTaskCommentReportAction(taskReportID, title, assigneeEmail, assigneeAccountID, `task for ${title}`, parentReportID); optimisticTaskReport.parentReportActionID = optimisticAddCommentReport.reportAction.reportActionID; const currentTime = DateUtils.getDBTime(); @@ -219,11 +219,10 @@ function createTaskAndNavigate(parentReportID, title, description, assigneeEmail /** * Complete a task * @param {Object} taskReport task report - * @param {String} taskTitle Title of the task */ -function completeTask(taskReport, taskTitle) { +function completeTask(taskReport) { const taskReportID = taskReport.reportID; - const message = `completed task: ${taskTitle}`; + const message = `marked as complete`; const completedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED, message); const optimisticData = [ @@ -303,11 +302,10 @@ function completeTask(taskReport, taskTitle) { /** * Reopen a closed task * @param {Object} taskReport task report - * @param {String} taskTitle Title of the task */ -function reopenTask(taskReport, taskTitle) { +function reopenTask(taskReport) { const taskReportID = taskReport.reportID; - const message = `reopened task: ${taskTitle}`; + const message = `marked as incomplete`; const reopenedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASKREOPENED, message); const optimisticData = [ @@ -924,7 +922,7 @@ function clearEditTaskErrors(reportID) { function getTaskReportActionMessage(actionName, reportID, isCreateTaskAction) { const report = ReportUtils.getReport(reportID); if (isCreateTaskAction) { - return `Created a task: ${report.reportName}`; + return `task for ${report.reportName}`; } let taskStatusText = ''; switch (actionName) { @@ -941,7 +939,7 @@ function getTaskReportActionMessage(actionName, reportID, isCreateTaskAction) { taskStatusText = Localize.translateLocal('task.task'); } - return `${taskStatusText} ${report.reportName}`; + return `${taskStatusText}`; } export { diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index f6adaf051e1c..de3e202bf7a3 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -98,8 +98,8 @@ function HeaderView(props) { threeDotMenuItems.push({ icon: Expensicons.Checkmark, iconFill: themeColors.icon, - text: props.translate('task.markAsDone'), - onSelected: () => Task.completeTask(props.report, title), + text: props.translate('task.markAsComplete'), + onSelected: () => Task.completeTask(props.report), }); } @@ -109,7 +109,7 @@ function HeaderView(props) { icon: Expensicons.Checkmark, iconFill: themeColors.icon, text: props.translate('task.markAsIncomplete'), - onSelected: () => Task.reopenTask(props.report, title), + onSelected: () => Task.reopenTask(props.report), }); } 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; diff --git a/src/styles/styles.js b/src/styles/styles.js index 8e90af0f027c..56868f930735 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3520,6 +3520,10 @@ const styles = (theme) => ({ backgroundColor: theme.border, }, + reportContainerBorderRadius: { + borderRadius: variables.componentBorderRadiusLarge, + }, + reportPreviewBoxBody: { padding: 16, },