diff --git a/assets/images/feed.svg b/assets/images/feed.svg
new file mode 100644
index 000000000000..2fd03eeadd00
--- /dev/null
+++ b/assets/images/feed.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/simple-illustrations/simple-illustration__rules.svg b/assets/images/simple-illustrations/simple-illustration__rules.svg
new file mode 100644
index 000000000000..6432f26d9ac6
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__rules.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/CONST.ts b/src/CONST.ts
index a9160e606bd9..5b22c60c4c78 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -86,6 +86,7 @@ const CONST = {
DEFAULT_TABLE_NAME: 'keyvaluepairs',
DEFAULT_ONYX_DUMP_FILE_NAME: 'onyx-state.txt',
DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL],
+ DISABLED_MAX_EXPENSE_VALUE: 10000000000,
// Note: Group and Self-DM excluded as these are not tied to a Workspace
WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT],
@@ -381,6 +382,7 @@ const CONST = {
REPORT_FIELDS_FEATURE: 'reportFieldsFeature',
WORKSPACE_FEEDS: 'workspaceFeeds',
NETSUITE_USA_TAX: 'netsuiteUsaTax',
+ WORKSPACE_RULES: 'workspaceRules',
},
BUTTON_STATES: {
DEFAULT: 'default',
@@ -2082,6 +2084,7 @@ const CONST = {
ARE_EXPENSIFY_CARDS_ENABLED: 'areExpensifyCardsEnabled',
ARE_INVOICES_ENABLED: 'areInvoicesEnabled',
ARE_TAXES_ENABLED: 'tax',
+ ARE_RULES_ENABLED: 'areRulesEnabled',
},
DEFAULT_CATEGORIES: [
'Advertising',
@@ -2415,6 +2418,7 @@ const CONST = {
WORKSPACE_BANK_ACCOUNT: 'WorkspaceBankAccount',
WORKSPACE_SETTINGS: 'WorkspaceSettings',
WORKSPACE_FEATURES: 'WorkspaceFeatures',
+ WORKSPACE_RULES: 'WorkspaceRules',
},
get EXPENSIFY_EMAILS() {
return [
@@ -5461,6 +5465,14 @@ const CONST = {
description: 'workspace.upgrade.taxCodes.description' as const,
icon: 'Coins',
},
+ rules: {
+ id: 'rules' as const,
+ alias: 'rules',
+ name: 'Rules',
+ title: 'workspace.upgrade.rules.title' as const,
+ description: 'workspace.upgrade.rules.description' as const,
+ icon: 'Rules',
+ },
};
},
REPORT_FIELD_TYPES: {
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index de495568daa3..282ac6f9f215 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -926,6 +926,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/expensify-card/settings/frequency',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/settings/frequency` as const,
},
+ WORKSPACE_RULES: {
+ route: 'settings/workspaces/:policyID/rules',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules` as const,
+ },
WORKSPACE_DISTANCE_RATES: {
route: 'settings/workspaces/:policyID/distance-rates',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 30adc5f89d08..dce7648ec671 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -440,6 +440,7 @@ const SCREENS = {
DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT: 'Distance_Rate_Tax_Reclaimable_On_Edit',
DISTANCE_RATE_TAX_RATE_EDIT: 'Distance_Rate_Tax_Rate_Edit',
UPGRADE: 'Workspace_Upgrade',
+ RULES: 'Policy_Rules',
},
EDIT_REQUEST: {
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index ea33af302670..b1adf360bae6 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -82,6 +82,7 @@ import ExpensifyLogoNew from '@assets/images/expensify-logo-new.svg';
import ExpensifyWordmark from '@assets/images/expensify-wordmark.svg';
import EyeDisabled from '@assets/images/eye-disabled.svg';
import Eye from '@assets/images/eye.svg';
+import Feed from '@assets/images/feed.svg';
import Filter from '@assets/images/filter.svg';
import Filters from '@assets/images/filters.svg';
import Flag from '@assets/images/flag.svg';
@@ -386,4 +387,5 @@ export {
Filters,
CalendarSolid,
Filter,
+ Feed,
};
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 9537e7a0a7a7..3b7b2068acd1 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -87,6 +87,7 @@ import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustra
import ReceiptLocationMarker from '@assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg';
import ReceiptWrangler from '@assets/images/simple-illustrations/simple-illustration__receipt-wrangler.svg';
import ReceiptUpload from '@assets/images/simple-illustrations/simple-illustration__receiptupload.svg';
+import Rules from '@assets/images/simple-illustrations/simple-illustration__rules.svg';
import SanFrancisco from '@assets/images/simple-illustrations/simple-illustration__sanfrancisco.svg';
import SendMoney from '@assets/images/simple-illustrations/simple-illustration__sendmoney.svg';
import ShieldYellow from '@assets/images/simple-illustrations/simple-illustration__shield.svg';
@@ -216,4 +217,5 @@ export {
Tire,
BigVault,
Filters,
+ Rules,
};
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 1684ed3057da..0c1e6773ee14 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -2122,6 +2122,7 @@ export default {
travel: 'Travel',
members: 'Members',
accounting: 'Accounting',
+ rules: 'Rules',
displayedAs: 'Displayed as',
plan: 'Plan',
profile: 'Profile',
@@ -2831,6 +2832,10 @@ export default {
title: 'Spend',
subtitle: 'Enable optional functionality that helps you scale your team.',
},
+ manageSection: {
+ title: 'Manage',
+ subtitle: 'Add controls that help keep spend within budget.',
+ },
earnSection: {
title: 'Earn',
subtitle: 'Enable optional functionality to streamline your revenue and get paid faster.',
@@ -2898,6 +2903,10 @@ export default {
disconnectText: "To disable accounting, you'll need to disconnect your accounting connection from your workspace.",
manageSettings: 'Manage settings',
},
+ rules: {
+ title: 'Rules',
+ subtitle: 'Configure when receipts are required, flag high spend, and more.',
+ },
},
reportFields: {
addField: 'Add field',
@@ -3537,6 +3546,11 @@ export default {
description: `Add tax codes to your taxes for easy export of expenses to your accounting and payroll systems.`,
onlyAvailableOnPlan: 'Tax codes are only available on the Control plan, starting at ',
},
+ rules: {
+ title: 'Rules',
+ description: `Rules run in the background and keep your spend under control so you don't have to sweat the small stuff.\n\nRequire expense details like receipts and descriptions, set limits and defaults, and automate approvals and payments – all in one place.`,
+ onlyAvailableOnPlan: 'Rules are only available on the Control plan, starting at ',
+ },
pricing: {
amount: '$9 ',
perActiveMember: 'per active member per month.',
@@ -3568,6 +3582,16 @@ export default {
chatInAdmins: 'Chat in #admins',
addPaymentCard: 'Add payment card',
},
+ rules: {
+ individualExpenseRules: {
+ title: 'Expenses',
+ subtitle: 'Set spend controls and defaults for individual expenses. You can also create rules for',
+ },
+ expenseReportRules: {
+ title: 'Expense reports',
+ subtitle: 'Automate expense report compliance, approvals, and payment.',
+ },
+ },
},
getAssistancePage: {
title: 'Get assistance',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index b4d071ba4a08..495443600faa 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2156,6 +2156,7 @@ export default {
travel: 'Viajes',
members: 'Miembros',
accounting: 'Contabilidad',
+ rules: 'Reglas',
plan: 'Plan',
profile: 'Perfil',
bankAccount: 'Cuenta bancaria',
@@ -2883,6 +2884,10 @@ export default {
title: 'Gasto',
subtitle: 'Habilita otras funcionalidades que ayudan a aumentar tu equipo.',
},
+ manageSection: {
+ title: 'Gestionar',
+ subtitle: 'Añade controles que ayudan a mantener los gastos dentro del presupuesto.',
+ },
earnSection: {
title: 'Gane',
subtitle: 'Habilita funciones opcionales para agilizar tus ingresos y recibir pagos más rápido.',
@@ -2950,6 +2955,10 @@ export default {
disconnectText: 'Para desactivar la contabilidad, desconecta tu conexión contable del espacio de trabajo.',
manageSettings: 'Gestionar la configuración',
},
+ rules: {
+ title: 'Reglas',
+ subtitle: 'Configura cuándo se exigen los recibos, marca los gastos elevados y mucho más.',
+ },
},
reportFields: {
addField: 'Añadir campo',
@@ -3590,6 +3599,11 @@ export default {
description: `Añada código de impuesto mayor a sus categorías para exportar fácilmente los gastos a sus sistemas de contabilidad y nómina.`,
onlyAvailableOnPlan: 'Los código de impuesto mayor solo están disponibles en el plan Control, a partir de ',
},
+ rules: {
+ title: 'Reglas',
+ description: `Las reglas se ejecutan en segundo plano y mantienen tus gastos bajo control para que no tengas que preocuparte por los detalles pequeños.\n\nExige detalles de los gastos, como recibos y descripciones, establece límites y valores predeterminados, y automatiza las aprobaciones y los pagos, todo en un mismo lugar.`,
+ onlyAvailableOnPlan: 'Las reglas están disponibles solo en el plan Control, que comienza en ',
+ },
note: {
upgradeWorkspace: 'Mejore su espacio de trabajo para acceder a esta función, o',
learnMore: 'más información',
@@ -3621,6 +3635,16 @@ export default {
chatInAdmins: 'Chatea en #admins',
addPaymentCard: 'Agregar tarjeta de pago',
},
+ rules: {
+ individualExpenseRules: {
+ title: 'Gastos',
+ subtitle: 'Establece controles y valores predeterminados para gastos individuales. También puedes crear reglas para',
+ },
+ expenseReportRules: {
+ title: 'Informes de gastos',
+ subtitle: 'Automatiza el cumplimiento, la aprobación y el pago de los informes de gastos.',
+ },
+ },
},
getAssistancePage: {
title: 'Obtener ayuda',
diff --git a/src/libs/API/parameters/SetPolicyRulesEnabledParams.ts b/src/libs/API/parameters/SetPolicyRulesEnabledParams.ts
new file mode 100644
index 000000000000..c748a98e4119
--- /dev/null
+++ b/src/libs/API/parameters/SetPolicyRulesEnabledParams.ts
@@ -0,0 +1,6 @@
+type SetPolicyRulesEnabledParams = {
+ policyID: string;
+ enabled: boolean;
+};
+
+export default SetPolicyRulesEnabledParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 53b58d224e17..3ad422db5997 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -272,6 +272,7 @@ export type {default as ExportSearchItemsToCSVParams} from './ExportSearchItemsT
export type {default as UpdateExpensifyCardLimitParams} from './UpdateExpensifyCardLimitParams';
export type {CreateWorkspaceApprovalParams, UpdateWorkspaceApprovalParams, RemoveWorkspaceApprovalParams} from './WorkspaceApprovalParams';
export type {default as StartIssueNewCardFlowParams} from './StartIssueNewCardFlowParams';
+export type {default as SetPolicyRulesEnabledParams} from './SetPolicyRulesEnabledParams';
export type {default as ConfigureExpensifyCardsForPolicyParams} from './ConfigureExpensifyCardsForPolicyParams';
export type {default as CreateExpensifyCardParams} from './CreateExpensifyCardParams';
export type {default as UpdateExpensifyCardTitleParams} from './UpdateExpensifyCardTitleParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index ef39e9bf005b..9fdc79d7750e 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -199,6 +199,7 @@ const WRITE_COMMANDS = {
ENABLE_POLICY_REPORT_FIELDS: 'EnablePolicyReportFields',
ENABLE_POLICY_EXPENSIFY_CARDS: 'EnablePolicyExpensifyCards',
ENABLE_POLICY_INVOICING: 'EnablePolicyInvoicing',
+ SET_POLICY_RULES_ENABLED: 'SetPolicyRulesEnabled',
SET_POLICY_TAXES_CURRENCY_DEFAULT: 'SetPolicyCurrencyDefaultTax',
SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT: 'SetPolicyForeignCurrencyDefaultTax',
SET_POLICY_CUSTOM_TAX_NAME: 'SetPolicyCustomTaxName',
@@ -527,6 +528,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS]: Parameters.EnablePolicyReportFieldsParams;
[WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS]: Parameters.EnablePolicyExpensifyCardsParams;
[WRITE_COMMANDS.ENABLE_POLICY_INVOICING]: Parameters.EnablePolicyInvoicingParams;
+ [WRITE_COMMANDS.SET_POLICY_RULES_ENABLED]: Parameters.SetPolicyRulesEnabledParams;
[WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams;
[WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams;
[WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
index 748d92b49a1c..077f42d32ec5 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
@@ -33,6 +33,7 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = {
[SCREENS.WORKSPACE.REPORT_FIELDS]: () => require('../../../../pages/workspace/reportFields/WorkspaceReportFieldsPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default,
[SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default,
+ [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPage').default,
} satisfies Screens;
function FullScreenNavigator() {
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 236b56882dde..731e0d1462ff 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1098,6 +1098,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.DISTANCE_RATES]: {
path: ROUTES.WORKSPACE_DISTANCE_RATES.route,
},
+ [SCREENS.WORKSPACE.RULES]: {
+ path: ROUTES.WORKSPACE_RULES.route,
+ },
},
},
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 8bd1c44568c4..91e6d5f631ca 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1194,6 +1194,9 @@ type FullScreenNavigatorParamList = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.RULES]: {
+ policyID: string;
+ };
};
type OnboardingModalNavigatorParamList = {
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index 15c15e113c8c..0a6756034f7d 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -36,6 +36,10 @@ function canUseNetSuiteUSATax(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.NETSUITE_USA_TAX) || canUseAllBetas(betas);
}
+function canUseWorkspaceRules(betas: OnyxEntry): boolean {
+ return !!betas?.includes(CONST.BETAS.WORKSPACE_RULES) || canUseAllBetas(betas);
+}
+
/**
* Link previews are temporarily disabled.
*/
@@ -52,4 +56,5 @@ export default {
canUseSpotnanaTravel,
canUseWorkspaceFeeds,
canUseNetSuiteUSATax,
+ canUseWorkspaceRules,
};
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index d914e47e5204..8128d3b9bb45 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -42,6 +42,7 @@ import type {
UpdateWorkspaceGeneralSettingsParams,
UpgradeToCorporateParams,
} from '@libs/API/parameters';
+import type SetPolicyRulesEnabledParams from '@libs/API/parameters/SetPolicyRulesEnabledParams';
import type UpdatePolicyAddressParams from '@libs/API/parameters/UpdatePolicyAddressParams';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import DateUtils from '@libs/DateUtils';
@@ -2937,6 +2938,68 @@ function enablePolicyWorkflows(policyID: string, enabled: boolean) {
}
}
+const DISABLED_MAX_EXPENSE_VALUES: Pick = {
+ maxExpenseAmountNoReceipt: CONST.DISABLED_MAX_EXPENSE_VALUE,
+ maxExpenseAmount: CONST.DISABLED_MAX_EXPENSE_VALUE,
+ maxExpenseAge: CONST.DISABLED_MAX_EXPENSE_VALUE,
+};
+
+function enablePolicyRules(policyID: string, enabled: boolean, disableRedirect = false) {
+ const policy = getPolicy(policyID);
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ areRulesEnabled: enabled,
+ ...(!enabled ? DISABLED_MAX_EXPENSE_VALUES : {}),
+ pendingFields: {
+ areRulesEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ pendingFields: {
+ areRulesEnabled: null,
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ areRulesEnabled: !enabled,
+ ...(!enabled
+ ? {
+ maxExpenseAmountNoReceipt: policy?.maxExpenseAmountNoReceipt,
+ maxExpenseAmount: policy?.maxExpenseAmount,
+ maxExpenseAge: policy?.maxExpenseAge,
+ }
+ : {}),
+ pendingFields: {
+ areRulesEnabled: null,
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters: SetPolicyRulesEnabledParams = {policyID, enabled};
+ API.write(WRITE_COMMANDS.SET_POLICY_RULES_ENABLED, parameters, onyxData);
+
+ if (enabled && getIsNarrowLayout() && !disableRedirect) {
+ navigateWhenEnableFeature(policyID);
+ }
+}
+
function enableDistanceRequestTax(policyID: string, customUnitName: string, customUnitID: string, attributes: Attributes) {
const policy = getPolicy(policyID);
const onyxData: OnyxData = {
@@ -3364,6 +3427,7 @@ export {
getAdminPoliciesConnectedToNetSuite,
getAdminPoliciesConnectedToSageIntacct,
hasInvoicingDetails,
+ enablePolicyRules,
};
export type {NewCustomUnit};
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 115a24691838..99b6ea9a7e82 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -16,6 +16,7 @@ import ScrollView from '@components/ScrollView';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
+import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -64,7 +65,8 @@ type WorkspaceMenuItem = {
| typeof SCREENS.WORKSPACE.PROFILE
| typeof SCREENS.WORKSPACE.MEMBERS
| typeof SCREENS.WORKSPACE.EXPENSIFY_CARD
- | typeof SCREENS.WORKSPACE.REPORT_FIELDS;
+ | typeof SCREENS.WORKSPACE.REPORT_FIELDS
+ | typeof SCREENS.WORKSPACE.RULES;
};
type WorkspaceInitialPageOnyxProps = {
@@ -99,6 +101,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc
const activeRoute = useNavigationState(getTopmostRouteName);
const {translate} = useLocalize();
const {isOffline} = useNetwork();
+ const {canUseWorkspaceRules} = usePermissions();
const wasRendered = useRef(false);
const prevPendingFields = usePrevious(policy?.pendingFields);
@@ -112,6 +115,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc
[CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED]: !!policy?.areConnectionsEnabled || !isEmptyObject(policy?.connections),
[CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED]: policy?.areExpensifyCardsEnabled,
[CONST.POLICY.MORE_FEATURES.ARE_REPORT_FIELDS_ENABLED]: policy?.areReportFieldsEnabled,
+ [CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED]: policy?.areRulesEnabled,
}),
[policy],
) as PolicyFeatureStates;
@@ -306,6 +310,15 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc
});
}
+ if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED] && canUseWorkspaceRules) {
+ protectedCollectPolicyMenuItems.push({
+ translationKey: 'workspace.common.rules',
+ icon: Expensicons.Feed,
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_RULES.getRoute(policyID)))),
+ routeName: SCREENS.WORKSPACE.RULES,
+ });
+ }
+
protectedCollectPolicyMenuItems.push({
translationKey: 'workspace.common.moreFeatures',
icon: Expensicons.Gear,
diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
index 47af86b53315..d33a83c4363c 100644
--- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
+++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
@@ -61,7 +61,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {translate} = useLocalize();
- const {canUseWorkspaceFeeds} = usePermissions();
+ const {canUseWorkspaceFeeds, canUseWorkspaceRules} = usePermissions();
const hasAccountingConnection = !isEmptyObject(policy?.connections);
const isAccountingEnabled = !!policy?.areConnectionsEnabled || !isEmptyObject(policy?.connections);
const isSyncTaxEnabled =
@@ -90,19 +90,6 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
DistanceRate.enablePolicyDistanceRates(policyID, isEnabled);
},
},
- {
- icon: Illustrations.Workflows,
- titleTranslationKey: 'workspace.moreFeatures.workflows.title',
- subtitleTranslationKey: 'workspace.moreFeatures.workflows.subtitle',
- isActive: policy?.areWorkflowsEnabled ?? false,
- pendingAction: policy?.pendingFields?.areWorkflowsEnabled,
- action: (isEnabled: boolean) => {
- if (!policyID) {
- return;
- }
- Policy.enablePolicyWorkflows(policyID, isEnabled);
- },
- },
];
// TODO remove this when feature will be fully done, and move spend item inside spendItems array
@@ -126,6 +113,44 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
});
}
+ const manageItems: Item[] = [
+ {
+ icon: Illustrations.Workflows,
+ titleTranslationKey: 'workspace.moreFeatures.workflows.title',
+ subtitleTranslationKey: 'workspace.moreFeatures.workflows.subtitle',
+ isActive: policy?.areWorkflowsEnabled ?? false,
+ pendingAction: policy?.pendingFields?.areWorkflowsEnabled,
+ action: (isEnabled: boolean) => {
+ if (!policyID) {
+ return;
+ }
+ Policy.enablePolicyWorkflows(policyID, isEnabled);
+ },
+ },
+ ];
+
+ // TODO remove this when feature will be fully done, and move manage item inside manageItems array
+ if (canUseWorkspaceRules) {
+ manageItems.splice(1, 0, {
+ icon: Illustrations.Rules,
+ titleTranslationKey: 'workspace.moreFeatures.rules.title',
+ subtitleTranslationKey: 'workspace.moreFeatures.rules.subtitle',
+ isActive: policy?.areRulesEnabled ?? false,
+ pendingAction: policy?.pendingFields?.areRulesEnabled,
+ action: (isEnabled: boolean) => {
+ if (!policyID) {
+ return;
+ }
+
+ if (isEnabled && !isControlPolicy(policy)) {
+ Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.rules.alias, ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)));
+ return;
+ }
+ Policy.enablePolicyRules(policyID, isEnabled);
+ },
+ });
+ }
+
const earnItems: Item[] = [
{
icon: Illustrations.InvoiceBlue,
@@ -262,6 +287,11 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitleTranslationKey: 'workspace.moreFeatures.spendSection.subtitle',
items: spendItems,
},
+ {
+ titleTranslationKey: 'workspace.moreFeatures.manageSection.title',
+ subtitleTranslationKey: 'workspace.moreFeatures.manageSection.subtitle',
+ items: manageItems,
+ },
{
titleTranslationKey: 'workspace.moreFeatures.earnSection.title',
subtitleTranslationKey: 'workspace.moreFeatures.earnSection.subtitle',
diff --git a/src/pages/workspace/rules/PolicyRulesPage.tsx b/src/pages/workspace/rules/PolicyRulesPage.tsx
new file mode 100644
index 000000000000..ec7cdffb8df5
--- /dev/null
+++ b/src/pages/workspace/rules/PolicyRulesPage.tsx
@@ -0,0 +1,106 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React from 'react';
+import {View} from 'react-native';
+import Section from '@components/Section';
+import Text from '@components/Text';
+import TextLink from '@components/TextLink';
+import useLocalize from '@hooks/useLocalize';
+import usePermissions from '@hooks/usePermissions';
+import usePolicy from '@hooks/usePolicy';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {FullScreenNavigatorParamList} from '@libs/Navigation/types';
+import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
+import * as Illustrations from '@src/components/Icon/Illustrations';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+
+type PolicyRulesPageProps = StackScreenProps;
+
+function PolicyRulesPage({route}: PolicyRulesPageProps) {
+ const {translate} = useLocalize();
+ const {policyID} = route.params;
+ const policy = usePolicy(policyID);
+ const styles = useThemeStyles();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const {canUseWorkspaceRules} = usePermissions();
+
+ const handleOnPressCategoriesLink = () => {
+ if (policy?.areCategoriesEnabled) {
+ Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID));
+ return;
+ }
+
+ Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID));
+ };
+
+ const handleOnPressTagsLink = () => {
+ if (policy?.areTagsEnabled) {
+ Navigation.navigate(ROUTES.WORKSPACE_TAGS.getRoute(policyID));
+ return;
+ }
+
+ Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID));
+ };
+
+ return (
+
+
+
+ (
+
+ {translate('workspace.rules.individualExpenseRules.subtitle')}{' '}
+
+ {translate('workspace.common.categories').toLowerCase()}
+ {' '}
+ {translate('common.and')}{' '}
+
+ {translate('workspace.common.tags').toLowerCase()}
+
+ .
+
+ )}
+ subtitle={translate('workspace.rules.individualExpenseRules.subtitle')}
+ titleStyles={styles.accountSettingsSectionTitle}
+ />
+
+
+
+
+ );
+}
+
+PolicyRulesPage.displayName = 'PolicyRulesPage';
+
+export default PolicyRulesPage;
diff --git a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
index 0837ac164600..bb4bdff27097 100644
--- a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
+++ b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
@@ -47,6 +47,9 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
case CONST.UPGRADE_FEATURE_INTRO_MAPPING.reportFields.id:
Policy.enablePolicyReportFields(policyID, true, true);
return Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID));
+ case CONST.UPGRADE_FEATURE_INTRO_MAPPING.rules.id:
+ Policy.enablePolicyRules(policyID, true, true);
+ return Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID));
default:
return route.params.backTo ? Navigation.navigate(route.params.backTo) : Navigation.goBack();
}
diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx
index dbd7a7cb3a58..91f069ac2224 100644
--- a/src/pages/workspace/withPolicy.tsx
+++ b/src/pages/workspace/withPolicy.tsx
@@ -45,6 +45,7 @@ type PolicyRoute = RouteProp<
| typeof SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE
| typeof SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS
| typeof SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION
+ | typeof SCREENS.WORKSPACE.RULES
>;
function getPolicyIDFromRoute(route: PolicyRoute): string {
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index 8f48205d8749..83f3e5082c9c 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -1510,6 +1510,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
/** Whether the workflows feature is enabled */
areWorkflowsEnabled?: boolean;
+ /** Whether the reules feature is enabled */
+ areRulesEnabled?: boolean;
+
/** Whether the Report Fields feature is enabled */
areReportFieldsEnabled?: boolean;