diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableHeaderButtons.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableHeaderButtons.tsx
index 6ea2263a442a..f66ffe56e5fb 100644
--- a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableHeaderButtons.tsx
+++ b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableHeaderButtons.tsx
@@ -42,11 +42,14 @@ type WorkspaceCompanyCardsTableHeaderButtonsProps = {
/** Whether to show the table controls */
showTableControls: boolean;
+ /** Whether the current member can edit company cards */
+ canWriteCompanyCards: boolean;
+
/** Card feed icon */
CardFeedIcon: React.ReactNode;
};
-function WorkspaceCompanyCardsTableHeaderButtons({policyID, feedName, isLoading, showTableControls, CardFeedIcon}: WorkspaceCompanyCardsTableHeaderButtonsProps) {
+function WorkspaceCompanyCardsTableHeaderButtons({policyID, feedName, isLoading, showTableControls, canWriteCompanyCards, CardFeedIcon}: WorkspaceCompanyCardsTableHeaderButtonsProps) {
const styles = useThemeStyles();
const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();
@@ -162,22 +165,24 @@ function WorkspaceCompanyCardsTableHeaderButtons({policyID, feedName, isLoading,
{!isLoading && (
<>
{showTableControls &&
}
- {}}
- shouldUseOptionIcon
- customText={translate('common.more')}
- options={secondaryActions}
- isSplitButton={false}
- wrapperStyle={shouldShowNarrowLayout ? styles.flex1 : styles.flexGrow0}
- sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.COMPANY_CARDS.MORE_DROPDOWN}
- />
+ {canWriteCompanyCards && (
+ {}}
+ shouldUseOptionIcon
+ customText={translate('common.more')}
+ options={secondaryActions}
+ isSplitButton={false}
+ wrapperStyle={shouldShowNarrowLayout ? styles.flex1 : styles.flexGrow0}
+ sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.COMPANY_CARDS.MORE_DROPDOWN}
+ />
+ )}
>
)}
- {!isLoading && (isFeedConnectionBroken || hasFeedErrors) && (
+ {!isLoading && canWriteCompanyCards && (isFeedConnectionBroken || hasFeedErrors) && (
void;
};
-function WorkspaceCompanyCardTableRow({item, policyID, CardFeedIcon, shouldUseNarrowTableLayout, rowIndex, isAssigningCardDisabled, onAssignCard}: WorkspaceCompanyCardTableRowProps) {
+function WorkspaceCompanyCardTableRow({
+ item,
+ policyID,
+ CardFeedIcon,
+ shouldUseNarrowTableLayout,
+ rowIndex,
+ isAssigningCardDisabled,
+ canWriteCompanyCards,
+ onAssignCard,
+}: WorkspaceCompanyCardTableRowProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {isOffline} = useNetwork();
@@ -94,14 +106,21 @@ function WorkspaceCompanyCardTableRow({item, policyID, CardFeedIcon, shouldUseNa
? {width: variables.cardAvatarWidth, height: variables.cardAvatarHeight}
: {width: variables.cardAvatarWidthSmall, height: variables.cardAvatarHeightSmall};
+ const canOpenCardDetails = !!assignedCard?.accountID && !!assignedCard?.fundID && assignedCard?.cardID !== undefined;
+ const canAssignCard = !isAssigned && canWriteCompanyCards && !isAssigningCardDisabled;
+ const canPressRow = canOpenCardDetails || canAssignCard;
+
const handleRowPress = () => {
if (!assignedCard) {
+ if (!canAssignCard) {
+ return;
+ }
onAssignCard(cardName, encryptedCardNumber);
return;
}
- if (!assignedCard?.accountID || !assignedCard?.fundID) {
+ if (!canOpenCardDetails || assignedCard.cardID === undefined) {
return;
}
@@ -115,7 +134,7 @@ function WorkspaceCompanyCardTableRow({item, policyID, CardFeedIcon, shouldUseNa
interactive
rowIndex={rowIndex}
isLoading={isDeleting}
- disabled={isCardDeleted || isAssigningCardDisabled}
+ disabled={isCardDeleted || !canPressRow}
skeletonReasonAttributes={reasonAttributes}
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.COMPANY_CARDS.TABLE_ITEM}
LoadingComponent={WorkspaceCompanyCardsTableSkeleton}
@@ -175,7 +194,7 @@ function WorkspaceCompanyCardTableRow({item, policyID, CardFeedIcon, shouldUseNa
)}
- {!isAssigned && (
+ {!isAssigned && canWriteCompanyCards && (
)}
-
+ {canPressRow && (
+
+ )}
>
)}
diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx
index ed57d777458c..02eb3349d830 100644
--- a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx
+++ b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx
@@ -51,6 +51,9 @@ type WorkspaceCompanyCardsTableProps = {
/** Whether to disable assign card button */
isAssigningCardDisabled: boolean;
+ /** Whether the current member can edit company cards */
+ canWriteCompanyCards: boolean;
+
/** On assign card callback */
onAssignCard: (cardID: string, encryptedCardNumber: string) => void;
@@ -68,6 +71,7 @@ function WorkspaceCompanyCardsTable({
companyCards,
onAssignCard,
isAssigningCardDisabled,
+ canWriteCompanyCards,
onReloadPage,
onReloadFeed,
}: WorkspaceCompanyCardsTableProps) {
@@ -294,6 +298,7 @@ function WorkspaceCompanyCardsTable({
CardFeedIcon={cardFeedIcon}
onAssignCard={onAssignCard}
isAssigningCardDisabled={isAssigningCardDisabled}
+ canWriteCompanyCards={canWriteCompanyCards}
shouldUseNarrowTableLayout={shouldUseNarrowTableLayout}
/>
);
@@ -337,6 +342,7 @@ function WorkspaceCompanyCardsTable({
policyID={policyID}
feedName={feedName}
showTableControls={showTableControls}
+ canWriteCompanyCards={canWriteCompanyCards}
CardFeedIcon={cardFeedIcon}
/>
@@ -397,6 +403,7 @@ function WorkspaceCompanyCardsTable({
)}
diff --git a/src/hooks/usePolicyFeatureWriteAccess.ts b/src/hooks/usePolicyFeatureWriteAccess.ts
new file mode 100644
index 000000000000..ed2cf02243c1
--- /dev/null
+++ b/src/hooks/usePolicyFeatureWriteAccess.ts
@@ -0,0 +1,34 @@
+import {canMemberWrite} from '@libs/PolicyUtils';
+import type {PolicyFeature} from '@libs/PolicyUtils';
+import type {OnyxInputOrEntry, Policy} from '@src/types/onyx';
+import useConfirmModal from './useConfirmModal';
+import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
+import useLocalize from './useLocalize';
+
+function usePolicyFeatureWriteAccess(policy: OnyxInputOrEntry, feature: PolicyFeature) {
+ const {translate} = useLocalize();
+ const {showConfirmModal} = useConfirmModal();
+ const {login: currentUserLogin = ''} = useCurrentUserPersonalDetails();
+ const canWrite = canMemberWrite(policy, currentUserLogin, feature);
+
+ const showReadOnlyModal = () => {
+ showConfirmModal({
+ title: translate('workspace.common.readOnlyActionTitle'),
+ prompt: translate('workspace.common.readOnlyActionPrompt'),
+ confirmText: translate('common.buttonConfirm'),
+ shouldShowCancelButton: false,
+ });
+ };
+
+ const getReadOnlyDisabledAction = (disabledAction?: () => void | Promise) => {
+ if (!canWrite) {
+ return showReadOnlyModal;
+ }
+
+ return disabledAction;
+ };
+
+ return {canWrite, showReadOnlyModal, getReadOnlyDisabledAction};
+}
+
+export default usePolicyFeatureWriteAccess;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 828eadd80c8c..f68f52cdc636 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -4360,6 +4360,8 @@ const translations = {
unavailable: 'Unavailable workspace',
memberNotFound: 'Member not found. To invite a new member to the workspace, please use the invite button above.',
notAuthorized: `You don't have access to this page. If you're trying to join this workspace, just ask the workspace owner to add you as a member. Something else? Reach out to ${CONST.EMAIL.CONCIERGE}.`,
+ readOnlyActionTitle: 'Not so fast...',
+ readOnlyActionPrompt: "Your workspace role can view these settings, but can't edit them.",
goToWorkspace: 'Go to workspace',
duplicateWorkspace: 'Duplicate workspace',
duplicateWorkspacePrefix: 'Duplicate',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index daf55d4bc1d3..2def7ad2508c 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -4162,6 +4162,8 @@ ${amount} para ${merchant} - ${date}`,
unavailable: 'Espacio de trabajo no disponible',
memberNotFound: 'Miembro no encontrado. Para invitar a un nuevo miembro al espacio de trabajo, por favor, utiliza el botón invitar que está arriba.',
notAuthorized: `No tienes acceso a esta página. Si estás intentando unirte a este espacio de trabajo, pide al dueño del espacio de trabajo que te añada como miembro. ¿Necesitas algo más? Comunícate con ${CONST.EMAIL.CONCIERGE}`,
+ readOnlyActionTitle: 'No tan rápido...',
+ readOnlyActionPrompt: 'Tu rol en el espacio de trabajo puede ver esta configuración, pero no editarla.',
goToWorkspace: 'Ir al espacio de trabajo',
duplicateWorkspace: 'Duplicar espacio de trabajo',
duplicateWorkspacePrefix: 'Duplicar',
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index f0e7780a1fa9..1e742d26c88a 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -191,8 +191,18 @@ const ROLE_PERMISSION_BUNDLES: Record controlPolicyOnlyRole === role);
+}
+
function hasPolicyFeaturePermission(policy: OnyxInputOrEntry, login: string, feature: PolicyFeature, requiredAccess: PolicyFeatureAccess): boolean {
- const role = getPolicyRole(policy, login, false);
+ const role = getPolicyRole(policy, login, !login);
+ if (isControlPolicyOnlyRole(role) && (!policy || !isControlPolicy(policy))) {
+ return false;
+ }
+
const access = role ? ROLE_PERMISSION_BUNDLES[role]?.[feature] : undefined;
if (requiredAccess === CONST.POLICY.POLICY_FEATURE_ACCESS.READ) {
diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx
index 722c933fe9bf..0fa296a87b49 100644
--- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx
+++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx
@@ -187,7 +187,8 @@ function AccessOrNotFoundWrapper({
const {isOffline} = useNetwork();
const isReportArchived = useReportIsArchived(report?.reportID);
- const isPageAccessible = accessVariants.reduce((acc, variant) => {
+ const accessVariantsToCheck = policyFeature ? accessVariants.filter((variant) => variant !== CONST.POLICY.ACCESS_VARIANTS.ADMIN) : accessVariants;
+ const isPageAccessible = accessVariantsToCheck.reduce((acc, variant) => {
const accessFunction = ACCESS_VARIANTS[variant];
if (variant === CONST.IOU.ACCESS_VARIANTS.CREATE) {
return acc && accessFunction(policy, login, report, allPolicies ?? null, betas, iouType, isReportArchived, isRestrictedToPreferredPolicy);
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 74cc2a133349..9f278b15bea0 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -34,6 +34,7 @@ import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import {
canEditWorkspaceSettings,
+ canMemberRead,
canPolicyAccessFeature,
shouldShowPolicy as checkIfShouldShowPolicy,
goBackFromInvalidPolicy,
@@ -47,6 +48,7 @@ import {
shouldShowSyncError,
shouldShowTaxRateError,
} from '@libs/PolicyUtils';
+import type {PolicyFeature} from '@libs/PolicyUtils';
import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils';
import type WORKSPACE_TO_RHP from '@navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP';
import type {WorkspaceSplitNavigatorParamList} from '@navigation/types';
@@ -137,7 +139,25 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
const policyName = policy?.name ?? '';
const hasPolicyCreationError = policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyObject(policy.errors);
- const shouldShowProtectedItems = canEditWorkspaceSettings(policy);
+ const canWriteWorkspaceSettings = canEditWorkspaceSettings(policy, currentUserLogin);
+ const canReadPolicyFeature = (policyFeature: PolicyFeature) => canMemberRead(policy, currentUserLogin ?? '', policyFeature);
+ const canReadMoreFeatures = canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.MORE_FEATURES);
+ const shouldShowProtectedItems =
+ canWriteWorkspaceSettings ||
+ [
+ CONST.POLICY.POLICY_FEATURE.REPORT_FIELDS,
+ CONST.POLICY.POLICY_FEATURE.ACCOUNTING,
+ CONST.POLICY.POLICY_FEATURE.CATEGORIES,
+ CONST.POLICY.POLICY_FEATURE.TAGS,
+ CONST.POLICY.POLICY_FEATURE.TAXES,
+ CONST.POLICY.POLICY_FEATURE.WORKFLOWS,
+ CONST.POLICY.POLICY_FEATURE.RULES,
+ CONST.POLICY.POLICY_FEATURE.DISTANCE_RATES,
+ CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD,
+ CONST.POLICY.POLICY_FEATURE.COMPANY_CARDS,
+ CONST.POLICY.POLICY_FEATURE.PER_DIEM,
+ CONST.POLICY.POLICY_FEATURE.MORE_FEATURES,
+ ].some(canReadPolicyFeature);
const accountingConnectionNames = CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES;
const hasSyncError = shouldShowSyncError(policy, isConnectionInProgress(connectionSyncProgress, policy), accountingConnectionNames);
@@ -245,15 +265,17 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
}
if (isGroupPolicy(policy) && shouldShowProtectedItems) {
- workspaceMenuItems.push({
- translationKey: 'common.reports',
- icon: expensifyIcons.Document,
- action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REPORTS.getRoute(policyID)))),
- screenName: SCREENS.WORKSPACE.REPORTS,
- sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.REPORTS,
- });
+ if (canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.REPORT_FIELDS)) {
+ workspaceMenuItems.push({
+ translationKey: 'common.reports',
+ icon: expensifyIcons.Document,
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REPORTS.getRoute(policyID)))),
+ screenName: SCREENS.WORKSPACE.REPORTS,
+ sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.REPORTS,
+ });
+ }
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.ACCOUNTING)) {
workspaceMenuItems.push({
translationKey: 'workspace.common.accounting',
icon: expensifyIcons.Sync,
@@ -265,7 +287,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED] && canReadMoreFeatures) {
workspaceMenuItems.push({
translationKey: 'workspace.common.hr',
icon: expensifyIcons.Users,
@@ -276,7 +298,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED] && canReadMoreFeatures) {
workspaceMenuItems.push({
translationKey: 'workspace.common.receiptPartners',
brickRoadIndicator: shouldShowEnterCredentialsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
@@ -288,7 +310,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.CATEGORIES)) {
workspaceMenuItems.push({
translationKey: 'workspace.common.categories',
icon: expensifyIcons.Folder,
@@ -300,7 +322,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.TAGS)) {
workspaceMenuItems.push({
translationKey: 'workspace.common.tags',
icon: expensifyIcons.Tag,
@@ -311,7 +333,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.TAXES)) {
workspaceMenuItems.push({
translationKey: 'workspace.common.taxes',
icon: expensifyIcons.Coins,
@@ -323,7 +345,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_WORKFLOWS_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_WORKFLOWS_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.WORKFLOWS)) {
workspaceMenuItems.push({
translationKey: 'workspace.common.workflows',
icon: expensifyIcons.Workflows,
@@ -335,7 +357,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.RULES)) {
workspaceMenuItems.push({
translationKey: 'workspace.common.rules',
icon: expensifyIcons.Feed,
@@ -346,7 +368,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.DISTANCE_RATES)) {
workspaceMenuItems.push({
translationKey: 'workspace.common.distanceRates',
icon: expensifyIcons.Car,
@@ -357,7 +379,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.IS_TRAVEL_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.IS_TRAVEL_ENABLED] && canReadMoreFeatures) {
workspaceMenuItems.push({
translationKey: 'workspace.common.travel',
icon: expensifyIcons.LuggageWithLines,
@@ -368,7 +390,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD)) {
workspaceMenuItems.push({
translationKey: 'workspace.common.expensifyCard',
icon: expensifyIcons.ExpensifyCard,
@@ -379,7 +401,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_COMPANY_CARDS_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_COMPANY_CARDS_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.COMPANY_CARDS)) {
workspaceMenuItems.push({
translationKey: 'workspace.common.companyCards',
icon: expensifyIcons.CreditCard,
@@ -391,7 +413,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_RATES_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_RATES_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.PER_DIEM)) {
workspaceMenuItems.push({
translationKey: 'common.perDiem',
icon: expensifyIcons.CalendarSolid,
@@ -402,7 +424,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.IS_TIME_TRACKING_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.IS_TIME_TRACKING_ENABLED] && canReadMoreFeatures) {
workspaceMenuItems.push({
translationKey: 'iou.time',
icon: expensifyIcons.Clock,
@@ -413,7 +435,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_INVOICES_ENABLED]) {
+ if (policyFeatureStates?.[CONST.POLICY.MORE_FEATURES.ARE_INVOICES_ENABLED] && canReadMoreFeatures) {
const currencyCode = policy?.outputCurrency ?? CONST.CURRENCY.USD;
workspaceMenuItems.push({
translationKey: 'workspace.common.invoices',
@@ -426,13 +448,15 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
});
}
- workspaceMenuItems.push({
- translationKey: 'workspace.common.moreFeatures',
- icon: expensifyIcons.Gear,
- action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)))),
- screenName: SCREENS.WORKSPACE.MORE_FEATURES,
- sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.MORE_FEATURES,
- });
+ if (canReadMoreFeatures) {
+ workspaceMenuItems.push({
+ translationKey: 'workspace.common.moreFeatures',
+ icon: expensifyIcons.Gear,
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)))),
+ screenName: SCREENS.WORKSPACE.MORE_FEATURES,
+ sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.MORE_FEATURES,
+ });
+ }
}
// Close RHP if we land on a route that no longer exists in the menu
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index 81b020baae7c..fb8acdac3a0a 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -977,6 +977,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers
shouldUseHeadlineHeader={!selectionModeHeader}
shouldShowOfflineIndicatorInWideScreen
shouldShowNonAdmin
+ policyFeature={CONST.POLICY.POLICY_FEATURE.MEMBERS}
onBackButtonPress={() => {
if (isMobileSelectionModeEnabled) {
setSelectedEmployees([]);
diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx
index e40601a63ae7..bbd03a9efd82 100644
--- a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx
+++ b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx
@@ -16,6 +16,7 @@ import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import usePolicyData from '@hooks/usePolicyData';
+import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle';
@@ -131,6 +132,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
const isSmartLimitEnabled = isSmartLimitEnabledUtil(workspaceCards);
const settings = getCardSettings(cardSettings);
const paymentBankAccountID = settings?.paymentBankAccountID;
+ const {canWrite: canWriteMoreFeatures, getReadOnlyDisabledAction} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.MORE_FEATURES);
const warnAccountingManagesOrganizeFeature = async () => {
if (!hasAccountingConnection || !policyID) {
@@ -248,6 +250,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
{
if (!policyID) {
return;
@@ -305,8 +308,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.receiptPartners.subtitle')}
isActive={policy?.receiptPartners?.enabled ?? false}
pendingAction={policy?.pendingFields?.receiptPartners}
- disabled={isUberConnected}
- disabledAction={warnReceiptPartnersStillConnected}
+ disabled={!canWriteMoreFeatures || isUberConnected}
+ disabledAction={getReadOnlyDisabledAction(warnReceiptPartnersStillConnected)}
onToggle={(isEnabled) => {
if (!policyID) {
return;
@@ -334,8 +337,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.hr.subtitle')}
isActive={((policy?.isHREnabled === true || isAnyHRConnected(policy)) && canPolicyAccessFeature(policy, CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED)) ?? false}
pendingAction={policy?.pendingFields?.isHREnabled}
- disabled={isAnyHRConnected(policy)}
- disabledAction={warnDisconnectHRFirst}
+ disabled={!canWriteMoreFeatures || isAnyHRConnected(policy)}
+ disabledAction={getReadOnlyDisabledAction(warnDisconnectHRFirst)}
onToggle={(isEnabled) => {
if (!policyID) {
return;
@@ -365,8 +368,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.categories.subtitle')}
isActive={policy?.areCategoriesEnabled ?? false}
pendingAction={policy?.pendingFields?.areCategoriesEnabled}
- disabled={hasAccountingConnection}
- disabledAction={warnAccountingManagesOrganizeFeature}
+ disabled={!canWriteMoreFeatures || hasAccountingConnection}
+ disabledAction={getReadOnlyDisabledAction(warnAccountingManagesOrganizeFeature)}
onToggle={(isEnabled) => {
if (!policyID) {
return;
@@ -386,8 +389,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.tags.subtitle')}
isActive={policy?.areTagsEnabled ?? false}
pendingAction={policy?.pendingFields?.areTagsEnabled}
- disabled={hasAccountingConnection}
- disabledAction={warnAccountingManagesOrganizeFeature}
+ disabled={!canWriteMoreFeatures || hasAccountingConnection}
+ disabledAction={getReadOnlyDisabledAction(warnAccountingManagesOrganizeFeature)}
onToggle={(isEnabled) => {
enablePolicyTags(policyData, isEnabled);
}}
@@ -404,8 +407,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.taxes.subtitle')}
isActive={(policy?.tax?.trackingEnabled ?? false) || isSyncTaxEnabled}
pendingAction={policy?.pendingFields?.tax}
- disabled={hasAccountingConnection}
- disabledAction={warnAccountingManagesOrganizeFeature}
+ disabled={!canWriteMoreFeatures || hasAccountingConnection}
+ disabledAction={getReadOnlyDisabledAction(warnAccountingManagesOrganizeFeature)}
onToggle={(isEnabled) => {
if (!policyID) {
return;
@@ -432,8 +435,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.workflows.subtitle')}
isActive={policy?.areWorkflowsEnabled ?? false}
pendingAction={policy?.pendingFields?.areWorkflowsEnabled}
- disabled={isSmartLimitEnabled}
- disabledAction={promptDisableSmartLimitForWorkflows}
+ disabled={!canWriteMoreFeatures || isSmartLimitEnabled}
+ disabledAction={getReadOnlyDisabledAction(promptDisableSmartLimitForWorkflows)}
onToggle={(isEnabled) => {
if (!policyID) {
return;
@@ -453,6 +456,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.rules.subtitle')}
isActive={policy?.areRulesEnabled ?? false}
pendingAction={policy?.pendingFields?.areRulesEnabled}
+ disabled={!canWriteMoreFeatures}
+ disabledAction={getReadOnlyDisabledAction()}
onToggle={(isEnabled) => {
if (!policyID) {
return;
@@ -481,6 +486,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.distanceRates.subtitle')}
isActive={policy?.areDistanceRatesEnabled ?? false}
pendingAction={policy?.pendingFields?.areDistanceRatesEnabled}
+ disabled={!canWriteMoreFeatures}
+ disabledAction={getReadOnlyDisabledAction()}
onToggle={(isEnabled) => {
if (!policyID) {
return;
@@ -500,6 +507,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.travel.subtitle')}
isActive={policy?.isTravelEnabled ?? false}
pendingAction={policy?.pendingFields?.isTravelEnabled}
+ disabled={!canWriteMoreFeatures}
+ disabledAction={getReadOnlyDisabledAction()}
onToggle={(isEnabled) => {
if (!policyID) {
return;
@@ -522,8 +531,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.expensifyCard.subtitle')}
isActive={policy?.areExpensifyCardsEnabled ?? false}
pendingAction={policy?.pendingFields?.areExpensifyCardsEnabled}
- disabled={(!!policy?.areExpensifyCardsEnabled && !!paymentBankAccountID) || !isEmptyObject(cardsList)}
- disabledAction={promptDisableExpensifyCardViaConcierge}
+ disabled={!canWriteMoreFeatures || (!!policy?.areExpensifyCardsEnabled && !!paymentBankAccountID) || !isEmptyObject(cardsList)}
+ disabledAction={getReadOnlyDisabledAction(promptDisableExpensifyCardViaConcierge)}
onToggle={(isEnabled) => {
if (!policyID) {
return;
@@ -546,8 +555,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.companyCards.subtitle')}
isActive={policy?.areCompanyCardsEnabled ?? false}
pendingAction={policy?.pendingFields?.areCompanyCardsEnabled}
- disabled={!isEmptyObject(getCompanyFeeds(cardFeeds))}
- disabledAction={promptDisableCompanyCardsViaConcierge}
+ disabled={!canWriteMoreFeatures || !isEmptyObject(getCompanyFeeds(cardFeeds))}
+ disabledAction={getReadOnlyDisabledAction(promptDisableCompanyCardsViaConcierge)}
onToggle={(isEnabled) => {
if (!policyID) {
return;
@@ -570,6 +579,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.perDiem.subtitle')}
isActive={(policy?.arePerDiemRatesEnabled && canPolicyAccessFeature(policy, CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_RATES_ENABLED)) ?? false}
pendingAction={policy?.pendingFields?.arePerDiemRatesEnabled}
+ disabled={!canWriteMoreFeatures}
+ disabledAction={getReadOnlyDisabledAction()}
onToggle={(isEnabled) => {
if (!policyID) {
return;
@@ -595,6 +606,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.timeTracking.subtitle')}
isActive={isTimeTrackingEnabled(policy)}
pendingAction={policy?.pendingFields?.isTimeTrackingEnabled}
+ disabled={!canWriteMoreFeatures}
+ disabledAction={getReadOnlyDisabledAction()}
onToggle={(isEnabled) => {
if (!policyID) {
return;
@@ -617,6 +630,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitle={translate('workspace.moreFeatures.invoices.subtitle')}
isActive={policy?.areInvoicesEnabled ?? false}
pendingAction={policy?.pendingFields?.areInvoicesEnabled}
+ disabled={!canWriteMoreFeatures}
+ disabledAction={getReadOnlyDisabledAction()}
onToggle={(isEnabled) => {
if (!policyID) {
return;
diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx
index f6fb37572ab5..5244c77b0492 100644
--- a/src/pages/workspace/WorkspaceOverviewPage.tsx
+++ b/src/pages/workspace/WorkspaceOverviewPage.tsx
@@ -677,6 +677,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa
shouldUseScrollView
shouldShowOfflineIndicatorInWideScreen
shouldShowNonAdmin
+ policyFeature={CONST.POLICY.POLICY_FEATURE.OVERVIEW}
icon={illustrationIcons.Building}
shouldShowNotFoundPage={policy === undefined}
onBackButtonPress={handleBackButtonPress}
diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx
index e36789e0e562..e54dad9d8e57 100644
--- a/src/pages/workspace/WorkspacePageWithSections.tsx
+++ b/src/pages/workspace/WorkspacePageWithSections.tsx
@@ -172,7 +172,7 @@ function WorkspacePageWithSections({
const shouldShowPolicy = useMemo(() => shouldShowPolicyUtil(policy, false, currentUserLogin), [policy, currentUserLogin]);
let hasAccessToPolicyFeature: boolean | undefined;
if (policyFeature) {
- hasAccessToPolicyFeature = currentUserLogin ? canMemberRead(policy, currentUserLogin, policyFeature) : false;
+ hasAccessToPolicyFeature = canMemberRead(policy, currentUserLogin ?? '', policyFeature);
}
const isPendingDelete = isPendingDeletePolicy(policy);
const prevIsPendingDelete = isPendingDeletePolicy(prevPolicy);
diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
index c1dd3dfc0b11..a4c84bab5c2c 100644
--- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx
+++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
@@ -19,6 +19,7 @@ import Text from '@components/Text';
import TextLink from '@components/TextLink';
import ThreeDotsMenu from '@components/ThreeDotsMenu';
import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds';
import useHasReusablePoliciesConnectedTo from '@hooks/useHasReusablePoliciesConnectedTo';
@@ -38,6 +39,7 @@ import {isExpensifyCardFullySetUp} from '@libs/CardUtils';
import {getOldDotURLFromEnvironment} from '@libs/Environment/Environment';
import {
areSettingsInErrorFields,
+ canMemberWrite,
findCurrentXeroOrganization,
getConnectedIntegration,
getCurrentSageIntacctEntityName,
@@ -84,6 +86,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
const oldDotEnvironmentURL = getOldDotURLFromEnvironment(environment);
const {isOffline} = useNetwork();
const {isBetaEnabled} = usePermissions();
+ const {login: currentUserLogin = ''} = useCurrentUserPersonalDetails();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false);
const [datetimeToRelative, setDateTimeToRelative] = useState('');
@@ -108,6 +111,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
const syncingAccountingIntegration = accountingIntegrations.find((integration) => integration === connectionSyncProgress?.connectionName);
const connectedIntegration = getConnectedIntegration(policy, accountingIntegrations) ?? syncingAccountingIntegration;
const hasAccountingConnection = hasAccountingConnections(policy);
+ const canWriteAccounting = canMemberWrite(policy, currentUserLogin, CONST.POLICY.POLICY_FEATURE.ACCOUNTING);
const synchronizationError = connectedIntegration && getSynchronizationErrorMessage(policy, connectedIntegration, isSyncInProgress, translate, styles);
const shouldShowEnterCredentials = connectedIntegration && !!synchronizationError && isAuthenticationError(policy, connectedIntegration);
@@ -185,7 +189,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
useFocusEffect(
useCallback(() => {
- if (!newConnectionName || !isControlPolicy(policy)) {
+ if (!newConnectionName || !isControlPolicy(policy) || !canWriteAccounting) {
return;
}
@@ -194,7 +198,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
integrationToDisconnect,
shouldDisconnectIntegrationBeforeConnecting,
});
- }, [newConnectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting, policy, startIntegrationFlow]),
+ }, [newConnectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting, policy, startIntegrationFlow, canWriteAccounting]),
);
useEffect(() => {
@@ -225,14 +229,14 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
title: getCurrentXeroOrganizationName(policy),
wrapperStyle: [styles.sectionMenuItemTopDescription],
titleStyle: styles.fontWeightNormal,
- shouldShowRightIcon: tenants.length > 1,
+ shouldShowRightIcon: canWriteAccounting && tenants.length > 1,
shouldShowDescriptionOnTop: true,
- onPress: () => {
- if (!(tenants.length > 1)) {
- return;
- }
- Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.getRoute(policyID, currentXeroOrganization?.id));
- },
+ onPress:
+ canWriteAccounting && tenants.length > 1
+ ? () => {
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.getRoute(policyID, currentXeroOrganization?.id));
+ }
+ : undefined,
pendingAction: settingsPendingAction([CONST.XERO_CONFIG.TENANT_ID], policy?.connections?.xero?.config?.pendingFields),
brickRoadIndicator: areSettingsInErrorFields([CONST.XERO_CONFIG.TENANT_ID], policy?.connections?.xero?.config?.errorFields)
? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
@@ -247,16 +251,16 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
title: policy?.connections?.netsuite?.options?.config?.subsidiary ?? '',
wrapperStyle: [styles.sectionMenuItemTopDescription],
titleStyle: styles.fontWeightNormal,
- shouldShowRightIcon: netSuiteSubsidiaryList?.length > 1,
+ shouldShowRightIcon: canWriteAccounting && netSuiteSubsidiaryList?.length > 1,
shouldShowDescriptionOnTop: true,
pendingAction: policy?.connections?.netsuite?.options?.config?.pendingFields?.subsidiary,
brickRoadIndicator: policy?.connections?.netsuite?.options?.config?.errorFields?.subsidiary ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- onPress: () => {
- if (!(netSuiteSubsidiaryList?.length > 1)) {
- return;
- }
- Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR.getRoute(policyID));
- },
+ onPress:
+ canWriteAccounting && netSuiteSubsidiaryList?.length > 1
+ ? () => {
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR.getRoute(policyID));
+ }
+ : undefined,
};
case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT:
return !sageIntacctEntityList.length
@@ -267,16 +271,11 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
title: getCurrentSageIntacctEntityName(policy, translate('workspace.common.topLevel')),
wrapperStyle: [styles.sectionMenuItemTopDescription],
titleStyle: styles.fontWeightNormal,
- shouldShowRightIcon: true,
+ shouldShowRightIcon: canWriteAccounting,
shouldShowDescriptionOnTop: true,
pendingAction: policy?.connections?.intacct?.config?.pendingFields?.entity,
brickRoadIndicator: policy?.connections?.intacct?.config?.errorFields?.entity ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- onPress: () => {
- if (!sageIntacctEntityList.length) {
- return;
- }
- Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ENTITY.getRoute(policyID));
- },
+ onPress: canWriteAccounting ? () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ENTITY.getRoute(policyID)) : undefined,
};
case CONST.POLICY.CONNECTIONS.NAME.QBO:
return !policy?.connections?.quickbooksOnline?.config?.companyName
@@ -293,10 +292,25 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
default:
return undefined;
}
- }, [connectedIntegration, currentXeroOrganization?.id, policy, policyID, styles.fontWeightNormal, styles.sectionMenuItemTopDescription, tenants.length, translate, icons.ArrowRight]);
+ }, [
+ canWriteAccounting,
+ connectedIntegration,
+ currentXeroOrganization?.id,
+ policy,
+ policyID,
+ styles.fontWeightNormal,
+ styles.sectionMenuItemTopDescription,
+ tenants.length,
+ translate,
+ icons.ArrowRight,
+ ]);
const connectionsMenuItems: MenuItemData[] = useMemo(() => {
if (!hasAccountingConnection && !isSyncInProgress && policyID) {
+ if (!canWriteAccounting) {
+ return [];
+ }
+
return accountingIntegrations
.map((integration) => {
const integrationData = getAccountingIntegrationData(
@@ -386,58 +400,86 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
connectionMessage = translate('workspace.accounting.lastSync', datetimeToRelative);
}
- const configurationOptions = [
- {
- icon: icons.Pencil,
- iconRight: icons.ArrowRight,
- shouldShowRightIcon: true,
- title: translate('workspace.accounting.import'),
- wrapperStyle: [styles.sectionMenuItemTopDescription],
- onPress: integrationData?.onImportPagePress,
- brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedImportSettings, integrationData?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- pendingAction: settingsPendingAction(integrationData?.subscribedImportSettings, integrationData?.pendingFields),
- },
- {
- icon: icons.Send,
- iconRight: icons.ArrowRight,
- shouldShowRightIcon: true,
- title: translate('workspace.accounting.export'),
- wrapperStyle: [styles.sectionMenuItemTopDescription],
- onPress: integrationData?.onExportPagePress,
- brickRoadIndicator:
- areSettingsInErrorFields(integrationData?.subscribedExportSettings, integrationData?.errorFields) || shouldShowQBOReimbursableExportDestinationAccountError(policy)
- ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
- : undefined,
- pendingAction: settingsPendingAction(integrationData?.subscribedExportSettings, integrationData?.pendingFields),
- },
- ...(shouldShowCardReconciliationOption
- ? [
- {
- icon: icons.ExpensifyCard,
- iconRight: icons.ArrowRight,
- shouldShowRightIcon: true,
- title: translate('workspace.accounting.cardReconciliation'),
- wrapperStyle: [styles.sectionMenuItemTopDescription],
- onPress: integrationData?.onCardReconciliationPagePress,
- },
- ]
- : []),
- {
- icon: icons.Gear,
- iconRight: icons.ArrowRight,
- shouldShowRightIcon: true,
- title: translate('workspace.accounting.advanced'),
- wrapperStyle: [styles.sectionMenuItemTopDescription],
- onPress: integrationData?.onAdvancedPagePress,
- brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedAdvancedSettings, integrationData?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- pendingAction: settingsPendingAction(integrationData?.subscribedAdvancedSettings, integrationData?.pendingFields),
- },
- ];
+ const configurationOptions = canWriteAccounting
+ ? [
+ {
+ icon: icons.Pencil,
+ iconRight: icons.ArrowRight,
+ shouldShowRightIcon: true,
+ title: translate('workspace.accounting.import'),
+ wrapperStyle: [styles.sectionMenuItemTopDescription],
+ onPress: integrationData?.onImportPagePress,
+ brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedImportSettings, integrationData?.errorFields)
+ ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
+ : undefined,
+ pendingAction: settingsPendingAction(integrationData?.subscribedImportSettings, integrationData?.pendingFields),
+ },
+ {
+ icon: icons.Send,
+ iconRight: icons.ArrowRight,
+ shouldShowRightIcon: true,
+ title: translate('workspace.accounting.export'),
+ wrapperStyle: [styles.sectionMenuItemTopDescription],
+ onPress: integrationData?.onExportPagePress,
+ brickRoadIndicator:
+ areSettingsInErrorFields(integrationData?.subscribedExportSettings, integrationData?.errorFields) || shouldShowQBOReimbursableExportDestinationAccountError(policy)
+ ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
+ : undefined,
+ pendingAction: settingsPendingAction(integrationData?.subscribedExportSettings, integrationData?.pendingFields),
+ },
+ ...(shouldShowCardReconciliationOption
+ ? [
+ {
+ icon: icons.ExpensifyCard,
+ iconRight: icons.ArrowRight,
+ shouldShowRightIcon: true,
+ title: translate('workspace.accounting.cardReconciliation'),
+ wrapperStyle: [styles.sectionMenuItemTopDescription],
+ onPress: integrationData?.onCardReconciliationPagePress,
+ },
+ ]
+ : []),
+ {
+ icon: icons.Gear,
+ iconRight: icons.ArrowRight,
+ shouldShowRightIcon: true,
+ title: translate('workspace.accounting.advanced'),
+ wrapperStyle: [styles.sectionMenuItemTopDescription],
+ onPress: integrationData?.onAdvancedPagePress,
+ brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedAdvancedSettings, integrationData?.errorFields)
+ ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
+ : undefined,
+ pendingAction: settingsPendingAction(integrationData?.subscribedAdvancedSettings, integrationData?.pendingFields),
+ },
+ ]
+ : [];
const syncActivityReasonAttributes: SkeletonSpanReasonAttributes = {
context: 'PolicyAccountingPage.connectionsMenuItems',
isSyncInProgress,
};
+ let rightComponent;
+ if (isSyncInProgress) {
+ rightComponent = (
+
+ );
+ } else if (canWriteAccounting) {
+ rightComponent = (
+
+ );
+ }
+
return [
{
...iconProps,
@@ -449,22 +491,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
errorTextStyle: [styles.mt5],
shouldShowRedDotIndicator: true,
description: connectionMessage,
- rightComponent: isSyncInProgress ? (
-
- ) : (
-
- ),
+ rightComponent,
},
...(isEmptyObject(integrationSpecificMenuItems) || shouldShowSynchronizationError || !hasAccountingConnection ? [] : [integrationSpecificMenuItems]),
...(!hasAccountingConnection || !isConnectionVerified ? [] : configurationOptions),
@@ -503,10 +530,11 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
datetimeToRelative,
hasReusablePoliciesConnectedToSageIntacct,
hasReusablePoliciesConnectedToQBD,
+ canWriteAccounting,
]);
const otherIntegrationsItems = useMemo(() => {
- if ((!hasAccountingConnection && !isSyncInProgress) || !policyID) {
+ if (!canWriteAccounting || (!hasAccountingConnection && !isSyncInProgress) || !policyID) {
return;
}
const otherIntegrations = accountingIntegrations.filter(
@@ -579,6 +607,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
startIntegrationFlow,
popoverAnchorRefs,
accountingIcons,
+ canWriteAccounting,
]);
const [chatTextLink, chatReportID] = useMemo(() => {
@@ -602,6 +631,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
+ policyFeature={CONST.POLICY.POLICY_FEATURE.ACCOUNTING}
>
)}
- {!!account?.guideDetails?.email && !hasAccountingConnections(policy) && (
+ {!!account?.guideDetails?.email && !hasAccountingConnections(policy) && canWriteAccounting && (
([]);
- const canSelectMultiple = isSmallScreenWidth ? isMobileSelectionModeEnabled : true;
+ const canSelectMultiple = canWriteCategories && (isSmallScreenWidth ? isMobileSelectionModeEnabled : true);
const isControlPolicyWithWideLayout = !shouldUseNarrowLayout && isControlPolicy(policy);
const shouldShowApproverColumn = isControlPolicyWithWideLayout && !!policy?.areRulesEnabled;
const icons = useMemoizedLazyExpensifyIcons(['Checkmark', 'Close', 'Download', 'Gear', 'Plus', 'Table', 'Trashcan']);
@@ -180,6 +182,10 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
const updateWorkspaceCategoryEnabled = useCallback(
(value: boolean, categoryName: string) => {
+ if (!canWriteCategories) {
+ showReadOnlyModal();
+ return;
+ }
setWorkspaceCategoryEnabled({
policyData,
categoriesToUpdate: {[categoryName]: {name: categoryName, enabled: value}},
@@ -211,6 +217,8 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
setupCategoriesAndTagsHasOutstandingChildTask,
setupCategoriesAndTagsParentReportAction,
policyHasTags,
+ canWriteCategories,
+ showReadOnlyModal,
],
);
@@ -273,7 +281,8 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
{
if (isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])) {
@@ -282,14 +291,15 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
}
updateWorkspaceCategoryEnabled(newValue, value.name);
}}
- showLockIcon={isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])}
+ showLockIcon={!canWriteCategories || isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])}
/>
>
) : (
{
if (isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])) {
@@ -298,7 +308,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
}
updateWorkspaceCategoryEnabled(newValue, value.name);
}}
- showLockIcon={isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])}
+ showLockIcon={!canWriteCategories || isDisablingOrDeletingLastEnabledCategory(policy, policyCategories, [value])}
/>
),
});
@@ -317,6 +327,8 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
glCodeTextStyle,
switchContainerStyle,
shouldShowApproverColumn,
+ canWriteCategories,
+ showReadOnlyModal,
styles.alignItemsCenter,
styles.flexRow,
styles.mr3,
@@ -361,8 +373,8 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
// Show GL Code column only on wide screens for control policies. Approver column additionally requires rules to be enabled
if (isControlPolicyWithWideLayout) {
- return (
-
+ const header = (
+
{translate('common.name')}
@@ -374,11 +386,17 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
{translate('common.approver')}
)}
-
+
{translate('common.enabled')}
);
+
+ if (canSelectMultiple) {
+ return header;
+ }
+
+ return {header};
}
return (
@@ -386,12 +404,15 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
canSelectMultiple={canSelectMultiple}
leftHeaderText={translate('common.name')}
rightHeaderText={translate('common.enabled')}
- shouldShowRightCaret
+ shouldShowRightCaret={canWriteCategories}
/>
);
};
const navigateToCategorySettings = (category: ListItem) => {
+ if (!canWriteCategories) {
+ return;
+ }
if (isSmallScreenWidth && isMobileSelectionModeEnabled) {
toggleCategory(category);
return;
@@ -460,13 +481,15 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
const secondaryActions = useMemo(() => {
const menuItems = [];
- menuItems.push({
- icon: icons.Gear,
- text: translate('common.settings'),
- onSelected: navigateToCategoriesSettings,
- value: CONST.POLICY.SECONDARY_ACTIONS.SETTINGS,
- });
- if (!policyHasAccountingConnections) {
+ if (canWriteCategories) {
+ menuItems.push({
+ icon: icons.Gear,
+ text: translate('common.settings'),
+ onSelected: navigateToCategoriesSettings,
+ value: CONST.POLICY.SECONDARY_ACTIONS.SETTINGS,
+ });
+ }
+ if (canWriteCategories && !policyHasAccountingConnections) {
menuItems.push({
icon: icons.Table,
text: translate('spreadsheet.importSpreadsheet'),
@@ -505,6 +528,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
icons.Table,
translate,
navigateToCategoriesSettings,
+ canWriteCategories,
policyHasAccountingConnections,
hasVisibleCategories,
navigateToImportSpreadsheet,
@@ -515,11 +539,15 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine();
const getHeaderButtons = () => {
+ if (!canWriteCategories && secondaryActions.length === 0) {
+ return null;
+ }
+
const options: Array>> = [];
const isThereAnyAccountingConnection = Object.keys(policy?.connections ?? {}).length !== 0;
const selectedCategoriesObject = selectedCategories.map((key) => policyCategories?.[key]);
- if (isSmallScreenWidth ? canSelectMultiple : selectedCategories.length > 0) {
+ if (canWriteCategories && (isSmallScreenWidth ? canSelectMultiple : selectedCategories.length > 0)) {
if (!isThereAnyAccountingConnection) {
options.push({
icon: icons.Trashcan,
@@ -638,7 +666,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
/>
);
}
- const shouldShowAddCategory = !policyHasAccountingConnections && hasVisibleCategories;
+ const shouldShowAddCategory = canWriteCategories && !policyHasAccountingConnections && hasVisibleCategories;
return (
{shouldShowAddCategory && (
@@ -651,16 +679,18 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
style={[shouldDisplayButtonsInSeparateLine && styles.flex1]}
/>
)}
- {}}
- shouldAlwaysShowDropdownMenu
- customText={translate('common.more')}
- sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.CATEGORIES.MORE_DROPDOWN}
- options={secondaryActions}
- isSplitButton={false}
- wrapperStyle={shouldShowAddCategory || !shouldDisplayButtonsInSeparateLine ? styles.flexGrow0 : styles.flexGrow1}
- />
+ {secondaryActions.length > 0 && (
+ {}}
+ shouldAlwaysShowDropdownMenu
+ customText={translate('common.more')}
+ sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.CATEGORIES.MORE_DROPDOWN}
+ options={secondaryActions}
+ isSplitButton={false}
+ wrapperStyle={shouldShowAddCategory || !shouldDisplayButtonsInSeparateLine ? styles.flexGrow0 : styles.flexGrow1}
+ />
+ )}
);
};
@@ -717,6 +747,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
policyID={policyId}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED}
+ policyFeature={CONST.POLICY.POLICY_FEATURE.CATEGORIES}
>
{!shouldDisplayButtonsInSeparateLine && getHeaderButtons()}
- {shouldDisplayButtonsInSeparateLine && {getHeaderButtons()}}
+ {shouldDisplayButtonsInSeparateLine && !!getHeaderButtons() && {getHeaderButtons()}}
{(!hasVisibleCategories || isLoading) && headerContent}
{isLoading && (
item && toggleCategory(item)}
- onSelectAll={filteredCategoryList.length > 0 ? toggleAllCategories : undefined}
+ onTurnOnSelectionMode={(item) => item && canWriteCategories && toggleCategory(item)}
+ onSelectAll={canWriteCategories && filteredCategoryList.length > 0 ? toggleAllCategories : undefined}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
- turnOnSelectionModeOnLongPress={isSmallScreenWidth}
+ turnOnSelectionModeOnLongPress={canWriteCategories && isSmallScreenWidth}
customListHeader={getCustomListHeader()}
customListHeaderContent={headerContent}
canSelectMultiple={canSelectMultiple}
@@ -777,7 +808,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
onDismissError={dismissError}
showScrollIndicator={false}
shouldHeaderBeInsideList
- shouldShowRightCaret
+ shouldShowRightCaret={canWriteCategories}
/>
)}
{!hasVisibleCategories && !isLoading && inputValue.length === 0 && (
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx
index 7e15f7d7292a..44633cb77c92 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx
@@ -20,6 +20,7 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePolicy from '@hooks/usePolicy';
+import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import useTheme from '@hooks/useTheme';
import useThemeIllustrations from '@hooks/useThemeIllustrations';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -63,6 +64,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
const {showConfirmModal} = useConfirmModal();
const policy = usePolicy(policyID);
+ const {canWrite: canWriteCompanyCards} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.COMPANY_CARDS);
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`);
const [customCardNames] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES);
const [shouldUseStagingServer = isUsingStagingApi()] = useOnyx(ONYXKEYS.SHOULD_USE_STAGING_SERVER);
@@ -132,6 +134,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
clearCompanyCardErrorField(domainOrWorkspaceAccountID, cardID, bank, 'lastScrape', true)}
>
-
+ {canWriteCompanyCards && (
+
+ )}
Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EDIT_CARD_NAME.getRoute(policyID, cardID, feedName))}
+ onPress={canWriteCompanyCards ? () => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EDIT_CARD_NAME.getRoute(policyID, cardID, feedName)) : undefined}
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.COMPANY_CARDS.CARD_NAME}
/>
@@ -230,8 +235,8 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EXPORT.getRoute(policyID, cardID, feedName, backTo))}
+ shouldShowRightIcon={canWriteCompanyCards}
+ onPress={canWriteCompanyCards ? () => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EXPORT.getRoute(policyID, cardID, feedName, backTo)) : undefined}
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.COMPANY_CARDS.CARD_EXPORT}
/>
@@ -245,13 +250,15 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EDIT_TRANSACTION_START_DATE.getRoute(policyID, cardID, feedName))}
+ onPress={
+ canWriteCompanyCards ? () => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EDIT_TRANSACTION_START_DATE.getRoute(policyID, cardID, feedName)) : undefined
+ }
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.COMPANY_CARDS.TRANSACTION_START_DATE}
/>
- {shouldShowBreakConnection && (
+ {canWriteCompanyCards && shouldShowBreakConnection && (
)}
-
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardPageEmptyState.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardPageEmptyState.tsx
index 7942c036cf6e..b694fe102452 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardPageEmptyState.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardPageEmptyState.tsx
@@ -24,9 +24,10 @@ import WorkspaceCompanyCardExpensifyCardPromotionBanner from './WorkspaceCompany
type WorkspaceCompanyCardPageEmptyStateProps = {
policyID: string;
shouldShowGBDisclaimer?: boolean;
+ canWriteCompanyCards?: boolean;
};
-function WorkspaceCompanyCardPageEmptyState({policyID, shouldShowGBDisclaimer}: WorkspaceCompanyCardPageEmptyStateProps) {
+function WorkspaceCompanyCardPageEmptyState({policyID, shouldShowGBDisclaimer, canWriteCompanyCards = true}: WorkspaceCompanyCardPageEmptyStateProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
@@ -113,9 +114,9 @@ function WorkspaceCompanyCardPageEmptyState({policyID, shouldShowGBDisclaimer}:
menuItems={companyCardFeatures as FeatureListItem[]}
title={translate('workspace.moreFeatures.companyCards.feed.title')}
subtitle={translate('workspace.moreFeatures.companyCards.feed.subtitle')}
- ctaText={translate('workspace.companyCards.addCards')}
- ctaAccessibilityLabel={translate('workspace.companyCards.addCards')}
- onCtaPress={handleCtaPress}
+ ctaText={canWriteCompanyCards ? translate('workspace.companyCards.addCards') : undefined}
+ ctaAccessibilityLabel={canWriteCompanyCards ? translate('workspace.companyCards.addCards') : undefined}
+ onCtaPress={canWriteCompanyCards ? handleCtaPress : undefined}
illustrationBackgroundColor={colors.blue800}
illustration={getCompanyCardIllustration()}
illustrationStyle={styles.getEmptyStateCompanyCardsIllustration(shouldUseNarrowLayout)}
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
index 6eec067f5f91..8459dea0dfdf 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
@@ -3,6 +3,7 @@ import DecisionModal from '@components/DecisionModal';
import WorkspaceCompanyCardsTable from '@components/Tables/WorkspaceCompanyCardsTable';
import useAssignCard from '@hooks/useAssignCard';
import useCompanyCards from '@hooks/useCompanyCards';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -12,7 +13,7 @@ import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle';
import {getDomainOrWorkspaceAccountID} from '@libs/CardUtils';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
-import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils';
+import {canMemberWrite, getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage} from '@userActions/CompanyCards';
@@ -25,6 +26,7 @@ type WorkspaceCompanyCardsPageProps = PlatformStackScreenProps
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
index 0bae0e6909ea..81ee41ac9f6a 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
@@ -24,6 +24,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePolicy from '@hooks/usePolicy';
+import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSearchBackPress from '@hooks/useSearchBackPress';
import useSearchResults from '@hooks/useSearchResults';
@@ -73,8 +74,9 @@ function PolicyDistanceRatesPage({
const policy = usePolicy(policyID);
useWorkspaceDocumentTitle(policy?.name, 'workspace.common.distanceRates');
const isMobileSelectionModeEnabled = useMobileSelectionMode();
+ const {canWrite: canWriteDistanceRates, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.DISTANCE_RATES);
- const canSelectMultiple = shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true;
+ const canSelectMultiple = canWriteDistanceRates && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true);
const {asset: CarIce} = useMemoizedLazyAsset(() => loadIllustration('CarIce' as IllustrationName));
const customUnit = useMemo(() => getDistanceRateCustomUnit(policy), [policy]);
const customUnitRates: Record = useMemo(() => customUnit?.rates ?? {}, [customUnit?.rates]);
@@ -211,6 +213,10 @@ function PolicyDistanceRatesPage({
const updateDistanceRateEnabled = useCallback(
(value: boolean, rateID: string) => {
+ if (!canWriteDistanceRates) {
+ showReadOnlyModal();
+ return;
+ }
if (!customUnit) {
return;
}
@@ -222,7 +228,7 @@ function PolicyDistanceRatesPage({
showWarningModal();
}
},
- [canDisableOrDeleteRate, customUnit, policyID, showWarningModal],
+ [canDisableOrDeleteRate, canWriteDistanceRates, customUnit, policyID, showReadOnlyModal, showWarningModal],
);
const unitTranslation = translate(`common.${customUnit?.attributes?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}`);
@@ -255,13 +261,23 @@ function PolicyDistanceRatesPage({
isOn={!!value?.enabled}
accessibilityLabel={value?.name ?? ''}
onToggle={(newValue: boolean) => updateDistanceRateEnabled(newValue, value.customUnitRateID)}
- showLockIcon={!canDisableOrDeleteRate(value.customUnitRateID)}
- disabled={value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}
+ showLockIcon={!canWriteDistanceRates || !canDisableOrDeleteRate(value.customUnitRateID)}
+ disabled={!canWriteDistanceRates || value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}
+ disabledAction={!canWriteDistanceRates ? showReadOnlyModal : undefined}
/>
),
};
}),
- [canDisableOrDeleteRate, customUnitRates, unitTranslation, customUnit?.pendingFields?.attributes, policy?.pendingAction, updateDistanceRateEnabled],
+ [
+ canDisableOrDeleteRate,
+ canWriteDistanceRates,
+ customUnitRates,
+ unitTranslation,
+ customUnit?.pendingFields?.attributes,
+ policy?.pendingAction,
+ showReadOnlyModal,
+ updateDistanceRateEnabled,
+ ],
);
const filterRate = useCallback((rate: RateForList, searchInput: string) => {
@@ -280,6 +296,9 @@ function PolicyDistanceRatesPage({
}, [policyID]);
const openRateDetails = (rate: RateForList) => {
+ if (!canWriteDistanceRates) {
+ return;
+ }
Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATE_DETAILS.getRoute(policyID, rate.value));
};
@@ -356,7 +375,7 @@ function PolicyDistanceRatesPage({
canSelectMultiple={canSelectMultiple}
leftHeaderText={translate('workspace.distanceRates.rate')}
rightHeaderText={translate('common.enabled')}
- shouldShowRightCaret
+ shouldShowRightCaret={canWriteDistanceRates}
/>
);
};
@@ -426,7 +445,7 @@ function PolicyDistanceRatesPage({
const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine();
- const headerButtons = (
+ const headerButtons = canWriteDistanceRates ? (
{(shouldUseNarrowLayout ? !isMobileSelectionModeEnabled : selectedDistanceRates.length === 0) ? (
<>
@@ -465,7 +484,7 @@ function PolicyDistanceRatesPage({
/>
)}
- );
+ ) : null;
const selectionModeHeader = isMobileSelectionModeEnabled && shouldUseNarrowLayout;
@@ -492,6 +511,7 @@ function PolicyDistanceRatesPage({
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED}
+ policyFeature={CONST.POLICY.POLICY_FEATURE.DISTANCE_RATES}
>
{!shouldDisplayButtonsInSeparateLine && headerButtons}
- {shouldDisplayButtonsInSeparateLine && {headerButtons}}
+ {shouldDisplayButtonsInSeparateLine && !!headerButtons && {headerButtons}}
{isLoading && (
item && toggleRate(item)}
- onSelectAll={filteredDistanceRatesList.length > 0 ? toggleAllRates : undefined}
+ onTurnOnSelectionMode={(item) => item && canWriteDistanceRates && toggleRate(item)}
+ onSelectAll={canWriteDistanceRates && filteredDistanceRatesList.length > 0 ? toggleAllRates : undefined}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
customListHeaderContent={headerContent}
canSelectMultiple={canSelectMultiple}
@@ -542,9 +562,9 @@ function PolicyDistanceRatesPage({
onDismissError={dismissError}
shouldShowListEmptyContent={false}
showScrollIndicator={false}
- turnOnSelectionModeOnLongPress
+ turnOnSelectionModeOnLongPress={canWriteDistanceRates}
shouldHeaderBeInsideList
- shouldShowRightCaret
+ shouldShowRightCaret={canWriteDistanceRates}
/>
)}
diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx
index dc08a6534505..b425711149c1 100644
--- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx
+++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx
@@ -8,6 +8,7 @@ import getBankIcon from '@components/Icon/BankIcons';
import MenuItem from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDefaultFundID from '@hooks/useDefaultFundID';
import useExpensifyCardUkEuSupported from '@hooks/useExpensifyCardUkEuSupported';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
@@ -19,6 +20,7 @@ import {getLastFourDigits} from '@libs/BankAccountUtils';
import {getEligibleBankAccountsForCard, getEligibleBankAccountsForUkEuCard} from '@libs/CardUtils';
import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import {canMemberWrite} from '@libs/PolicyUtils';
import {REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '@libs/ReimbursementAccountUtils';
import Navigation from '@navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
@@ -40,6 +42,8 @@ function WorkspaceExpensifyCardBankAccounts({route}: WorkspaceExpensifyCardBankA
const policyID = route?.params?.policyID;
const policy = usePolicy(policyID);
+ const {email: currentUserEmail = ''} = useCurrentUserPersonalDetails();
+ const canWriteExpensifyCard = canMemberWrite(policy, currentUserEmail, CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD);
const isUkEuCurrencySupported = useExpensifyCardUkEuSupported(policyID);
@@ -140,6 +144,8 @@ function WorkspaceExpensifyCardBankAccounts({route}: WorkspaceExpensifyCardBankA
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED}
+ policyFeature={CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD}
+ shouldBeBlocked={!canWriteExpensifyCard}
>
{
@@ -129,7 +129,7 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail
useEffect(() => fetchCardDetails(), [fetchCardDetails]);
useEffect(() => {
- if (!isAdmin) {
+ if (!canWriteExpensifyCard) {
return;
}
if (!defaultFundID || defaultFundID === CONST.DEFAULT_NUMBER_ID) {
@@ -143,7 +143,7 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail
}
openPolicyExpensifyCardsPage(policyID, defaultFundID);
- }, [defaultFundID, fundCardSettings?.hasOnceLoaded, fundCardSettings?.isLoading, isAdmin, policyID]);
+ }, [canWriteExpensifyCard, defaultFundID, fundCardSettings?.hasOnceLoaded, fundCardSettings?.isLoading, policyID]);
const deactivateCard = async () => {
const {action} = await showConfirmModal({
@@ -215,7 +215,7 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail
[spendRulesSummary],
);
- const canManageCardFreeze = isAdmin && !!card;
+ const canManageCardFreeze = canWriteExpensifyCard && !!card;
const scarfOverlayStyle = useMemo(
() => ({
top: 0,
@@ -258,6 +258,7 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED}
+ policyFeature={CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD}
>
{canManageCardFreeze && isCardFrozen(card) ? (
Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.EXPENSIFY_CARD_NAME.path))}
+ shouldShowRightIcon={canWriteExpensifyCard}
+ onPress={canWriteExpensifyCard ? () => Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.EXPENSIFY_CARD_NAME.path)) : undefined}
/>
Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.EXPENSIFY_CARD_LIMIT_TYPE.path))}
+ shouldShowRightIcon={canWriteExpensifyCard}
+ onPress={canWriteExpensifyCard ? () => Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.EXPENSIFY_CARD_LIMIT_TYPE.path)) : undefined}
hintText={getCardHintText(card?.nameValuePairs?.validFrom, card?.nameValuePairs?.validThru, cardholder?.timezone?.selected, translate)}
/>
@@ -370,8 +371,8 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail
Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.EXPENSIFY_CARD_LIMIT.path))}
+ shouldShowRightIcon={canWriteExpensifyCard}
+ onPress={canWriteExpensifyCard ? () => Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.EXPENSIFY_CARD_LIMIT.path)) : undefined}
/>
@@ -380,18 +381,18 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail
description={translate('cardPage.spendRules')}
descriptionTextStyle={[styles.fontSizeLabel]}
titleComponent={spendRulesTitleComponent}
- onPress={navigateToSpendRules}
+ onPress={canWriteExpensifyCard ? navigateToSpendRules : undefined}
accessibilityLabel={spendRulesSummary.join('. ')}
/>
)}
- {!isProduction && isAdmin && (
+ {!isProduction && canWriteExpensifyCard && (
)}
- {!isDeactivated && (
+ {canWriteExpensifyCard && !isDeactivated && (
(undefined);
+ const {email: currentUserEmail = ''} = useCurrentUserPersonalDetails();
const {primaryFeeds, otherFeeds} = useExpensifyCardFeedsForFeedSelector(policyID);
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
+ const policy = usePolicy(policyID);
+ const canWriteExpensifyCard = canMemberWrite(policy, currentUserEmail, CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD);
const getIssueCardFundID = () => {
if (primaryFeeds.length === 0) {
@@ -183,7 +189,7 @@ function WorkspaceExpensifyCardFeedSelectorPage({route}: WorkspaceExpensifyCardF
const primaryListData = primaryFeeds.map((entry) => toListItem(entry, false));
- const issueNewCardAndOtherFeedsFooter = (
+ const issueNewCardAndOtherFeedsFooter = canWriteExpensifyCard ? (
)}
- );
+ ) : undefined;
return (
Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS.getRoute(policyID)),
- value: CONST.POLICY.SECONDARY_ACTIONS.SETTINGS,
- },
- ];
+ const secondaryActions = canWriteExpensifyCard
+ ? [
+ {
+ icon: icons.Gear,
+ text: translate('common.settings'),
+ onSelected: () => Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS.getRoute(policyID)),
+ value: CONST.POLICY.SECONDARY_ACTIONS.SETTINGS,
+ },
+ ]
+ : [];
const getHeaderButtons = () => {
const headerButtonsRowStyle = [
styles.flexRow,
@@ -196,7 +201,7 @@ function WorkspaceExpensifyCardListPage({route, cardsList, fundID}: WorkspaceExp
return (
- {!isCardListEmpty && (
+ {!isCardListEmpty && canWriteExpensifyCard && (
)}
- {}}
- customText={translate('common.more')}
- options={secondaryActions}
- isSplitButton={false}
- shouldUseOptionIcon
- wrapperStyle={isCardListEmpty && !isInLandscapeMode ? styles.flexGrow1 : styles.flexGrow0}
- sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.EXPENSIFY_CARD.MORE_DROPDOWN}
- />
+ {secondaryActions.length > 0 && (
+ {}}
+ customText={translate('common.more')}
+ options={secondaryActions}
+ isSplitButton={false}
+ shouldUseOptionIcon
+ wrapperStyle={isCardListEmpty && !isInLandscapeMode ? styles.flexGrow1 : styles.flexGrow0}
+ sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.EXPENSIFY_CARD.MORE_DROPDOWN}
+ />
+ )}
);
};
@@ -312,6 +319,7 @@ function WorkspaceExpensifyCardListPage({route, cardsList, fundID}: WorkspaceExp
Navigation.goBack();
return true;
};
+ const shouldShowHeaderButtons = selectedCardIDs.length > 0 || canWriteExpensifyCard;
useAndroidBackButtonHandler(handleBackButtonPress);
@@ -331,9 +339,9 @@ function WorkspaceExpensifyCardListPage({route, cardsList, fundID}: WorkspaceExp
shouldDisplayHelpButton
onBackButtonPress={handleBackButtonPress}
>
- {!shouldShowSelector && !shouldDisplayButtonsInSeparateLine && isBankAccountVerified && getHeaderButtons()}
+ {!shouldShowSelector && !shouldDisplayButtonsInSeparateLine && isBankAccountVerified && shouldShowHeaderButtons && getHeaderButtons()}
- {!shouldShowSelector && shouldDisplayButtonsInSeparateLine && isBankAccountVerified && {getHeaderButtons()}}
+ {!shouldShowSelector && shouldDisplayButtonsInSeparateLine && isBankAccountVerified && shouldShowHeaderButtons && {getHeaderButtons()}}
{shouldShowSelector && (
- {isBankAccountVerified && getHeaderButtons()}
+ {isBankAccountVerified && (canWriteExpensifyCard || secondaryActions.length > 0) && getHeaderButtons()}
)}
{isCardListEmpty ? (
) : (
{renderContent()}
diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx
index 2c694a0253c8..24a495ecab69 100644
--- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx
+++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx
@@ -7,6 +7,7 @@ import {useLockedAccountActions, useLockedAccountState} from '@components/Locked
import {ModalActions} from '@components/Modal/Global/ModalContext';
import Text from '@components/Text';
import useConfirmModal from '@hooks/useConfirmModal';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useExpensifyCardUkEuSupported from '@hooks/useExpensifyCardUkEuSupported';
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
@@ -20,6 +21,7 @@ import {getEligibleBankAccountsForCard, getEligibleBankAccountsForUkEuCard} from
import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
+import {canMemberWrite} from '@libs/PolicyUtils';
import {hasInProgressUSDVBBA, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '@libs/ReimbursementAccountUtils';
import Navigation from '@navigation/Navigation';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
@@ -50,6 +52,8 @@ function WorkspaceExpensifyCardPageEmptyState({route, policy}: WorkspaceExpensif
const {showDelegateNoAccessModal} = useDelegateNoAccessActions();
const {isAccountLocked} = useLockedAccountState();
const {showLockedAccountModal} = useLockedAccountActions();
+ const {email: currentUserEmail = ''} = useCurrentUserPersonalDetails();
+ const canWriteExpensifyCard = canMemberWrite(policy, currentUserEmail, CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD);
// Dismiss the "Update to USD" modal if the currency changes to USD externally (e.g. from another device)
const isCurrencyModalOpen = useRef(false);
@@ -121,6 +125,7 @@ function WorkspaceExpensifyCardPageEmptyState({route, policy}: WorkspaceExpensif
route={route}
showLoadingAsFirstRender={false}
shouldShowOfflineIndicatorInWideScreen
+ policyFeature={CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD}
addBottomSafeAreaPadding
>
@@ -128,7 +133,7 @@ function WorkspaceExpensifyCardPageEmptyState({route, policy}: WorkspaceExpensif
menuItems={isUkEuCurrencySupported ? expensifyCardFeatures.slice(1) : expensifyCardFeatures}
title={translate('workspace.moreFeatures.expensifyCard.feed.title')}
subtitle={translate('workspace.moreFeatures.expensifyCard.feed.subTitle')}
- ctaText={translate(isSetupUnfinished ? 'workspace.expensifyCard.finishSetup' : 'workspace.expensifyCard.issueNewCard')}
+ ctaText={canWriteExpensifyCard ? translate(isSetupUnfinished ? 'workspace.expensifyCard.finishSetup' : 'workspace.expensifyCard.issueNewCard') : undefined}
ctaAccessibilityLabel={translate('workspace.moreFeatures.expensifyCard.feed.ctaTitle')}
onCtaPress={() => {
if (isDelegateAccessRestricted) {
diff --git a/src/pages/workspace/hr/HRProviderCard.tsx b/src/pages/workspace/hr/HRProviderCard.tsx
index 718e001cebaa..1f98445de92f 100644
--- a/src/pages/workspace/hr/HRProviderCard.tsx
+++ b/src/pages/workspace/hr/HRProviderCard.tsx
@@ -30,9 +30,12 @@ type HRProviderCardProps = {
/** Callback invoked when the user taps the "Connect" button for an unconnected provider. */
handleConnect: () => void;
+
+ /** Whether the current user can edit this HR connection. */
+ canWriteMoreFeatures: boolean;
};
-function HRProviderCard({card, policy, handleConnect}: HRProviderCardProps) {
+function HRProviderCard({card, policy, handleConnect, canWriteMoreFeatures}: HRProviderCardProps) {
const {translate, datetimeToRelative} = useLocalize();
const styles = useThemeStyles();
const {isOffline} = useNetwork();
@@ -84,8 +87,10 @@ function HRProviderCard({card, policy, handleConnect}: HRProviderCardProps) {
},
];
- let rightInset;
- if (!card.isConnected) {
+ let rightInset: React.ReactNode;
+ if (!canWriteMoreFeatures) {
+ rightInset = null;
+ } else if (!card.isConnected) {
rightInset = (
@@ -143,9 +148,9 @@ function HRProviderCard({card, policy, handleConnect}: HRProviderCardProps) {
description={translate('workspace.hr.approvalMode')}
title={card.approvalModeLabel}
style={[styles.sectionMenuItemTopDescription, styles.mt2]}
- shouldShowRightIcon
+ shouldShowRightIcon={canWriteMoreFeatures}
brickRoadIndicator={card.config?.errorFields?.approvalMode ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
- onPress={() => Navigation.navigate(approvalModeRoute)}
+ onPress={canWriteMoreFeatures ? () => Navigation.navigate(approvalModeRoute) : undefined}
/>
)}
@@ -159,9 +164,9 @@ function HRProviderCard({card, policy, handleConnect}: HRProviderCardProps) {
description={translate('workspace.hr.finalApprover')}
title={card.finalApproverDisplayName}
style={styles.sectionMenuItemTopDescription}
- shouldShowRightIcon
+ shouldShowRightIcon={canWriteMoreFeatures}
brickRoadIndicator={card.config?.errorFields?.finalApprover ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
- onPress={() => Navigation.navigate(finalApproverRoute)}
+ onPress={canWriteMoreFeatures ? () => Navigation.navigate(finalApproverRoute) : undefined}
/>
)}
diff --git a/src/pages/workspace/hr/WorkspaceHRPage.tsx b/src/pages/workspace/hr/WorkspaceHRPage.tsx
index eb9bb7320a8d..e552ddbd1fa2 100644
--- a/src/pages/workspace/hr/WorkspaceHRPage.tsx
+++ b/src/pages/workspace/hr/WorkspaceHRPage.tsx
@@ -8,6 +8,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useHRSyncResultsModal from '@hooks/useHRSyncResultsModal';
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
@@ -22,6 +23,7 @@ import {openPolicyHRPage} from '@libs/actions/PolicyConnections';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
+import {canMemberWrite} from '@libs/PolicyUtils';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -45,6 +47,7 @@ function WorkspaceHRPage({
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const policy = usePolicy(policyID);
+ const {login: currentUserLogin = ''} = useCurrentUserPersonalDetails();
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`);
const icons = useMemoizedLazyExpensifyIcons(['GustoSquare', 'TriNetSquare']);
const illustrations = useMemoizedLazyIllustrations(['NewUser']);
@@ -81,6 +84,7 @@ function WorkspaceHRPage({
disconnectedCards.sort(byName);
const shouldBeBlocked = !HR_BETAS.some(isBetaEnabled);
+ const canWriteMoreFeatures = canMemberWrite(policy, currentUserLogin, CONST.POLICY.POLICY_FEATURE.MORE_FEATURES);
const handleConnect = (setupLink: string | undefined) => {
if (!setupLink) {
@@ -95,6 +99,7 @@ function WorkspaceHRPage({
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED}
+ policyFeature={CONST.POLICY.POLICY_FEATURE.MORE_FEATURES}
shouldBeBlocked={shouldBeBlocked}
>
handleConnect(card.setupLink)}
+ canWriteMoreFeatures={canWriteMoreFeatures}
/>
))}
{connectedCards.length === 0 &&
@@ -140,6 +146,7 @@ function WorkspaceHRPage({
card={card}
policy={policy}
handleConnect={() => handleConnect(card.setupLink)}
+ canWriteMoreFeatures={canWriteMoreFeatures}
/>
))}
@@ -157,6 +164,7 @@ function WorkspaceHRPage({
card={card}
policy={policy}
handleConnect={() => handleConnect(card.setupLink)}
+ canWriteMoreFeatures={canWriteMoreFeatures}
/>
))}
diff --git a/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx
index 81833f80be25..1e86ca5a525f 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx
+++ b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx
@@ -29,12 +29,15 @@ import ROUTES from '@src/ROUTES';
type WorkspaceInvoiceVBASectionProps = {
/** The policy ID currently being configured */
policyID: string;
+
+ /** Whether the current user can edit invoicing settings. */
+ canWriteMoreFeatures: boolean;
};
type CurrencyType = TupleToUnion;
// TODO: can be refactored to use ThreeDotsMenu component instead handling the popover and positioning
-function WorkspaceInvoiceVBASection({policyID}: WorkspaceInvoiceVBASectionProps) {
+function WorkspaceInvoiceVBASection({policyID, canWriteMoreFeatures}: WorkspaceInvoiceVBASectionProps) {
const icons = useMemoizedLazyExpensifyIcons(['Star', 'Trashcan']);
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
@@ -242,7 +245,8 @@ function WorkspaceInvoiceVBASection({policyID}: WorkspaceInvoiceVBASectionProps)
shouldSkipDefaultAccountValidation={!isSupportedGlobalReimbursement}
invoiceTransferBankAccountID={transferBankAccountID}
activePaymentMethodID={transferBankAccountID}
- threeDotsMenuItems={threeDotsMenuItems}
+ threeDotsMenuItems={canWriteMoreFeatures ? threeDotsMenuItems : undefined}
+ shouldShowAddBankAccount={canWriteMoreFeatures}
style={[styles.mt5, shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8]}
listItemStyle={shouldUseNarrowLayout ? styles.ph5 : styles.ph8}
policyID={policyID}
diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
index 85d49ca58bff..21dd4aad469f 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
+++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import {View} from 'react-native';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
@@ -7,6 +8,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import {canMemberWrite} from '@libs/PolicyUtils';
import type {WorkspaceSplitNavigatorParamList} from '@navigation/types';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
@@ -24,12 +26,15 @@ function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) {
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const illustrations = useMemoizedLazyIllustrations(['InvoiceBlue']);
+ const {login: currentUserLogin = ''} = useCurrentUserPersonalDetails();
+ const canWriteMoreFeatures = canMemberWrite(policy, currentUserLogin, CONST.POLICY.POLICY_FEATURE.MORE_FEATURES);
return (
{(policyID?: string) => (
{!!policyID && }
- {!!policyID && }
- {!!policyID && }
+ {!!policyID && (
+
+ )}
+ {!!policyID && (
+
+ )}
)}
diff --git a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx
index 06bdec66f9a1..dd3f06d40487 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx
+++ b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx
@@ -13,9 +13,12 @@ import ROUTES from '@src/ROUTES';
type WorkspaceInvoicingDetailsSectionProps = {
/** The current policy ID */
policyID: string;
+
+ /** Whether the current user can edit invoicing details. */
+ canWriteMoreFeatures: boolean;
};
-function WorkspaceInvoicingDetailsSection({policyID}: WorkspaceInvoicingDetailsSectionProps) {
+function WorkspaceInvoicingDetailsSection({policyID, canWriteMoreFeatures}: WorkspaceInvoicingDetailsSectionProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
@@ -35,20 +38,20 @@ function WorkspaceInvoicingDetailsSection({policyID}: WorkspaceInvoicingDetailsS
>
Navigation.navigate(ROUTES.WORKSPACE_INVOICES_COMPANY_NAME.getRoute(policyID))}
+ onPress={canWriteMoreFeatures ? () => Navigation.navigate(ROUTES.WORKSPACE_INVOICES_COMPANY_NAME.getRoute(policyID)) : undefined}
style={horizontalPadding}
/>
Navigation.navigate(ROUTES.WORKSPACE_INVOICES_COMPANY_WEBSITE.getRoute(policyID))}
+ onPress={canWriteMoreFeatures ? () => Navigation.navigate(ROUTES.WORKSPACE_INVOICES_COMPANY_WEBSITE.getRoute(policyID)) : undefined}
style={horizontalPadding}
/>
diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx
index a9e21cb185f4..bfbd96a5db0c 100644
--- a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx
+++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx
@@ -18,6 +18,7 @@ import SelectionListWithModal from '@components/SelectionListWithModal';
import Text from '@components/Text';
import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions';
import useConfirmModal from '@hooks/useConfirmModal';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useGenericEmptyStateIllustration from '@hooks/useGenericEmptyStateIllustration';
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
@@ -37,7 +38,7 @@ import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
import {hasEnabledOptions} from '@libs/OptionsListUtils';
-import {getPerDiemCustomUnit} from '@libs/PolicyUtils';
+import {canMemberWrite, getPerDiemCustomUnit} from '@libs/PolicyUtils';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import tokenizedSearch from '@libs/tokenizedSearch';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
@@ -128,6 +129,8 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
const policyID = route.params.policyID;
const backTo = route.params?.backTo;
const policy = usePolicy(policyID);
+ const {email: currentUserEmail = ''} = useCurrentUserPersonalDetails();
+ const canWritePerDiem = canMemberWrite(policy, currentUserEmail, CONST.POLICY.POLICY_FEATURE.PER_DIEM);
useWorkspaceDocumentTitle(policy?.name, 'workspace.common.perDiem');
const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`);
const isMobileSelectionModeEnabled = useMobileSelectionMode();
@@ -143,7 +146,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
return [customUnits, allRates, allSubRatesMemo];
}, [policy]);
- const canSelectMultiple = shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true;
+ const canSelectMultiple = canWritePerDiem && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true);
const fetchPerDiem = useCallback(() => {
openPolicyPerDiemPage(policyID);
@@ -256,6 +259,9 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
}, [policyID]);
const openSubRateDetails = (rate: PolicyOption) => {
+ if (!canWritePerDiem) {
+ return;
+ }
if (isSmallScreenWidth && isMobileSelectionModeEnabled) {
toggleSubRate(rate);
return;
@@ -283,7 +289,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
const secondaryActions = useMemo(() => {
const menuItems = [];
- if (policy?.areCategoriesEnabled && hasEnabledOptions(policyCategories ?? {})) {
+ if (canWritePerDiem && policy?.areCategoriesEnabled && hasEnabledOptions(policyCategories ?? {})) {
menuItems.push({
icon: expensifyIcons.Gear,
text: translate('common.settings'),
@@ -291,18 +297,20 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
value: CONST.POLICY.SECONDARY_ACTIONS.SETTINGS,
});
}
- menuItems.push({
- icon: expensifyIcons.Table,
- text: translate('spreadsheet.importSpreadsheet'),
- onSelected: () => {
- if (isOffline) {
- showOfflineModal();
- return;
- }
- Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_IMPORT.getRoute(policyID));
- },
- value: CONST.POLICY.SECONDARY_ACTIONS.IMPORT_SPREADSHEET,
- });
+ if (canWritePerDiem) {
+ menuItems.push({
+ icon: expensifyIcons.Table,
+ text: translate('spreadsheet.importSpreadsheet'),
+ onSelected: () => {
+ if (isOffline) {
+ showOfflineModal();
+ return;
+ }
+ Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_IMPORT.getRoute(policyID));
+ },
+ value: CONST.POLICY.SECONDARY_ACTIONS.IMPORT_SPREADSHEET,
+ });
+ }
if (hasVisibleSubRates) {
menuItems.push({
icon: expensifyIcons.Download,
@@ -331,6 +339,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
showOfflineModal,
policy?.areCategoriesEnabled,
policyCategories,
+ canWritePerDiem,
translate,
hasVisibleSubRates,
openSettings,
@@ -380,6 +389,10 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
);
}
+ if (secondaryActions.length === 0) {
+ return null;
+ }
+
return (
{!shouldDisplayButtonsInSeparateLine && getHeaderButtons()}
- {shouldDisplayButtonsInSeparateLine && {getHeaderButtons()}}
+ {!!getHeaderButtons() && shouldDisplayButtonsInSeparateLine && {getHeaderButtons()}}
{(!hasVisibleSubRates || isLoading) && headerContent}
{isLoading && (
item.subRateID)}
- onSelectAll={filteredSubRatesList.length > 0 ? toggleAllSubRates : undefined}
+ onSelectAll={canWritePerDiem && filteredSubRatesList.length > 0 ? toggleAllSubRates : undefined}
style={{listItemTitleContainerStyles: styles.flex3}}
- onTurnOnSelectionMode={(item) => item && toggleSubRate(item)}
+ onTurnOnSelectionMode={(item) => canWritePerDiem && item && toggleSubRate(item)}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
customListHeaderContent={headerContent}
shouldShowListEmptyContent={false}
showScrollIndicator={false}
turnOnSelectionModeOnLongPress
shouldHeaderBeInsideList
- shouldShowRightCaret
+ shouldShowRightCaret={canWritePerDiem}
/>
)}
{!hasVisibleSubRates && !isLoading && (
@@ -507,19 +521,23 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
title={translate('workspace.perDiem.emptyList.title')}
subtitle={translate('workspace.perDiem.emptyList.subtitle')}
headerStyles={styles.emptyStateCardIllustrationContainer}
- buttons={[
- {
- buttonText: translate('spreadsheet.importSpreadsheet'),
- buttonAction: () => {
- if (isOffline) {
- showOfflineModal();
- return;
- }
- Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_IMPORT.getRoute(policyID));
- },
- success: true,
- },
- ]}
+ buttons={
+ canWritePerDiem
+ ? [
+ {
+ buttonText: translate('spreadsheet.importSpreadsheet'),
+ buttonAction: () => {
+ if (isOffline) {
+ showOfflineModal();
+ return;
+ }
+ Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_IMPORT.getRoute(policyID));
+ },
+ success: true,
+ },
+ ]
+ : []
+ }
/>
)}
diff --git a/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx b/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx
index cb034fa156a0..f55908bc5f2a 100644
--- a/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx
+++ b/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx
@@ -18,6 +18,7 @@ import {useMemoizedLazyAsset, useMemoizedLazyExpensifyIcons} from '@hooks/useLaz
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePolicy from '@hooks/usePolicy';
+import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -60,6 +61,7 @@ function WorkspaceReceiptPartnersPage({route}: WorkspaceReceiptPartnersPageProps
const {asset: ReceiptPartners} = useMemoizedLazyAsset(() => loadIllustration('ReceiptPartners' as IllustrationName));
// Track focus and connection change to route to the invite flow once after successful connection
const prevIsUberConnected = usePrevious(isUberConnected);
+ const {canWrite: canWriteMoreFeatures, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.MORE_FEATURES);
const startIntegrationFlow = useCallback(
({name}: {name: string}) => {
@@ -89,11 +91,11 @@ function WorkspaceReceiptPartnersPage({route}: WorkspaceReceiptPartnersPageProps
// When Uber connection status flips from false -> true, navigate to the invite flow once
useEffect(() => {
- if (!isUberConnected || prevIsUberConnected) {
+ if (!isUberConnected || prevIsUberConnected || !canWriteMoreFeatures) {
return;
}
Navigation.navigate(ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE.getRoute(policyID, CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER));
- }, [prevIsUberConnected, isUberConnected, policyID]);
+ }, [prevIsUberConnected, isUberConnected, policyID, canWriteMoreFeatures]);
const calculateAndSetThreeDotsMenuPosition = useCallback(() => {
if (shouldUseNarrowLayout) {
@@ -174,7 +176,7 @@ function WorkspaceReceiptPartnersPage({route}: WorkspaceReceiptPartnersPageProps
if (!integrationData) {
return undefined;
}
- const overflowMenu = getOverflowMenu(integration);
+ const overflowMenu = canWriteMoreFeatures ? getOverflowMenu(integration) : [];
const iconProps = integrationData?.icon
? {
@@ -184,6 +186,32 @@ function WorkspaceReceiptPartnersPage({route}: WorkspaceReceiptPartnersPageProps
: {};
const isUber = integration === CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER;
+ let rightComponent: React.ReactNode;
+ if (canWriteMoreFeatures && (isUberConnected || shouldShowEnterCredentialsError)) {
+ rightComponent = (
+
+
+
+ );
+ } else if (canWriteMoreFeatures) {
+ rightComponent = (
+
@@ -323,6 +336,9 @@ function WorkspaceReceiptPartnersPage({route}: WorkspaceReceiptPartnersPageProps
switchAccessibilityLabel={translate('workspace.receiptPartners.uber.autoRemove')}
onToggle={toggleWorkspaceUberAutoRemove}
isActive={isAutoRemove}
+ disabled={!canWriteMoreFeatures}
+ disabledAction={showReadOnlyModal}
+ showLockIcon={!canWriteMoreFeatures}
/>
@@ -331,23 +347,28 @@ function WorkspaceReceiptPartnersPage({route}: WorkspaceReceiptPartnersPageProps
- Navigation.navigate(
- ROUTES.WORKSPACE_RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT.getRoute(policyID, CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER),
- )
+ onPress={
+ canWriteMoreFeatures
+ ? () =>
+ Navigation.navigate(
+ ROUTES.WORKSPACE_RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT.getRoute(policyID, CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER),
+ )
+ : undefined
}
/>
)}
- Navigation.navigate(ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT.getRoute(policyID, CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER))}
- />
+ {canWriteMoreFeatures && (
+ Navigation.navigate(ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT.getRoute(policyID, CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER))}
+ />
+ )}
>
)}
diff --git a/src/pages/workspace/reports/WorkspaceReportsPage.tsx b/src/pages/workspace/reports/WorkspaceReportsPage.tsx
index 517fed78a904..cc5403362344 100644
--- a/src/pages/workspace/reports/WorkspaceReportsPage.tsx
+++ b/src/pages/workspace/reports/WorkspaceReportsPage.tsx
@@ -22,6 +22,7 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePolicy from '@hooks/usePolicy';
+import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle';
@@ -67,6 +68,7 @@ function WorkspaceReportFieldsPage({
const {translate, localeCompare} = useLocalize();
const policy = usePolicy(policyID);
const {showConfirmModal} = useConfirmModal();
+ const {canWrite: canWriteReportFields, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.REPORT_FIELDS);
useWorkspaceDocumentTitle(policy?.name, 'workspace.common.reports');
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`);
const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy);
@@ -122,6 +124,10 @@ function WorkspaceReportFieldsPage({
: [];
const navigateToReportFieldsSettings = (reportField: ReportFieldForList) => {
+ if (!canWriteReportFields) {
+ return;
+ }
+
Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_SETTINGS.getRoute(policyID, reportField.fieldID));
};
@@ -149,8 +155,8 @@ function WorkspaceReportFieldsPage({
onPress={() => navigateToReportFieldsSettings(item)}
description={item.text}
disabled={item.isDisabled}
- shouldShowRightIcon={!item.isDisabled}
- interactive={!item.isDisabled}
+ shouldShowRightIcon={!item.isDisabled && canWriteReportFields}
+ interactive={!item.isDisabled && canWriteReportFields}
rightLabel={item.rightLabel}
descriptionTextStyle={[styles.popoverMenuText, styles.textStrong]}
/>
@@ -200,6 +206,7 @@ function WorkspaceReportFieldsPage({
Navigation.navigate(ROUTES.REPORTS_DEFAULT_TITLE.getRoute(policyID))}
+ onPress={canWriteReportFields ? () => Navigation.navigate(ROUTES.REPORTS_DEFAULT_TITLE.getRoute(policyID)) : undefined}
+ interactive={canWriteReportFields}
/>
{
+ if (!canWriteReportFields) {
+ showReadOnlyModal();
+ return;
+ }
+
if (isEnabled && !isControlPolicy(policy)) {
Navigation.navigate(
ROUTES.WORKSPACE_UPGRADE.getRoute(
@@ -268,6 +281,9 @@ function WorkspaceReportFieldsPage({
setPolicyPreventMemberCreatedTitle(policyID, isEnabled, policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE]);
}}
+ disabled={!canWriteReportFields}
+ disabledAction={showReadOnlyModal}
+ showLockIcon={!canWriteReportFields}
/>
{
+ if (!canWriteReportFields) {
+ showReadOnlyModal();
+ return;
+ }
+
if (!isEnabled) {
showConfirmModal({
danger: true,
@@ -307,8 +328,9 @@ function WorkspaceReportFieldsPage({
}
enablePolicyReportFields(policyID, isEnabled);
}}
- disabled={hasAccountingConnections}
- disabledAction={onDisabledOrganizeSwitchPress}
+ disabled={hasAccountingConnections || !canWriteReportFields}
+ disabledAction={canWriteReportFields ? onDisabledOrganizeSwitchPress : showReadOnlyModal}
+ showLockIcon={!canWriteReportFields}
subMenuItems={
!!policy?.areReportFieldsEnabled && (
<>
@@ -320,7 +342,7 @@ function WorkspaceReportFieldsPage({
maintainVisibleContentPosition={{disabled: true}}
/>
- {!hasAccountingConnections && (
+ {!hasAccountingConnections && canWriteReportFields && (
Navigation.navigate(ROUTES.WORKSPACE_CREATE_REPORT_FIELD.getRoute(policyID))}
title={translate('workspace.reportFields.addField')}
diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx
index fe9d4a8ad7a7..6dfb19686db4 100644
--- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx
+++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx
@@ -16,9 +16,12 @@ import ROUTES from '@src/ROUTES';
type ExpenseReportRulesSectionProps = {
policyID: string;
+ canWriteApprovals: boolean;
+ canWritePayments: boolean;
+ showReadOnlyModal: () => void;
};
-function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) {
+function ExpenseReportRulesSection({policyID, canWriteApprovals, canWritePayments, showReadOnlyModal}: ExpenseReportRulesSectionProps) {
const {convertToDisplayString} = useCurrencyListActions();
const {translate} = useLocalize();
const styles = useThemeStyles();
@@ -45,8 +48,9 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) {
shouldParseSubtitle: workflowApprovalsUnavailable,
switchAccessibilityLabel: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'),
isActive: policy?.preventSelfApproval,
- disabled: workflowApprovalsUnavailable,
- showLockIcon: workflowApprovalsUnavailable,
+ disabled: workflowApprovalsUnavailable || !canWriteApprovals,
+ disabledAction: !canWriteApprovals ? showReadOnlyModal : undefined,
+ showLockIcon: workflowApprovalsUnavailable || !canWriteApprovals,
pendingAction: policy?.pendingFields?.preventSelfApproval ?? policy?.pendingAction,
onToggle: (isEnabled: boolean) => {
if (isEnabled && !isControlPolicy(policy)) {
@@ -67,8 +71,9 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) {
shouldParseSubtitle: workflowApprovalsUnavailable,
switchAccessibilityLabel: translate('workspace.rules.expenseReportRules.autoApproveCompliantReportsTitle'),
isActive: policy?.shouldShowAutoApprovalOptions && !workflowApprovalsUnavailable,
- disabled: workflowApprovalsUnavailable,
- showLockIcon: workflowApprovalsUnavailable,
+ disabled: workflowApprovalsUnavailable || !canWriteApprovals,
+ disabledAction: !canWriteApprovals ? showReadOnlyModal : undefined,
+ showLockIcon: workflowApprovalsUnavailable || !canWriteApprovals,
pendingAction: policy?.pendingFields?.shouldShowAutoApprovalOptions ?? policy?.pendingAction,
onToggle: (isEnabled: boolean) => {
if (isEnabled && !isControlPolicy(policy)) {
@@ -88,9 +93,9 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) {
Navigation.navigate(ROUTES.RULES_AUTO_APPROVE_REPORTS_UNDER.getRoute(policyID))}
+ onPress={canWriteApprovals ? () => Navigation.navigate(ROUTES.RULES_AUTO_APPROVE_REPORTS_UNDER.getRoute(policyID)) : undefined}
/>
,
Navigation.navigate(ROUTES.RULES_RANDOM_REPORT_AUDIT.getRoute(policyID))}
+ onPress={canWriteApprovals ? () => Navigation.navigate(ROUTES.RULES_RANDOM_REPORT_AUDIT.getRoute(policyID)) : undefined}
/>
,
],
@@ -126,8 +131,9 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) {
enablePolicyAutoReimbursementLimit(policyID, isEnabled, policy?.shouldShowAutoReimbursementLimitOption, policy?.autoReimbursement?.limit);
},
- disabled: autoPayApprovedReportsUnavailable,
- showLockIcon: autoPayApprovedReportsUnavailable,
+ disabled: autoPayApprovedReportsUnavailable || !canWritePayments,
+ disabledAction: !canWritePayments ? showReadOnlyModal : undefined,
+ showLockIcon: autoPayApprovedReportsUnavailable || !canWritePayments,
isActive: policy?.shouldShowAutoReimbursementLimitOption && !autoPayApprovedReportsUnavailable,
pendingAction: policy?.pendingFields?.shouldShowAutoReimbursementLimitOption ?? policy?.pendingAction,
subMenuItems: [
@@ -142,9 +148,9 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) {
Navigation.navigate(ROUTES.RULES_AUTO_PAY_REPORTS_UNDER.getRoute(policyID))}
+ onPress={canWritePayments ? () => Navigation.navigate(ROUTES.RULES_AUTO_PAY_REPORTS_UNDER.getRoute(policyID)) : undefined}
/>
,
],
@@ -160,7 +166,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) {
subtitleTextStyles={policy?.pendingAction ? styles.opacitySemiTransparent : undefined}
subtitleMuted
>
- {optionItems.map(({title, subtitle, shouldParseSubtitle, isActive, subMenuItems, showLockIcon, disabled, onToggle, pendingAction}, index) => {
+ {optionItems.map(({title, subtitle, shouldParseSubtitle, isActive, subMenuItems, showLockIcon, disabled, disabledAction, onToggle, pendingAction}, index) => {
const showBorderBottom = index !== optionItems.length - 1;
return (
@@ -176,6 +182,7 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) {
isActive={!!isActive}
showLockIcon={showLockIcon}
disabled={disabled}
+ disabledAction={disabledAction}
subMenuItems={subMenuItems}
onToggle={onToggle}
pendingAction={pendingAction}
diff --git a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx
index 15da61a2f56e..d344c4474541 100644
--- a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx
+++ b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx
@@ -23,6 +23,8 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
type IndividualExpenseRulesSectionProps = {
policyID: string;
+ canWriteRules: boolean;
+ showReadOnlyModal: () => void;
};
type IndividualExpenseRulesSectionSubtitleProps = {
@@ -60,7 +62,7 @@ function IndividualExpenseRulesSectionSubtitle({policy, translate, environmentUR
return ;
}
-function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSectionProps) {
+function IndividualExpenseRulesSection({policyID, canWriteRules, showReadOnlyModal}: IndividualExpenseRulesSectionProps) {
const {convertToDisplayString} = useCurrencyListActions();
const {translate} = useLocalize();
const styles = useThemeStyles();
@@ -223,11 +225,11 @@ function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSection
key={translate(item.descriptionTranslationKey)}
>
@@ -237,15 +239,16 @@ function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSection
title={translate('workspace.rules.individualExpenseRules.requireCompanyCard')}
subtitle={translate('workspace.rules.individualExpenseRules.requireCompanyCardDescription')}
switchAccessibilityLabel={translate('workspace.rules.individualExpenseRules.requireCompanyCard')}
- disabled={disableRequireCompanyCardToggle}
- showLockIcon={disableRequireCompanyCardToggle}
+ disabled={!canWriteRules || disableRequireCompanyCardToggle}
+ disabledAction={!canWriteRules ? showReadOnlyModal : undefined}
+ showLockIcon={!canWriteRules || disableRequireCompanyCardToggle}
disabledText={translate('workspace.rules.individualExpenseRules.requireCompanyCardDisabledTooltip')}
wrapperStyle={[styles.mt3]}
titleStyle={styles.pv2}
subtitleStyle={styles.pt1}
isActive={requireCompanyCardsEnabled}
pendingAction={policy?.pendingFields?.requireCompanyCardsEnabled}
- onToggle={() => (policy ? setPolicyRequireCompanyCardsEnabled(policy, !requireCompanyCardsEnabled) : undefined)}
+ onToggle={() => (canWriteRules && policy ? setPolicyRequireCompanyCardsEnabled(policy, !requireCompanyCardsEnabled) : undefined)}
/>
setWorkspaceEReceiptsEnabled(policyID, !areEReceiptsEnabled, policy?.eReceipts)}
+ disabled={!canWriteRules || policyCurrency !== CONST.CURRENCY.USD}
+ disabledAction={!canWriteRules ? showReadOnlyModal : undefined}
+ showLockIcon={!canWriteRules || policyCurrency !== CONST.CURRENCY.USD}
+ onToggle={() => (canWriteRules ? setWorkspaceEReceiptsEnabled(policyID, !areEReceiptsEnabled, policy?.eReceipts) : undefined)}
pendingAction={policy?.pendingFields?.eReceipts}
/>
handleAttendeeTrackingToggle(!isAttendeeTrackingEnabledForPolicy)}
+ disabled={!canWriteRules}
+ disabledAction={!canWriteRules ? showReadOnlyModal : undefined}
+ showLockIcon={!canWriteRules}
+ onToggle={() => (canWriteRules ? handleAttendeeTrackingToggle(!isAttendeeTrackingEnabledForPolicy) : undefined)}
pendingAction={policy?.pendingFields?.isAttendeeTrackingEnabled}
/>
diff --git a/src/pages/workspace/rules/MerchantRulesSection.tsx b/src/pages/workspace/rules/MerchantRulesSection.tsx
index 341e4c723cd8..fc3701b9bd95 100644
--- a/src/pages/workspace/rules/MerchantRulesSection.tsx
+++ b/src/pages/workspace/rules/MerchantRulesSection.tsx
@@ -27,6 +27,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
type MerchantRulesSectionProps = {
policyID: string;
+ canWriteRules: boolean;
};
type FieldLabels = {
@@ -69,7 +70,7 @@ function getRuleDescription(rule: CodingRule, translate: ReturnType (index === 0 ? action : action.charAt(0).toLowerCase() + action.slice(1))).join(', ');
}
-function MerchantRulesSection({policyID}: MerchantRulesSectionProps) {
+function MerchantRulesSection({policyID, canWriteRules}: MerchantRulesSectionProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
@@ -171,8 +172,8 @@ function MerchantRulesSection({policyID}: MerchantRulesSectionProps) {
wrapperStyle={[styles.borderedContentCard, styles.ph4, styles.pv4]}
descriptionTextStyle={[styles.textNormalThemeText, {lineHeight: variables.fontSizeNormalHeight}]}
titleStyle={[styles.textLabelSupporting, styles.fontSizeLabel]}
- shouldShowRightIcon
- onPress={() => Navigation.navigate(ROUTES.RULES_MERCHANT_EDIT.getRoute(policyID, rule.ruleID))}
+ shouldShowRightIcon={canWriteRules}
+ onPress={canWriteRules ? () => Navigation.navigate(ROUTES.RULES_MERCHANT_EDIT.getRoute(policyID, rule.ruleID)) : undefined}
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.MERCHANT_RULE_ITEM}
disabled={rule.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}
/>
@@ -182,16 +183,18 @@ function MerchantRulesSection({policyID}: MerchantRulesSectionProps) {
})}
)}
- Navigation.navigate(ROUTES.RULES_MERCHANT_NEW.getRoute(policyID))}
- sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.ADD_MERCHANT_RULE}
- />
+ {canWriteRules && (
+ Navigation.navigate(ROUTES.RULES_MERCHANT_NEW.getRoute(policyID))}
+ sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.ADD_MERCHANT_RULE}
+ />
+ )}
);
}
diff --git a/src/pages/workspace/rules/PolicyRulesPage.tsx b/src/pages/workspace/rules/PolicyRulesPage.tsx
index 1685065dc958..181bb58e9962 100644
--- a/src/pages/workspace/rules/PolicyRulesPage.tsx
+++ b/src/pages/workspace/rules/PolicyRulesPage.tsx
@@ -3,6 +3,7 @@ import {View} from 'react-native';
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
+import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle';
@@ -27,6 +28,7 @@ function PolicyRulesPage({route}: PolicyRulesPageProps) {
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const illustrations = useMemoizedLazyIllustrations(['Rules']);
+ const {canWrite: canWriteRules, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.RULES);
const fetchRules = useCallback(() => {
openPolicyRulesPage(policyID);
@@ -41,6 +43,7 @@ function PolicyRulesPage({route}: PolicyRulesPageProps) {
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED}
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
+ policyFeature={CONST.POLICY.POLICY_FEATURE.RULES}
>
-
-
- {!!policy?.areExpensifyCardsEnabled && }
+
+
+ {!!policy?.areExpensifyCardsEnabled && (
+
+ )}
diff --git a/src/pages/workspace/rules/SpendRules/SpendRulesSection.tsx b/src/pages/workspace/rules/SpendRules/SpendRulesSection.tsx
index e589e25be767..5f47343c19fc 100644
--- a/src/pages/workspace/rules/SpendRules/SpendRulesSection.tsx
+++ b/src/pages/workspace/rules/SpendRules/SpendRulesSection.tsx
@@ -32,9 +32,10 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
type SpendRulesSectionProps = {
policyID: string;
+ canWriteRules: boolean;
};
-function SpendRulesSection({policyID}: SpendRulesSectionProps) {
+function SpendRulesSection({policyID, canWriteRules}: SpendRulesSectionProps) {
const {convertToDisplayString} = useCurrencyListActions();
const {translate, localeCompare} = useLocalize();
const styles = useThemeStyles();
@@ -184,8 +185,8 @@ function SpendRulesSection({policyID}: SpendRulesSectionProps) {
titleComponent={menuItemBody}
accessibilityLabel={`${descriptionLabel}. ${blockLabel} ${defaultRuleTitle}`}
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.SPEND_RULE_ITEM}
- onPress={showBuiltInProtectionModal}
- shouldShowRightIcon
+ onPress={canWriteRules ? showBuiltInProtectionModal : undefined}
+ shouldShowRightIcon={canWriteRules}
/>
{isSpendRulesListLoading ? (
@@ -238,14 +239,14 @@ function SpendRulesSection({policyID}: SpendRulesSectionProps) {
}
accessibilityLabel={`${rule.summaryParts.map((part) => `${part.badgeLabel}. ${part.text}`).join('. ')}. ${rule.cardSummary}`}
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.SPEND_RULE_ITEM}
- shouldShowRightIcon
+ shouldShowRightIcon={canWriteRules}
disabled={rule.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}
- onPress={() => Navigation.navigate(ROUTES.RULES_SPEND_EDIT.getRoute(policyID, rule.ruleID))}
+ onPress={canWriteRules ? () => Navigation.navigate(ROUTES.RULES_SPEND_EDIT.getRoute(policyID, rule.ruleID)) : undefined}
/>
))
)}
- {!isProduction && (
+ {!isProduction && canWriteRules && (
{
@@ -217,16 +219,26 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
const updateWorkspaceTagEnabled = useCallback(
(value: boolean, tagName: string) => {
+ if (!canWriteTags) {
+ showReadOnlyModal();
+ return;
+ }
+
setWorkspaceTagEnabled(policyData, {[tagName]: {name: tagName, enabled: value}}, 0);
},
- [policyData],
+ [canWriteTags, policyData, showReadOnlyModal],
);
const updateWorkspaceRequiresTag = useCallback(
(value: boolean, orderWeight: number) => {
+ if (!canWriteTags) {
+ showReadOnlyModal();
+ return;
+ }
+
setPolicyTagsRequired(policyData, value, orderWeight);
},
- [policyData],
+ [canWriteTags, policyData, showReadOnlyModal],
);
const glCodeContainerStyle = useMemo(() => [styles.flex1], [styles.flex1]);
const glCodeTextStyle = useMemo(() => [styles.alignSelfStart], [styles.alignSelfStart]);
@@ -238,23 +250,16 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
const areTagsEnabled = !!Object.values(policyTagList?.tags ?? {}).some((tag) => tag.enabled);
const isSwitchDisabled = !policyTagList.required && !areTagsEnabled;
const isSwitchEnabled = policyTagList.required && areTagsEnabled;
+ let rightElement;
- if (policyTagList.required && !areTagsEnabled) {
- updateWorkspaceRequiresTag(false, policyTagList.orderWeight);
- }
- return {
- value: policyTagList.name,
- orderWeight: policyTagList.orderWeight,
- text: getCleanedTagName(policyTagList.name),
- alternateText: !hasDependentTags ? translate('workspace.tags.tagCount', {count: Object.keys(policyTagList?.tags ?? {}).length}) : '',
- keyForList: getCleanedTagName(policyTagList.name),
- pendingAction: getPendingAction(policyTagList),
- enabled: true,
- required: policyTagList.required,
- isDisabledCheckbox: isSwitchDisabled,
- rightElement: hasDependentTags ? (
+ if (hasDependentTags) {
+ rightElement = canWriteTags ? (
) : (
+ {translate('workspace.tags.tagCount', {count: Object.keys(policyTagList?.tags ?? {}).length})}
+ );
+ } else {
+ rightElement = (
- ),
+ );
+ }
+
+ if (canWriteTags && policyTagList.required && !areTagsEnabled) {
+ updateWorkspaceRequiresTag(false, policyTagList.orderWeight);
+ }
+ return {
+ value: policyTagList.name,
+ orderWeight: policyTagList.orderWeight,
+ text: getCleanedTagName(policyTagList.name),
+ alternateText: !hasDependentTags ? translate('workspace.tags.tagCount', {count: Object.keys(policyTagList?.tags ?? {}).length}) : '',
+ keyForList: getCleanedTagName(policyTagList.name),
+ pendingAction: getPendingAction(policyTagList),
+ enabled: true,
+ required: policyTagList.required,
+ isDisabledCheckbox: isSwitchDisabled,
+ rightElement,
};
});
}
@@ -328,7 +350,8 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
{
if (isDisablingOrDeletingLastEnabledTag(policyTagLists.at(0), [tag])) {
@@ -342,14 +365,15 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
}
updateWorkspaceTagEnabled(newValue, tag.name);
}}
- showLockIcon={isDisablingOrDeletingLastEnabledTag(policyTagLists.at(0), [tag])}
+ showLockIcon={!canWriteTags || isDisablingOrDeletingLastEnabledTag(policyTagLists.at(0), [tag])}
/>
>
) : (
{
if (isDisablingOrDeletingLastEnabledTag(policyTagLists.at(0), [tag])) {
@@ -363,7 +387,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
}
updateWorkspaceTagEnabled(newValue, tag.name);
}}
- showLockIcon={isDisablingOrDeletingLastEnabledTag(policyTagLists.at(0), [tag])}
+ showLockIcon={!canWriteTags || isDisablingOrDeletingLastEnabledTag(policyTagLists.at(0), [tag])}
/>
),
};
@@ -386,6 +410,8 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
styles.alignItemsCenter,
styles.flexRow,
styles.mr3,
+ canWriteTags,
+ showReadOnlyModal,
]);
const filterTag = useCallback((tag: TagListItem, searchInput: string) => {
@@ -435,15 +461,15 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
canSelectMultiple={false}
leftHeaderText={translate('common.name')}
rightHeaderText={translate('common.count')}
- shouldShowRightCaret
+ shouldShowRightCaret={canWriteTags}
/>
);
}
// Show GL Code column only on wide screens for control policies. Approver column additionally requires rules to be enabled
if (isControlPolicyWithWideLayout && !isMultiLevelTags) {
- return (
-
+ const header = (
+
{translate('common.name')}
@@ -455,11 +481,17 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
{translate('common.approver')}
)}
-
+
{translate('common.enabled')}
);
+
+ if (canSelectMultiple) {
+ return header;
+ }
+
+ return {header};
}
return (
@@ -467,7 +499,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
canSelectMultiple={canSelectMultiple}
leftHeaderText={translate('common.name')}
rightHeaderText={translate(isMultiLevelTags ? 'common.required' : 'common.enabled')}
- shouldShowRightCaret
+ shouldShowRightCaret={canWriteTags}
/>
);
};
@@ -481,6 +513,10 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
};
const navigateToTagSettings = (tag: TagListItem) => {
+ if (!canWriteTags) {
+ return;
+ }
+
if (isSmallScreenWidth && isMobileSelectionModeEnabled) {
toggleTag(tag);
return;
@@ -532,14 +568,16 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy);
const secondaryActions = useMemo(() => {
const menuItems = [];
- menuItems.push({
- icon: expensifyIcons.Gear,
- text: translate('common.settings'),
- onSelected: navigateToTagsSettings,
- value: CONST.POLICY.SECONDARY_ACTIONS.SETTINGS,
- });
+ if (canWriteTags) {
+ menuItems.push({
+ icon: expensifyIcons.Gear,
+ text: translate('common.settings'),
+ onSelected: navigateToTagsSettings,
+ value: CONST.POLICY.SECONDARY_ACTIONS.SETTINGS,
+ });
+ }
- if (!hasAccountingConnections) {
+ if (canWriteTags && !hasAccountingConnections) {
menuItems.push({
icon: expensifyIcons.Table,
text: translate('spreadsheet.importSpreadsheet'),
@@ -601,16 +639,21 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
hasDependentTags,
expensifyIcons,
showConfirmModal,
+ canWriteTags,
]);
const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine();
const getHeaderButtons = () => {
+ if (!canWriteTags && secondaryActions.length === 0) {
+ return null;
+ }
+
const selectedTagsObject = selectedTags.map((key) => policyTagLists.at(0)?.tags?.[key]);
const selectedTagLists = selectedTags.map((selectedTag) => policyTagLists.find((policyTagList) => policyTagList.name === selectedTag));
- if (shouldUseNarrowLayout ? !isMobileSelectionModeEnabled : selectedTags.length === 0) {
- const hasPrimaryActions = !hasAccountingConnections && !isMultiLevelTags && hasVisibleTags;
+ if (!canWriteTags || (shouldUseNarrowLayout ? !isMobileSelectionModeEnabled : selectedTags.length === 0)) {
+ const hasPrimaryActions = canWriteTags && !hasAccountingConnections && !isMultiLevelTags && hasVisibleTags;
return (
{hasPrimaryActions && (
@@ -623,16 +666,18 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
style={[shouldDisplayButtonsInSeparateLine && styles.flex1]}
/>
)}
- {}}
- shouldAlwaysShowDropdownMenu
- customText={translate('common.more')}
- sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.TAGS.MORE_DROPDOWN}
- options={secondaryActions}
- isSplitButton={false}
- wrapperStyle={isInLandscapeMode || hasPrimaryActions ? styles.flexGrow0 : styles.flexGrow1}
- />
+ {secondaryActions.length > 0 && (
+ {}}
+ shouldAlwaysShowDropdownMenu
+ customText={translate('common.more')}
+ sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.TAGS.MORE_DROPDOWN}
+ options={secondaryActions}
+ isSplitButton={false}
+ wrapperStyle={isInLandscapeMode || hasPrimaryActions ? styles.flexGrow0 : styles.flexGrow1}
+ />
+ )}
);
}
@@ -862,6 +907,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
policyID={policyID}
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED}
+ policyFeature={CONST.POLICY.POLICY_FEATURE.TAGS}
>
{!shouldDisplayButtonsInSeparateLine && getHeaderButtons()}
- {shouldDisplayButtonsInSeparateLine && {getHeaderButtons()}}
+ {shouldDisplayButtonsInSeparateLine && !!getHeaderButtons() && {getHeaderButtons()}}
{(!hasVisibleTags || isLoading) && headerContent}
{isLoading && (
0 ? toggleAllTags : undefined}
+ onSelectAll={canWriteTags && filteredTagList.length > 0 ? toggleAllTags : undefined}
customListHeader={filteredTagList.length > 0 ? getCustomListHeader() : undefined}
onDismissError={(item) => !hasDependentTags && clearPolicyTagErrors({policyID, tagName: item.value, tagListIndex: 0, policyTags})}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
- onTurnOnSelectionMode={(item) => item && toggleTag(item)}
- turnOnSelectionModeOnLongPress={!hasDependentTags}
+ onTurnOnSelectionMode={(item) => item && canWriteTags && toggleTag(item)}
+ turnOnSelectionModeOnLongPress={canWriteTags && !hasDependentTags}
shouldSingleExecuteRowSelect={!canSelectMultiple}
customListHeaderContent={headerContent}
shouldShowListEmptyContent={false}
@@ -924,7 +970,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
onSelectionButtonPress={toggleTag}
isSelected={isTagSelected}
shouldHeaderBeInsideList
- shouldShowRightCaret
+ shouldShowRightCaret={canWriteTags}
/>
)}
{!hasVisibleTags && !isLoading && (
@@ -935,7 +981,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
subtitleText={subtitleText}
headerStyles={styles.emptyStateCardIllustrationContainer}
buttons={
- !hasAccountingConnections
+ canWriteTags && !hasAccountingConnections
? [
{
icon: expensifyIcons.Table,
diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx
index 231f3e039260..5b3ac02fe427 100644
--- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx
@@ -23,6 +23,7 @@ import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useNetwork from '@hooks/useNetwork';
import usePolicyData from '@hooks/usePolicyData';
+import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSearchBackPress from '@hooks/useSearchBackPress';
import useSearchResults from '@hooks/useSearchResults';
@@ -82,6 +83,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) {
const hasDependentTags = useMemo(() => hasDependentTagsPolicyUtils(policy, policyTags), [policy, policyTags]);
const isMultiLevelTags = isMultiLevelTagsPolicyUtils(policyTags);
const currentPolicyTag = policyTags?.[currentTagListName];
+ const {canWrite: canWriteTags, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.TAGS);
const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_TAGS.DYNAMIC_SETTINGS_TAG_LIST_VIEW;
const backPath = useDynamicBackPath(DYNAMIC_ROUTES.SETTINGS_TAG_LIST_VIEW.path);
const fetchTags = useCallback(() => {
@@ -94,11 +96,14 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) {
const {isOffline} = useNetwork({onReconnect: fetchTags});
const canSelectMultiple = useMemo(() => {
+ if (!canWriteTags) {
+ return false;
+ }
if (hasDependentTags) {
return false;
}
return isSmallScreenWidth ? isMobileSelectionModeEnabled : true;
- }, [hasDependentTags, isSmallScreenWidth, isMobileSelectionModeEnabled]);
+ }, [canWriteTags, hasDependentTags, isSmallScreenWidth, isMobileSelectionModeEnabled]);
useEffect(() => {
if (isFocused) {
@@ -119,47 +124,60 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) {
const updateWorkspaceTagEnabled = useCallback(
(value: boolean, tagName: string) => {
+ if (!canWriteTags) {
+ showReadOnlyModal();
+ return;
+ }
+
setWorkspaceTagEnabled(policyData, {[tagName]: {name: tagName, enabled: value}}, orderWeight);
},
- [policyData, orderWeight],
+ [canWriteTags, policyData, orderWeight, showReadOnlyModal],
);
const tagList = useMemo(
() =>
- Object.values(currentPolicyTag?.tags ?? {}).map((tag) => ({
- value: tag.name,
- text: hasDependentTags ? tag.name : getCleanedTagName(tag.name),
- keyForList: hasDependentTags ? `${tag.name}-${tag.rules?.parentTagsFilter ?? ''}` : tag.name,
- isSelected: selectedTags.includes(tag.name) && canSelectMultiple,
- pendingAction: tag.pendingAction,
- rules: tag.rules,
- errors: tag.errors ?? undefined,
- enabled: tag.enabled,
- isDisabled: tag.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
- rightElement: hasDependentTags ? (
-
- ) : (
- {
- if (isDisablingOrDeletingLastEnabledTag(currentPolicyTag, [tag])) {
- showConfirmModal({
- title: translate('workspace.tags.cannotDeleteOrDisableAllTags.title'),
- prompt: translate('workspace.tags.cannotDeleteOrDisableAllTags.description'),
- confirmText: translate('common.buttonConfirm'),
- shouldShowCancelButton: false,
- });
- return;
- }
- updateWorkspaceTagEnabled(newValue, tag.name);
- }}
- showLockIcon={isDisablingOrDeletingLastEnabledTag(currentPolicyTag, [tag])}
- />
- ),
- })),
- [currentPolicyTag, hasDependentTags, selectedTags, canSelectMultiple, translate, updateWorkspaceTagEnabled, showConfirmModal],
+ Object.values(currentPolicyTag?.tags ?? {}).map((tag) => {
+ let rightElement;
+ if (hasDependentTags && canWriteTags) {
+ rightElement = ;
+ } else if (!hasDependentTags) {
+ rightElement = (
+ {
+ if (isDisablingOrDeletingLastEnabledTag(currentPolicyTag, [tag])) {
+ showConfirmModal({
+ title: translate('workspace.tags.cannotDeleteOrDisableAllTags.title'),
+ prompt: translate('workspace.tags.cannotDeleteOrDisableAllTags.description'),
+ confirmText: translate('common.buttonConfirm'),
+ shouldShowCancelButton: false,
+ });
+ return;
+ }
+ updateWorkspaceTagEnabled(newValue, tag.name);
+ }}
+ showLockIcon={!canWriteTags || isDisablingOrDeletingLastEnabledTag(currentPolicyTag, [tag])}
+ />
+ );
+ }
+
+ return {
+ value: tag.name,
+ text: hasDependentTags ? tag.name : getCleanedTagName(tag.name),
+ keyForList: hasDependentTags ? `${tag.name}-${tag.rules?.parentTagsFilter ?? ''}` : tag.name,
+ isSelected: selectedTags.includes(tag.name) && canSelectMultiple,
+ pendingAction: tag.pendingAction,
+ rules: tag.rules,
+ errors: tag.errors ?? undefined,
+ enabled: tag.enabled,
+ isDisabled: tag.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
+ rightElement,
+ };
+ }),
+ [currentPolicyTag, hasDependentTags, canWriteTags, selectedTags, canSelectMultiple, translate, updateWorkspaceTagEnabled, showConfirmModal, showReadOnlyModal],
);
const filterTag = useCallback((tag: TagListItem, searchInput: string) => {
@@ -209,12 +227,16 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) {
canSelectMultiple={canSelectMultiple}
leftHeaderText={translate('common.name')}
rightHeaderText={hasDependentTags ? undefined : translate('common.enabled')}
- shouldShowRightCaret
+ shouldShowRightCaret={canWriteTags}
/>
);
};
const navigateToTagSettings = (tag: TagListItem) => {
+ if (!canWriteTags) {
+ return;
+ }
+
Navigation.navigate(
isQuickSettingsFlow
? createDynamicRoute(DYNAMIC_ROUTES.SETTINGS_TAG_SETTINGS.getRoute(orderWeight, tag.value))
@@ -236,7 +258,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) {
) : undefined;
const getHeaderButtons = () => {
- if ((!isSmallScreenWidth && selectedTags.length === 0) || (isSmallScreenWidth && !isMobileSelectionModeEnabled)) {
+ if (!canWriteTags || (!isSmallScreenWidth && selectedTags.length === 0) || (isSmallScreenWidth && !isMobileSelectionModeEnabled)) {
return null;
}
@@ -334,11 +356,15 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) {
);
};
- if (!!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled)) {
+ if (canWriteTags && !!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled)) {
setPolicyTagsRequired(policyData, false, orderWeight);
}
const navigateToEditTag = () => {
+ if (!canWriteTags) {
+ return;
+ }
+
Navigation.navigate(
isQuickSettingsFlow
? createDynamicRoute(DYNAMIC_ROUTES.SETTINGS_TAGS_EDIT.getRoute(currentPolicyTag?.orderWeight ?? 0))
@@ -353,6 +379,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) {
policyID={policyID}
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED}
+ policyFeature={CONST.POLICY.POLICY_FEATURE.TAGS}
>
{!shouldDisplayButtonsInSeparateLine && getHeaderButtons()}
- {shouldDisplayButtonsInSeparateLine && {getHeaderButtons()}}
+ {shouldDisplayButtonsInSeparateLine && !!getHeaderButtons() && {getHeaderButtons()}}
{!hasDependentTags && (
{
+ if (!canWriteTags) {
+ showReadOnlyModal();
+ return;
+ }
+
if (!isMultiLevelTags) {
showConfirmModal({
title: translate('workspace.tags.cannotMakeTagListRequired.title'),
@@ -403,8 +435,9 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) {
pendingAction={currentPolicyTag.pendingFields?.required}
errors={currentPolicyTag?.errorFields?.required ?? undefined}
onCloseError={() => clearPolicyTagListErrorField({policyID, tagListIndex: orderWeight, errorField: 'required', policyTags})}
- disabled={!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled)}
- showLockIcon={!isMultiLevelTags || isMakingLastRequiredTagListOptional(policy, policyTags, [currentPolicyTag])}
+ disabled={!canWriteTags || (!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled))}
+ disabledAction={!canWriteTags ? showReadOnlyModal : undefined}
+ showLockIcon={!canWriteTags || !isMultiLevelTags || isMakingLastRequiredTagListOptional(policy, policyTags, [currentPolicyTag])}
/>
)}
@@ -417,8 +450,9 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) {
{isLoading && (
@@ -434,11 +468,11 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) {
ListItem={TableListItem}
selectedItems={selectedTags}
customListHeader={getCustomListHeader()}
- onSelectAll={filteredTagList.length > 0 ? toggleAllTags : undefined}
+ onSelectAll={canWriteTags && filteredTagList.length > 0 ? toggleAllTags : undefined}
onDismissError={(item) => clearPolicyTagErrors({policyID, tagName: item.value, tagListIndex: orderWeight, policyTags})}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
- onTurnOnSelectionMode={(item) => item && toggleTag(item)}
- turnOnSelectionModeOnLongPress={!hasDependentTags}
+ onTurnOnSelectionMode={(item) => item && canWriteTags && toggleTag(item)}
+ turnOnSelectionModeOnLongPress={canWriteTags && !hasDependentTags}
customListHeaderContent={listHeaderContent}
canSelectMultiple={canSelectMultiple}
selectAllAccessibilityLabel={translate('accessibilityHints.selectAllTags')}
@@ -446,7 +480,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) {
shouldShowListEmptyContent={false}
onSelectionButtonPress={toggleTag}
shouldHeaderBeInsideList
- shouldShowRightCaret
+ shouldShowRightCaret={canWriteTags}
showScrollIndicator
/>
)}
diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
index 7c8316821549..f7daed31c66c 100644
--- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
+++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
@@ -22,6 +22,7 @@ import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
+import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSearchBackPress from '@hooks/useSearchBackPress';
import useSearchResults from '@hooks/useSearchResults';
@@ -72,6 +73,7 @@ function WorkspaceTaxesPage({
const [selectedTaxesIDs, setSelectedTaxesIDs] = useState([]);
const {showConfirmModal} = useConfirmModal();
const isMobileSelectionModeEnabled = useMobileSelectionMode();
+ const {canWrite: canWriteTaxes, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.TAXES);
const defaultExternalID = policy?.taxRates?.defaultExternalID;
const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault;
const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy);
@@ -83,7 +85,7 @@ function WorkspaceTaxesPage({
const connectedIntegration = getConnectedIntegration(policy) ?? syncingAccountingIntegration;
const isConnectionVerified = connectedIntegration && !isConnectionUnverified(policy, connectedIntegration);
const currentConnectionName = getCurrentConnectionName(policy);
- const canSelectMultiple = shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true;
+ const canSelectMultiple = canWriteTaxes && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true);
const enabledRatesCount = selectedTaxesIDs.filter((taxID) => !policy?.taxRates?.taxes[taxID]?.isDisabled).length;
const disabledRatesCount = selectedTaxesIDs.length - enabledRatesCount;
@@ -154,9 +156,13 @@ function WorkspaceTaxesPage({
const updateWorkspaceTaxEnabled = useCallback(
(value: boolean, taxID: string) => {
+ if (!canWriteTaxes) {
+ showReadOnlyModal();
+ return;
+ }
setPolicyTaxesEnabled(policy, [taxID], value);
},
- [policy],
+ [canWriteTaxes, policy, showReadOnlyModal],
);
const taxesList = useMemo(() => {
@@ -164,7 +170,7 @@ function WorkspaceTaxesPage({
return [];
}
return Object.entries(policy.taxRates?.taxes ?? {}).map(([key, value]) => {
- const canEditTaxRate = policy && canEditTaxRatePolicyUtils(policy, key);
+ const canEditTaxRate = canWriteTaxes && policy && canEditTaxRatePolicyUtils(policy, key);
return {
text: value.name,
@@ -178,13 +184,15 @@ function WorkspaceTaxesPage({
updateWorkspaceTaxEnabled(newValue, key)}
/>
),
};
});
- }, [policy, textForDefault, translate, updateWorkspaceTaxEnabled]);
+ }, [canWriteTaxes, policy, showReadOnlyModal, textForDefault, translate, updateWorkspaceTaxEnabled]);
const filterTax = useCallback((tax: ListItem, searchInput: string) => {
const results = tokenizedSearch([tax], searchInput, (option) => [option.text ?? '', option.alternateText ?? '']);
@@ -240,7 +248,7 @@ function WorkspaceTaxesPage({
canSelectMultiple={canSelectMultiple}
leftHeaderText={translate('common.name')}
rightHeaderText={translate('common.enabled')}
- shouldShowRightCaret
+ shouldShowRightCaret={canWriteTaxes}
/>
);
};
@@ -269,6 +277,9 @@ function WorkspaceTaxesPage({
if (!taxRate.keyForList) {
return;
}
+ if (!canWriteTaxes) {
+ return;
+ }
if (isSmallScreenWidth && isMobileSelectionModeEnabled) {
toggleTax(taxRate);
return;
@@ -337,7 +348,7 @@ function WorkspaceTaxesPage({
deleteTaxes,
]);
- const shouldShowBulkActionsButton = shouldUseNarrowLayout ? isMobileSelectionModeEnabled : selectedTaxesIDs.length > 0;
+ const shouldShowBulkActionsButton = canWriteTaxes && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : selectedTaxesIDs.length > 0);
const secondaryActions = useMemo(
() => [
@@ -351,42 +362,54 @@ function WorkspaceTaxesPage({
[icons.Gear, policyID, translate],
);
- const headerButtons = !shouldShowBulkActionsButton ? (
-
- {!hasAccountingConnections && (
- Navigation.navigate(ROUTES.WORKSPACE_TAX_CREATE.getRoute(policyID))}
- sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.TAXES.ADD_BUTTON}
- icon={icons.Plus}
- text={translate('workspace.taxes.addRate')}
- style={[shouldDisplayButtonsInSeparateLine && styles.flex1]}
- />
- )}
- {
+ if (!canWriteTaxes) {
+ return null;
+ }
+
+ if (!shouldShowBulkActionsButton) {
+ return (
+
+ {!hasAccountingConnections && (
+ Navigation.navigate(ROUTES.WORKSPACE_TAX_CREATE.getRoute(policyID))}
+ sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.TAXES.ADD_BUTTON}
+ icon={icons.Plus}
+ text={translate('workspace.taxes.addRate')}
+ style={[shouldDisplayButtonsInSeparateLine && styles.flex1]}
+ />
+ )}
+ {}}
+ shouldUseOptionIcon
+ customText={translate('common.more')}
+ sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.TAXES.MORE_DROPDOWN}
+ options={secondaryActions}
+ isSplitButton={false}
+ wrapperStyle={hasAccountingConnections ? styles.flexGrow1 : styles.flexGrow0}
+ />
+
+ );
+ }
+
+ return (
+
onPress={() => {}}
- shouldUseOptionIcon
- customText={translate('common.more')}
- sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.TAXES.MORE_DROPDOWN}
- options={secondaryActions}
+ options={dropdownMenuOptions}
+ buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
+ customText={translate('workspace.common.selected', {count: selectedTaxesIDs.length})}
+ shouldAlwaysShowDropdownMenu
isSplitButton={false}
- wrapperStyle={hasAccountingConnections ? styles.flexGrow1 : styles.flexGrow0}
+ style={[shouldDisplayButtonsInSeparateLine && styles.flexGrow1, shouldDisplayButtonsInSeparateLine && styles.mb3]}
+ isDisabled={!selectedTaxesIDs.length}
+ sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.TAXES.BULK_ACTIONS_DROPDOWN}
/>
-
- ) : (
-
- onPress={() => {}}
- options={dropdownMenuOptions}
- buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
- customText={translate('workspace.common.selected', {count: selectedTaxesIDs.length})}
- shouldAlwaysShowDropdownMenu
- isSplitButton={false}
- style={[shouldDisplayButtonsInSeparateLine && styles.flexGrow1, shouldDisplayButtonsInSeparateLine && styles.mb3]}
- isDisabled={!selectedTaxesIDs.length}
- sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.TAXES.BULK_ACTIONS_DROPDOWN}
- />
- );
+ );
+ };
+
+ const headerButtons = getHeaderButtons();
const selectionModeHeader = isMobileSelectionModeEnabled && shouldUseNarrowLayout;
@@ -420,6 +443,7 @@ function WorkspaceTaxesPage({
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED}
+ policyFeature={CONST.POLICY.POLICY_FEATURE.TAXES}
>
{!shouldDisplayButtonsInSeparateLine && headerButtons}
- {shouldDisplayButtonsInSeparateLine && {headerButtons}}
+ {shouldDisplayButtonsInSeparateLine && !!headerButtons && {headerButtons}}
{isLoading && (
item && toggleTax(item)}
- onSelectAll={filteredTaxesList.length > 0 ? toggleAllTaxes : undefined}
+ onTurnOnSelectionMode={(item) => canWriteTaxes && item && toggleTax(item)}
+ onSelectAll={canWriteTaxes && filteredTaxesList.length > 0 ? toggleAllTaxes : undefined}
onDismissError={(item) => (item.keyForList ? clearTaxRateError(policyID, item.keyForList, item.pendingAction) : undefined)}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
customListHeader={getCustomListHeader()}
@@ -469,9 +493,9 @@ function WorkspaceTaxesPage({
shouldShowListEmptyContent={false}
onSelectionButtonPress={toggleTax}
showScrollIndicator={false}
- turnOnSelectionModeOnLongPress
+ turnOnSelectionModeOnLongPress={canWriteTaxes}
shouldHeaderBeInsideList
- shouldShowRightCaret
+ shouldShowRightCaret={canWriteTaxes}
/>
diff --git a/src/pages/workspace/timeTracking/WorkspaceTimeTrackingDefaultRateSection.tsx b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingDefaultRateSection.tsx
index 011b69899999..92ae24859bc1 100644
--- a/src/pages/workspace/timeTracking/WorkspaceTimeTrackingDefaultRateSection.tsx
+++ b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingDefaultRateSection.tsx
@@ -12,7 +12,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import {policyTimeTrackingSelector} from '@src/selectors/Policy';
-function WorkspaceTimeTrackingDefaultRateSection({policyID}: {policyID: string}) {
+function WorkspaceTimeTrackingDefaultRateSection({policyID, canWriteMoreFeatures}: {policyID: string; canWriteMoreFeatures: boolean}) {
const {convertToDisplayString} = useCurrencyListActions();
const {translate} = useLocalize();
const styles = useThemeStyles();
@@ -33,10 +33,10 @@ function WorkspaceTimeTrackingDefaultRateSection({policyID}: {policyID: string})
Navigation.navigate(ROUTES.WORKSPACE_TIME_TRACKING_DEFAULT_RATE.getRoute(policyID))}
+ onPress={canWriteMoreFeatures ? () => Navigation.navigate(ROUTES.WORKSPACE_TIME_TRACKING_DEFAULT_RATE.getRoute(policyID)) : undefined}
style={styles.sectionMenuItemTopDescription}
/>
diff --git a/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx
index 70a4d4bd44cb..37baab2806d0 100644
--- a/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx
+++ b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import {View} from 'react-native';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
@@ -7,6 +8,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import {canMemberWrite} from '@libs/PolicyUtils';
import type {WorkspaceSplitNavigatorParamList} from '@navigation/types';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
@@ -24,12 +26,15 @@ function WorkspaceTimeTrackingPage({route}: WorkspaceTimeTrackingPageProps) {
const styles = useThemeStyles();
const illustrations = useMemoizedLazyIllustrations(['Clock']);
const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const {login: currentUserLogin = ''} = useCurrentUserPersonalDetails();
+ const canWriteMoreFeatures = canMemberWrite(policy, currentUserLogin, CONST.POLICY.POLICY_FEATURE.MORE_FEATURES);
return (
-
+
diff --git a/src/pages/workspace/travel/BookOrManageYourTrip.tsx b/src/pages/workspace/travel/BookOrManageYourTrip.tsx
index e1d3c2100a54..33cf4b986915 100644
--- a/src/pages/workspace/travel/BookOrManageYourTrip.tsx
+++ b/src/pages/workspace/travel/BookOrManageYourTrip.tsx
@@ -5,6 +5,7 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import usePolicy from '@hooks/usePolicy';
+import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import useThemeStyles from '@hooks/useThemeStyles';
import {setPolicyTravelSettings} from '@libs/actions/Policy/Travel';
import {openTravelDotLink} from '@libs/openTravelDotLink';
@@ -20,6 +21,7 @@ function GetStartedTravel({policyID}: GetStartedTravelProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const policy = usePolicy(policyID);
+ const {canWrite: canWriteMoreFeatures, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.MORE_FEATURES);
const icons = useMemoizedLazyExpensifyIcons(['LuggageWithLines', 'NewWindow']);
const {isBetaEnabled} = usePermissions();
const isTravelInvoicingEnabled = isBetaEnabled(CONST.BETAS.TRAVEL_INVOICING);
@@ -48,15 +50,17 @@ function GetStartedTravel({policyID}: GetStartedTravelProps) {
subtitleMuted
isCentralPane
>
-
+ {canWriteMoreFeatures && (
+
+ )}
diff --git a/src/pages/workspace/travel/GetStartedTravel.tsx b/src/pages/workspace/travel/GetStartedTravel.tsx
index f26c689c5836..2f7fb33bf0a8 100644
--- a/src/pages/workspace/travel/GetStartedTravel.tsx
+++ b/src/pages/workspace/travel/GetStartedTravel.tsx
@@ -9,9 +9,10 @@ import CONST from '@src/CONST';
type GetStartedTravelProps = {
policyID: string;
+ canWriteMoreFeatures: boolean;
};
-function GetStartedTravel({policyID}: GetStartedTravelProps) {
+function GetStartedTravel({policyID, canWriteMoreFeatures}: GetStartedTravelProps) {
const handleCtaPress = () => {};
const {translate} = useLocalize();
@@ -30,13 +31,15 @@ function GetStartedTravel({policyID}: GetStartedTravelProps) {
illustrationContainerStyle={[styles.emptyStateCardIllustrationContainer, styles.justifyContentCenter]}
titleStyles={styles.textHeadlineH1}
footer={
-
+ canWriteMoreFeatures ? (
+
+ ) : undefined
}
/>
);
diff --git a/src/pages/workspace/travel/PolicyTravelPage.tsx b/src/pages/workspace/travel/PolicyTravelPage.tsx
index 399907b90f79..731a2e4e370b 100644
--- a/src/pages/workspace/travel/PolicyTravelPage.tsx
+++ b/src/pages/workspace/travel/PolicyTravelPage.tsx
@@ -12,6 +12,7 @@ import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import usePolicy from '@hooks/usePolicy';
+import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID';
@@ -50,6 +51,7 @@ function WorkspaceTravelPage({
const {login: currentUserLogin} = useCurrentUserPersonalDetails();
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
+ const {canWrite: canWriteMoreFeatures} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.MORE_FEATURES);
const fetchTravelData = useCallback(() => {
openPolicyTravelPage(policyID, workspaceAccountID);
@@ -81,7 +83,12 @@ function WorkspaceTravelPage({
case CONST.TRAVEL.STEPS.REVIEWING_REQUEST:
return ;
default:
- return ;
+ return (
+
+ );
}
})();
@@ -99,6 +106,7 @@ function WorkspaceTravelPage({
policyID={policyID}
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
featureName={CONST.POLICY.MORE_FEATURES.IS_TRAVEL_ENABLED}
+ policyFeature={CONST.POLICY.POLICY_FEATURE.MORE_FEATURES}
>
- {step === CONST.TRAVEL.STEPS.BOOK_OR_MANAGE_YOUR_TRIP && (
+ {step === CONST.TRAVEL.STEPS.BOOK_OR_MANAGE_YOUR_TRIP && canWriteMoreFeatures && (
{}}
diff --git a/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx b/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx
index f8ce87367218..d4ad173793ca 100644
--- a/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx
+++ b/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx
@@ -13,6 +13,7 @@ import {useCurrencyListActions} from '@hooks/useCurrencyList';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
+import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID';
import {
@@ -82,6 +83,7 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec
const [cardOnWaitlist] = useOnyx(`${ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST}${policyID}`);
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
+ const {canWrite: canWriteMoreFeatures, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.MORE_FEATURES);
const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);
@@ -283,6 +285,16 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec
return ;
};
+ const getToggleDisabledAction = () => {
+ if (!canWriteMoreFeatures) {
+ return showReadOnlyModal;
+ }
+ if (isOnWaitlist) {
+ return () => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL_SETTINGS_ACCOUNT.getRoute(policyID));
+ }
+ return undefined;
+ };
+
const travelInvoicingSubMenuItems = (
<>
{hasTravelProvisioningErrors && (
@@ -312,7 +324,7 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec
)}
- {shouldShowPayButton && (
+ {shouldShowPayButton && canWriteMoreFeatures && (
Navigation.navigate(ROUTES.WORKSPACE_TRAVEL_SETTINGS_ACCOUNT.getRoute(policyID))}
+ onPress={canWriteMoreFeatures ? () => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL_SETTINGS_ACCOUNT.getRoute(policyID)) : undefined}
wrapperStyle={[styles.sectionMenuItemTopDescription]}
titleStyle={settlementAccountNumber ? styles.textNormalThemeText : styles.colorMuted}
descriptionTextStyle={styles.textLabelSupportingNormal}
- shouldShowRightIcon
+ shouldShowRightIcon={canWriteMoreFeatures}
brickRoadIndicator={hasSettlementAccountError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
/>
@@ -357,11 +369,11 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec
Navigation.navigate(ROUTES.WORKSPACE_TRAVEL_SETTINGS_FREQUENCY.getRoute(policyID))}
+ onPress={canWriteMoreFeatures ? () => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL_SETTINGS_FREQUENCY.getRoute(policyID)) : undefined}
wrapperStyle={[styles.sectionMenuItemTopDescription]}
titleStyle={styles.textNormalThemeText}
descriptionTextStyle={styles.textLabelSupportingNormal}
- shouldShowRightIcon
+ shouldShowRightIcon={canWriteMoreFeatures}
brickRoadIndicator={hasSettlementFrequencyError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
/>
@@ -375,11 +387,11 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec
Navigation.navigate(ROUTES.WORKSPACE_TRAVEL_SETTINGS_MONTHLY_LIMIT.getRoute(policyID))}
+ onPress={canWriteMoreFeatures ? () => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL_SETTINGS_MONTHLY_LIMIT.getRoute(policyID)) : undefined}
wrapperStyle={[styles.sectionMenuItemTopDescription]}
titleStyle={styles.textNormalThemeText}
descriptionTextStyle={styles.textLabelSupportingNormal}
- shouldShowRightIcon
+ shouldShowRightIcon={canWriteMoreFeatures}
brickRoadIndicator={hasMonthlyLimitError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
/>
@@ -396,8 +408,9 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec
switchAccessibilityLabel={translate('workspace.moreFeatures.travel.travelInvoicing.travelInvoicingSection.subtitle')}
onToggle={handleToggle}
isActive={isTravelInvoicingEnabled}
- disabled={isLoading || isOnWaitlist}
- disabledAction={isOnWaitlist ? () => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL_SETTINGS_ACCOUNT.getRoute(policyID)) : undefined}
+ disabled={!canWriteMoreFeatures || isLoading || isOnWaitlist}
+ disabledAction={getToggleDisabledAction()}
+ showLockIcon={!canWriteMoreFeatures || isOnWaitlist}
pendingAction={togglePendingAction}
errors={toggleErrors}
onCloseError={() => clearTravelInvoicingErrors(workspaceAccountID)}
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
index 4e13e60f3a2d..9ead957d44e7 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
@@ -28,6 +28,7 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
+import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSearchResults from '@hooks/useSearchResults';
import useTheme from '@hooks/useTheme';
@@ -52,7 +53,7 @@ import {getPaymentMethodDescription} from '@libs/PaymentUtils';
import {getDisplayNameOrDefault, getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
import {
canAccessSubmitWorkspaceFeatures,
- canEditWorkspaceSettings,
+ canMemberRead,
getConnectedHRProvider,
getCorrectedAutoReportingFrequency,
hasDynamicExternalWorkflow,
@@ -181,6 +182,10 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
const {isOffline} = useNetwork({onReconnect: fetchData});
const isPolicyAdmin = isPolicyAdminUtil(policy);
+ const canReadWorkflows = canMemberRead(policy, currentUserEmail, CONST.POLICY.POLICY_FEATURE.WORKFLOWS);
+ const {canWrite: canWriteWorkflows, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.WORKFLOWS);
+ const {canWrite: canWriteApprovals} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.WORKFLOWS_APPROVALS);
+ const {canWrite: canWritePayments} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.WORKFLOWS_PAYMENTS);
const {isAccountLocked} = useLockedAccountState();
const {showLockedAccountModal} = useLockedAccountActions();
@@ -362,17 +367,27 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
title: translate('workflowsPage.submissionFrequency'),
subtitle: translate('workflowsPage.submissionFrequencyDescription'),
switchAccessibilityLabel: translate('workflowsPage.submissionFrequencyDescription'),
- onToggle: (isEnabled: boolean) => (policy ? setWorkspaceAutoHarvesting(policy, isEnabled) : undefined),
+ onToggle: (isEnabled: boolean) => {
+ if (!canWriteWorkflows) {
+ showReadOnlyModal();
+ return;
+ }
+ if (!policy) {
+ return;
+ }
+ setWorkspaceAutoHarvesting(policy, isEnabled);
+ },
subMenuItems: (
@@ -381,12 +396,19 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
pendingAction: policy?.pendingFields?.autoReporting ?? policy?.pendingFields?.autoReportingFrequency,
errors: getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING),
onCloseError: () => clearPolicyErrorField(route.params.policyID, CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING),
+ disabled: !canWriteWorkflows,
+ disabledAction: showReadOnlyModal,
+ showLockIcon: !canWriteWorkflows,
},
{
title: translate('workflowsPage.addApprovalsTitle'),
subtitle: approvalOptionSubtitle,
switchAccessibilityLabel: isSmartLimitEnabled ? translate('workspace.moreFeatures.workflows.disableApprovalPrompt') : translate('workflowsPage.addApprovalsDescription'),
onToggle: (isEnabled: boolean) => {
+ if (!canWriteApprovals) {
+ showReadOnlyModal();
+ return;
+ }
if (isHRConnected) {
return;
}
@@ -452,16 +474,16 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, workflow.approvers.at(0)?.email ?? ''))
}
currency={policy?.outputCurrency}
- isDisabled={shouldBlockApprovalWorkflowEditing}
+ isDisabled={shouldBlockApprovalWorkflowEditing || !canWriteApprovals}
/>
))}
- {!shouldBlockApprovalWorkflowEditing && (
+ {!shouldBlockApprovalWorkflowEditing && canWriteApprovals && (
),
- disabled: isSmartLimitEnabled || isDEWEnabled || isHRConnected || canAccessSubmit2026Features,
- disabledAction: getAddApprovalsToggleDisabledAction(),
+ disabled: !canWriteApprovals || isSmartLimitEnabled || isDEWEnabled || isHRConnected || canAccessSubmit2026Features,
+ disabledAction: canWriteApprovals ? getAddApprovalsToggleDisabledAction() : showReadOnlyModal,
+ showLockIcon: !canWriteApprovals,
isActive:
isHRConnected ||
isDEWEnabled ||
@@ -490,6 +513,10 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
subtitle: translate('workflowsPage.makeOrTrackPaymentsDescription'),
switchAccessibilityLabel: translate('workflowsPage.makeOrTrackPaymentsDescription'),
onToggle: (isEnabled: boolean) => {
+ if (!canWritePayments) {
+ showReadOnlyModal();
+ return;
+ }
if (isEnabled && canAccessSubmit2026Features) {
Navigation.navigate(
ROUTES.WORKSPACE_UPGRADE.getRoute(
@@ -533,29 +560,33 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
{
- if (isAccountLocked) {
- showLockedAccountModal();
- return;
- }
- // User who is reimburser can initiate unlocking process
- if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && isUserReimburser) {
- pressLockedBankAccount(bankAccountID, translate, conciergeReportID ?? undefined, delegateAccountID);
- navigateToConciergeChat(conciergeReportID ?? undefined, introSelected, currentUserAccountID, isSelfTourViewed, betas);
- return;
- }
-
- // User who is not reimburser can't initiate unlocking process but can connect new account
- if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && !isUserReimburser) {
- // If user has existing accounts and no bank account setup in progress we should show screen to choose an existing account
- if (hasValidExistingAccounts && !shouldShowContinueModal) {
- Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(route.params.policyID));
- return;
- }
- }
-
- navigateToBankAccountRoute({policyID: route.params.policyID, backTo: ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID)});
- }}
+ onPress={
+ canWritePayments
+ ? () => {
+ if (isAccountLocked) {
+ showLockedAccountModal();
+ return;
+ }
+ // User who is reimburser can initiate unlocking process
+ if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && isUserReimburser) {
+ pressLockedBankAccount(bankAccountID, translate, conciergeReportID ?? undefined, delegateAccountID);
+ navigateToConciergeChat(conciergeReportID ?? undefined, introSelected, currentUserAccountID, isSelfTourViewed, betas);
+ return;
+ }
+
+ // User who is not reimburser can't initiate unlocking process but can connect new account
+ if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && !isUserReimburser) {
+ // If user has existing accounts and no bank account setup in progress we should show screen to choose an existing account
+ if (hasValidExistingAccounts && !shouldShowContinueModal) {
+ Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(route.params.policyID));
+ return;
+ }
+ }
+
+ navigateToBankAccountRoute({policyID: route.params.policyID, backTo: ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID)});
+ }
+ : undefined
+ }
displayInDefaultIconColor
icon={bankIcon.icon}
iconHeight={bankIcon.iconHeight ?? bankIcon.iconSize}
@@ -567,57 +598,60 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
badgeIcon={isAccountInSetupState || (isBusinessBankAccountLocked && isPolicyAdmin) ? expensifyIcons.DotIndicator : undefined}
isBadgeSuccess={isAccountInSetupState}
isBadgeError={isBusinessBankAccountLocked && isPolicyAdmin}
- shouldShowRightIcon
+ shouldShowRightIcon={canWritePayments}
+ interactive={canWritePayments}
shouldGreyOutWhenDisabled={!policy?.pendingFields?.reimbursementChoice}
wrapperStyle={[styles.sectionMenuItemTopDescription, styles.mt3, styles.mbn3]}
brickRoadIndicator={hasReimburserError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
/>
>
) : (
- {
- if (isAccountLocked) {
- showLockedAccountModal();
- return;
- }
- if (!isCurrencySupportedForGlobalReimbursement((policy?.outputCurrency ?? '') as CurrencyType)) {
- showConfirmModal({
- title: translate('workspace.bankAccount.workspaceCurrencyNotSupported'),
- prompt: updateWorkspaceCurrencyPrompt,
- confirmText: translate('workspace.bankAccount.updateWorkspaceCurrency'),
- cancelText: translate('common.cancel'),
- }).then((result) => {
- if (result.action !== ModalActions.CONFIRM) {
- return;
- }
- confirmCurrencyChangeAndHideModal();
- });
-
- return;
- }
- if (!shouldShowBankAccount && hasValidExistingAccounts && !shouldShowContinueModal) {
- Navigation.navigate(
- ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(
- route.params.policyID,
- ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID),
- ),
- );
- return;
- }
- navigateToBankAccountRoute({policyID: route.params.policyID, backTo: ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID)});
- }}
- icon={expensifyIcons.Plus}
- iconHeight={20}
- iconWidth={20}
- shouldShowRightIcon
- disabled={isOffline || !isPolicyAdmin}
- shouldGreyOutWhenDisabled={!policy?.pendingFields?.reimbursementChoice}
- sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.WORKFLOWS.ADD_BANK_ACCOUNT}
- wrapperStyle={[styles.sectionMenuItemTopDescription, styles.mt3, styles.mbn3]}
- brickRoadIndicator={hasReimburserError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
- />
+ canWritePayments && (
+ {
+ if (isAccountLocked) {
+ showLockedAccountModal();
+ return;
+ }
+ if (!isCurrencySupportedForGlobalReimbursement((policy?.outputCurrency ?? '') as CurrencyType)) {
+ showConfirmModal({
+ title: translate('workspace.bankAccount.workspaceCurrencyNotSupported'),
+ prompt: updateWorkspaceCurrencyPrompt,
+ confirmText: translate('workspace.bankAccount.updateWorkspaceCurrency'),
+ cancelText: translate('common.cancel'),
+ }).then((result) => {
+ if (result.action !== ModalActions.CONFIRM) {
+ return;
+ }
+ confirmCurrencyChangeAndHideModal();
+ });
+
+ return;
+ }
+ if (!shouldShowBankAccount && hasValidExistingAccounts && !shouldShowContinueModal) {
+ Navigation.navigate(
+ ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(
+ route.params.policyID,
+ ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID),
+ ),
+ );
+ return;
+ }
+ navigateToBankAccountRoute({policyID: route.params.policyID, backTo: ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID)});
+ }}
+ icon={expensifyIcons.Plus}
+ iconHeight={20}
+ iconWidth={20}
+ shouldShowRightIcon
+ disabled={isOffline || !isPolicyAdmin}
+ shouldGreyOutWhenDisabled={!policy?.pendingFields?.reimbursementChoice}
+ sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.WORKFLOWS.ADD_BANK_ACCOUNT}
+ wrapperStyle={[styles.sectionMenuItemTopDescription, styles.mt3, styles.mbn3]}
+ brickRoadIndicator={hasReimburserError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ />
+ )
)}
{shouldShowBankAccount && (
Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_PAYER.getRoute(route.params.policyID))}
+ onPress={canWritePayments ? () => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_PAYER.getRoute(route.params.policyID)) : undefined}
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.WORKFLOWS.AUTHORIZED_PAYER}
- shouldShowRightIcon
+ shouldShowRightIcon={canWritePayments}
+ interactive={canWritePayments}
wrapperStyle={[styles.sectionMenuItemTopDescription, styles.mt3, styles.mbn3]}
brickRoadIndicator={hasReimburserError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
/>
@@ -647,6 +682,9 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
pendingAction: policy?.pendingFields?.reimbursementChoice,
errors: getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.REIMBURSEMENT_CHOICE),
onCloseError: () => clearPolicyErrorField(route.params.policyID, CONST.POLICY.COLLECTION_KEYS.REIMBURSEMENT_CHOICE),
+ disabled: !canWritePayments,
+ disabledAction: showReadOnlyModal,
+ showLockIcon: !canWritePayments,
},
];
}, [
@@ -696,6 +734,10 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
confirmCurrencyChangeAndHideModal,
delegateAccountID,
canAccessSubmit2026Features,
+ canWriteApprovals,
+ canWritePayments,
+ canWriteWorkflows,
+ showReadOnlyModal,
]);
const renderOptionItem = (item: ToggleSettingOptionRowProps, index: number) => (
@@ -719,6 +761,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
onCloseError={item.onCloseError}
disabled={item.disabled}
disabledAction={item.disabledAction}
+ showLockIcon={item.showLockIcon}
/>
);
@@ -730,13 +773,15 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
{optionItems.map(renderOptionItem)}
-
+
diff --git a/tests/ui/WorkspaceMoreFeaturesPageTest.tsx b/tests/ui/WorkspaceMoreFeaturesPageTest.tsx
index 87767b1b349c..544c7ee63183 100644
--- a/tests/ui/WorkspaceMoreFeaturesPageTest.tsx
+++ b/tests/ui/WorkspaceMoreFeaturesPageTest.tsx
@@ -96,23 +96,34 @@ const renderPage = (initialParams: WorkspaceSplitNavigatorParamList[typeof SCREE
,
);
+const TEST_USER_LOGIN = 'test@user.com';
+
/** Build a minimal admin policy for use as Onyx fixture. Lock states are driven by mocked hooks/utils, not policy fields. */
-const buildPolicy = (overrides: Partial> = {}) => ({
- ...LHNTestUtils.getFakePolicy(),
- role: CONST.POLICY.ROLE.ADMIN,
- type: CONST.POLICY.TYPE.CORPORATE,
- areWorkflowsEnabled: true,
- areConnectionsEnabled: false,
- areCategoriesEnabled: true,
- areTagsEnabled: false,
- areReportFieldsEnabled: false,
- areExpensifyCardsEnabled: false,
- areCompanyCardsEnabled: false,
- areDistanceRatesEnabled: false,
- areRulesEnabled: false,
- isTravelEnabled: false,
- ...overrides,
-});
+const buildPolicy = (overrides: Partial> = {}) => {
+ const role = overrides.role ?? CONST.POLICY.ROLE.ADMIN;
+
+ return {
+ ...LHNTestUtils.getFakePolicy(),
+ role,
+ type: CONST.POLICY.TYPE.CORPORATE,
+ employeeList: {
+ [TEST_USER_LOGIN]: {
+ role,
+ },
+ },
+ areWorkflowsEnabled: true,
+ areConnectionsEnabled: false,
+ areCategoriesEnabled: true,
+ areTagsEnabled: false,
+ areReportFieldsEnabled: false,
+ areExpensifyCardsEnabled: false,
+ areCompanyCardsEnabled: false,
+ areDistanceRatesEnabled: false,
+ areRulesEnabled: false,
+ isTravelEnabled: false,
+ ...overrides,
+ };
+};
const isSmartLimitEnabledMock = jest.mocked(CardUtils.isSmartLimitEnabled);
const getCompanyFeedsMock = jest.mocked(CardUtils.getCompanyFeeds);