diff --git a/assets/images/tax.svg b/assets/images/tax.svg
new file mode 100644
index 000000000000..aa3c68e72ea8
--- /dev/null
+++ b/assets/images/tax.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index defb945ba8c2..ad2d9c10700b 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -585,6 +585,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/tags/edit',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/edit` as const,
},
+ WORKSPACE_TAXES: {
+ route: 'settings/workspaces/:policyID/taxes',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/taxes` as const,
+ },
WORKSPACE_MEMBER_DETAILS: {
route: 'settings/workspaces/:policyID/members/:accountID',
getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/members/${accountID}`, backTo),
@@ -597,7 +601,6 @@ const ROUTES = {
route: 'workspace/:policyID/distance-rates',
getRoute: (policyID: string) => `workspace/${policyID}/distance-rates` as const,
},
-
// Referral program promotion
REFERRAL_DETAILS_MODAL: {
route: 'referral/:contentType',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 4db5fd9115a5..6c742f08bfb7 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -219,6 +219,7 @@ const SCREENS = {
TAGS: 'Workspace_Tags',
TAGS_SETTINGS: 'Tags_Settings',
TAGS_EDIT: 'Tags_Edit',
+ TAXES: 'Workspace_Taxes',
TAG_CREATE: 'Tag_Create',
CURRENCY: 'Workspace_Profile_Currency',
WORKFLOWS: 'Workspace_Workflows',
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 3f1cde92a583..73a091815460 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -138,6 +138,7 @@ import Stopwatch from '@assets/images/stopwatch.svg';
import Sync from '@assets/images/sync.svg';
import Tag from '@assets/images/tag.svg';
import Task from '@assets/images/task.svg';
+import Tax from '@assets/images/tax.svg';
import ThreeDots from '@assets/images/three-dots.svg';
import ThumbsUp from '@assets/images/thumbs-up.svg';
import Transfer from '@assets/images/transfer.svg';
@@ -226,6 +227,7 @@ export {
Fullscreen,
Folder,
Tag,
+ Tax,
Gallery,
Gear,
Globe,
diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx
index 6eedc322f393..2af07701c5d3 100644
--- a/src/components/SelectionList/BaseListItem.tsx
+++ b/src/components/SelectionList/BaseListItem.tsx
@@ -80,9 +80,10 @@ function BaseListItem({
{item.isSelected && (
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index db6204c8c1ef..5ab4eea6404a 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -119,7 +119,8 @@ function BaseSelectionList(
});
// If disabled, add to the disabled indexes array
- if (!!section.isDisabled || item.isDisabled) {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ if (!!section.isDisabled || item.isDisabled || item.isDisabledCheckbox) {
disabledOptionsIndexes.push(disabledIndex);
}
disabledIndex += 1;
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 152a44996fea..08b10369e31f 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -60,6 +60,9 @@ type ListItem = {
/** Whether this option is selected */
isSelected?: boolean;
+ /** Whether the checkbox should be disabled */
+ isDisabledCheckbox?: boolean;
+
/** Whether this option is disabled for selection */
isDisabled?: boolean | null;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index c602b2fad14c..6ec5983583fc 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1741,6 +1741,7 @@ export default {
reimburse: 'Reimbursements',
categories: 'Categories',
tags: 'Tags',
+ taxes: 'Taxes',
bills: 'Bills',
invoices: 'Invoices',
travel: 'Travel',
@@ -1848,6 +1849,12 @@ export default {
existingTagError: 'A tag with this name already exists.',
genericFailureMessage: 'An error occurred while updating the tag, please try again.',
},
+ taxes: {
+ subtitle: 'Add tax names, rates, and set defaults.',
+ addRate: 'Add rate',
+ workspaceDefault: 'Workspace currency default',
+ foreignDefault: 'Foreign currency default',
+ },
emptyWorkspace: {
title: 'Create a workspace',
subtitle: 'Workspaces are where you’ll chat with your team, reimburse expenses, issue cards, send invoices, pay bills, and more - all in one place.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index bb54aa6e51f9..c2eb6374affa 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1765,6 +1765,7 @@ export default {
reimburse: 'Reembolsos',
categories: 'Categorías',
tags: 'Etiquetas',
+ taxes: 'Impuestos',
bills: 'Pagar facturas',
invoices: 'Enviar facturas',
travel: 'Viajes',
@@ -1872,6 +1873,12 @@ export default {
existingTagError: 'Ya existe una etiqueta con este nombre.',
genericFailureMessage: 'Se produjo un error al actualizar la etiqueta, inténtelo nuevamente.',
},
+ taxes: {
+ subtitle: 'Añade nombres, tasas y establezca valores por defecto para los impuestos.',
+ addRate: 'Añadir tasa',
+ workspaceDefault: 'Moneda por defecto del espacio de trabajo',
+ foreignDefault: 'Moneda extranjera por defecto',
+ },
emptyWorkspace: {
title: 'Crea un espacio de trabajo',
subtitle: 'En los espacios de trabajo podrás chatear con tu equipo, reembolsar gastos, emitir tarjetas, enviar y pagar facturas, y mucho más - todo en un mismo lugar.',
diff --git a/src/libs/API/parameters/OpenPolicyTaxesPageParams.ts b/src/libs/API/parameters/OpenPolicyTaxesPageParams.ts
new file mode 100644
index 000000000000..67c345e5db78
--- /dev/null
+++ b/src/libs/API/parameters/OpenPolicyTaxesPageParams.ts
@@ -0,0 +1,5 @@
+type OpenPolicyTaxesPageParams = {
+ policyID: string;
+};
+
+export default OpenPolicyTaxesPageParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index b594e555518a..25c336753203 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -174,5 +174,6 @@ export type {default as DeclineJoinRequestParams} from './DeclineJoinRequest';
export type {default as JoinPolicyInviteLinkParams} from './JoinPolicyInviteLink';
export type {default as OpenPolicyWorkflowsPageParams} from './OpenPolicyWorkflowsPageParams';
export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams';
+export type {default as OpenPolicyTaxesPageParams} from './OpenPolicyTaxesPageParams';
export type {default as OpenPolicyMoreFeaturesPageParams} from './OpenPolicyMoreFeaturesPageParams';
export type {default as CreatePolicyTagsParams} from './CreatePolicyTagsParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 24237dbb48bc..07f1ca09d7c5 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -376,6 +376,7 @@ const READ_COMMANDS = {
OPEN_WORKSPACE_MEMBERS_PAGE: 'OpenWorkspaceMembersPage',
OPEN_POLICY_CATEGORIES_PAGE: 'OpenPolicyCategoriesPage',
OPEN_POLICY_TAGS_PAGE: 'OpenPolicyTagsPage',
+ OPEN_POLICY_TAXES_PAGE: 'OpenPolicyTaxesPage',
OPEN_WORKSPACE_INVITE_PAGE: 'OpenWorkspaceInvitePage',
OPEN_DRAFT_WORKSPACE_REQUEST: 'OpenDraftWorkspaceRequest',
OPEN_POLICY_WORKFLOWS_PAGE: 'OpenPolicyWorkflowsPage',
@@ -415,6 +416,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.OPEN_WORKSPACE_MEMBERS_PAGE]: Parameters.OpenWorkspaceMembersPageParams;
[READ_COMMANDS.OPEN_POLICY_CATEGORIES_PAGE]: Parameters.OpenPolicyCategoriesPageParams;
[READ_COMMANDS.OPEN_POLICY_TAGS_PAGE]: Parameters.OpenPolicyTagsPageParams;
+ [READ_COMMANDS.OPEN_POLICY_TAXES_PAGE]: Parameters.OpenPolicyTaxesPageParams;
[READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE]: Parameters.OpenWorkspaceInvitePageParams;
[READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST]: Parameters.OpenDraftWorkspaceRequestParams;
[READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE]: Parameters.OpenPolicyWorkflowsPageParams;
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index 7df1da23d068..a4d7593cf750 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -199,6 +199,7 @@ const WorkspaceSettingsModalStackNavigator = createModalStackNavigator(
[SCREENS.WORKSPACE.CATEGORIES]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesPage').default as React.ComponentType,
[SCREENS.WORKSPACE.MORE_FEATURES]: () => require('../../../pages/workspace/WorkspaceMoreFeaturesPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAGS]: () => require('../../../pages/workspace/tags/WorkspaceTagsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.TAXES]: () => require('../../../pages/workspace/taxes/WorkspaceTaxesPage').default as React.ComponentType,
[SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default as React.ComponentType,
},
(styles) => ({cardStyle: styles.navigationScreenCardStyle, headerShown: false}),
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 1461c27e03e0..04bc53e7b542 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -582,6 +582,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.TAGS]: {
path: ROUTES.WORKSPACE_TAGS.route,
},
+ [SCREENS.WORKSPACE.TAXES]: {
+ path: ROUTES.WORKSPACE_TAXES.route,
+ },
[SCREENS.WORKSPACE.DISTANCE_RATES]: {
path: ROUTES.WORKSPACE_DISTANCE_RATES.route,
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index a1ec30fa303e..da418625ff55 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -540,6 +540,9 @@ type WorkspacesCentralPaneNavigatorParamList = {
policyID: string;
categoryName: string;
};
+ [SCREENS.WORKSPACE.TAXES]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.DISTANCE_RATES]: {
policyID: string;
};
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index 652de8aedf7d..2adcfd29e00d 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -27,6 +27,7 @@ import type {
OpenPolicyDistanceRatesPageParams,
OpenPolicyMoreFeaturesPageParams,
OpenPolicyTagsPageParams,
+ OpenPolicyTaxesPageParams,
OpenPolicyWorkflowsPageParams,
OpenWorkspaceInvitePageParams,
OpenWorkspaceMembersPageParams,
@@ -2109,6 +2110,19 @@ function openPolicyTagsPage(policyID: string) {
API.read(READ_COMMANDS.OPEN_POLICY_TAGS_PAGE, params);
}
+function openPolicyTaxesPage(policyID: string) {
+ if (!policyID) {
+ Log.warn('openPolicyTaxesPage invalid params', {policyID});
+ return;
+ }
+
+ const params: OpenPolicyTaxesPageParams = {
+ policyID,
+ };
+
+ API.read(READ_COMMANDS.OPEN_POLICY_TAXES_PAGE, params);
+}
+
function openWorkspaceInvitePage(policyID: string, clientMemberEmails: string[]) {
if (!policyID || !clientMemberEmails) {
Log.warn('openWorkspaceInvitePage invalid params', {policyID, clientMemberEmails});
@@ -3342,6 +3356,10 @@ function enablePolicyTaxes(policyID: string, enabled: boolean) {
const parameters: EnablePolicyTaxesParams = {policyID, enabled};
API.write(WRITE_COMMANDS.ENABLE_POLICY_TAXES, parameters, onyxData);
+
+ if (enabled) {
+ navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_TAXES.getRoute(policyID));
+ }
}
function enablePolicyWorkflows(policyID: string, enabled: boolean) {
@@ -3530,6 +3548,7 @@ export {
openWorkspaceMembersPage,
openPolicyCategoriesPage,
openPolicyTagsPage,
+ openPolicyTaxesPage,
openWorkspaceInvitePage,
openWorkspace,
removeWorkspace,
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index c1b3a490fec0..871bb2cf7980 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -188,6 +188,15 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
});
}
+ if (policy?.tax?.trackingEnabled) {
+ protectedCollectPolicyMenuItems.push({
+ translationKey: 'workspace.common.taxes',
+ icon: Expensicons.Tax,
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAXES.getRoute(policyID)))),
+ routeName: SCREENS.WORKSPACE.TAXES,
+ });
+ }
+
protectedCollectPolicyMenuItems.push({
translationKey: 'workspace.common.moreFeatures',
icon: Expensicons.Gear,
diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
index 457c96ac2fd7..2c8123670e0b 100644
--- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
+++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
@@ -88,6 +88,16 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
Policy.enablePolicyTags(policy?.id ?? '', isEnabled);
},
},
+ {
+ icon: Illustrations.Coins,
+ titleTranslationKey: 'workspace.moreFeatures.taxes.title',
+ subtitleTranslationKey: 'workspace.moreFeatures.taxes.subtitle',
+ isActive: policy?.tax?.trackingEnabled ?? false,
+ pendingAction: policy?.pendingFields?.tax,
+ action: (isEnabled: boolean) => {
+ Policy.enablePolicyTaxes(policy?.id ?? '', isEnabled);
+ },
+ },
];
const sections: SectionObject[] = [
diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
new file mode 100644
index 000000000000..18123d109645
--- /dev/null
+++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
@@ -0,0 +1,195 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import {ActivityIndicator, View} from 'react-native';
+import Button from '@components/Button';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import * as Illustrations from '@components/Icon/Illustrations';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import TableListItem from '@components/SelectionList/TableListItem';
+import type {ListItem} from '@components/SelectionList/types';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import {openPolicyTaxesPage} from '@libs/actions/Policy';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import CONST from '@src/CONST';
+import type SCREENS from '@src/SCREENS';
+
+type WorkspaceTaxesPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
+
+function WorkspaceTaxesPage({policy, route}: WorkspaceTaxesPageProps) {
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const {translate} = useLocalize();
+ const [selectedTaxesIDs, setSelectedTaxesIDs] = useState([]);
+ const defaultExternalID = policy?.taxRates?.defaultExternalID;
+ const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault;
+
+ const fetchTaxes = () => {
+ openPolicyTaxesPage(route.params.policyID);
+ };
+
+ const {isOffline} = useNetwork({onReconnect: fetchTaxes});
+
+ useEffect(() => {
+ fetchTaxes();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const textForDefault = useCallback(
+ (taxID: string): string => {
+ if (taxID === defaultExternalID && taxID === foreignTaxDefault) {
+ return translate('common.default');
+ }
+ if (taxID === defaultExternalID) {
+ return translate('workspace.taxes.workspaceDefault');
+ }
+ if (taxID === foreignTaxDefault) {
+ return translate('workspace.taxes.foreignDefault');
+ }
+ return '';
+ },
+ [defaultExternalID, foreignTaxDefault, translate],
+ );
+
+ const taxesList = useMemo(
+ () =>
+ Object.entries(policy?.taxRates?.taxes ?? {})
+ .map(([key, value]) => ({
+ text: value.name,
+ alternateText: textForDefault(key),
+ keyForList: key,
+ isSelected: !!selectedTaxesIDs.includes(key),
+ isDisabledCheckbox: key === defaultExternalID,
+ rightElement: (
+
+
+ {value.isDisabled ? translate('workspace.common.disabled') : translate('workspace.common.enabled')}
+
+
+
+
+
+ ),
+ }))
+ .sort((a, b) => a.text.localeCompare(b.text)),
+ [policy?.taxRates?.taxes, textForDefault, defaultExternalID, selectedTaxesIDs, styles, theme.icon, translate],
+ );
+
+ const isLoading = !isOffline && taxesList === undefined;
+
+ const toggleTax = (tax: ListItem) => {
+ const key = tax.keyForList;
+ if (typeof key !== 'string') {
+ return;
+ }
+
+ setSelectedTaxesIDs((prev) => {
+ if (prev?.includes(key)) {
+ return prev.filter((item) => item !== key);
+ }
+ return [...prev, key];
+ });
+ };
+
+ const toggleAllTaxes = () => {
+ const taxesToSelect = taxesList.filter((tax) => tax.keyForList !== defaultExternalID);
+ setSelectedTaxesIDs((prev) => {
+ if (prev.length === taxesToSelect.length) {
+ return [];
+ }
+
+ return taxesToSelect.map((item) => (item.keyForList ? item.keyForList : ''));
+ });
+ };
+
+ const getCustomListHeader = () => (
+
+ {translate('common.name')}
+ {translate('statusPage.status')}
+
+ );
+
+ const headerButtons = (
+
+
+ );
+
+ return (
+
+
+
+
+ {!isSmallScreenWidth && headerButtons}
+
+
+ {isSmallScreenWidth && {headerButtons}}
+
+
+ {translate('workspace.taxes.subtitle')}
+
+ {isLoading && (
+
+ )}
+ {}}
+ onSelectAll={toggleAllTaxes}
+ showScrollIndicator
+ ListItem={TableListItem}
+ customListHeader={getCustomListHeader()}
+ listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
+ />
+
+
+
+ );
+}
+
+WorkspaceTaxesPage.displayName = 'WorkspaceTaxesPage';
+
+export default withPolicyAndFullscreenLoading(WorkspaceTaxesPage);