diff --git a/src/CONST.ts b/src/CONST.ts
index 143034afb2e9..cf550923d637 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -3897,6 +3897,7 @@ const CONST = {
SUBMITTER: 'submitter',
ALL: 'all',
},
+ DELEGATE_ROLE_HELPDOT_ARTICLE_LINK: 'https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/',
STRIPE_GBP_AUTH_STATUSES: {
SUCCEEDED: 'succeeded',
CARD_AUTHENTICATION_REQUIRED: 'authentication_required',
diff --git a/src/components/DelegateNoAccessModal.tsx b/src/components/DelegateNoAccessModal.tsx
new file mode 100644
index 000000000000..8b708459c122
--- /dev/null
+++ b/src/components/DelegateNoAccessModal.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import useLocalize from '@hooks/useLocalize';
+import CONST from '@src/CONST';
+import ConfirmModal from './ConfirmModal';
+import Text from './Text';
+import TextLink from './TextLink';
+
+type DelegateNoAccessModalProps = {
+ isNoDelegateAccessMenuVisible: boolean;
+ onClose: () => void;
+ delegatorEmail: string;
+};
+
+export default function DelegateNoAccessModal({isNoDelegateAccessMenuVisible = false, onClose, delegatorEmail = ''}: DelegateNoAccessModalProps) {
+ const {translate} = useLocalize();
+ const noDelegateAccessPromptStart = translate('delegate.notAllowedMessageStart', {accountOwnerEmail: delegatorEmail});
+ const noDelegateAccessHyperLinked = translate('delegate.notAllowedMessageHyperLinked');
+ const noDelegateAccessPromptEnd = translate('delegate.notAllowedMessageEnd');
+
+ const delegateNoAccessPrompt = (
+
+ {noDelegateAccessPromptStart}
+ {noDelegateAccessHyperLinked}
+ {noDelegateAccessPromptEnd}
+
+ );
+
+ return (
+
+ );
+}
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 321482f7177c..3cfa862faf14 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -17,6 +17,7 @@ import variables from '@styles/variables';
import * as IOU from '@userActions/IOU';
import * as TransactionActions from '@userActions/Transaction';
import CONST from '@src/CONST';
+import useDelegateUserDetails from '@src/hooks/useDelegateUserDetails';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
@@ -26,6 +27,7 @@ import type IconAsset from '@src/types/utils/IconAsset';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
+import DelegateNoAccessModal from './DelegateNoAccessModal';
import HeaderWithBackButton from './HeaderWithBackButton';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
@@ -142,6 +144,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const isAnyTransactionOnHold = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID);
const displayedAmount = isAnyTransactionOnHold && canAllowSettlement ? nonHeldAmount : formattedAmount;
const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout);
+ const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails();
+ const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
const confirmPayment = (type?: PaymentMethodType | undefined, payAsBusiness?: boolean) => {
if (!type || !chatReport) {
@@ -149,7 +153,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
}
setPaymentType(type);
setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY);
- if (isAnyTransactionOnHold) {
+ if (isDelegateAccessRestricted) {
+ setIsNoDelegateAccessMenuVisible(true);
+ } else if (isAnyTransactionOnHold) {
setIsHoldMenuVisible(true);
} else if (ReportUtils.isInvoiceReport(moneyRequestReport)) {
IOU.payInvoice(type, chatReport, moneyRequestReport, payAsBusiness);
@@ -160,7 +166,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const confirmApproval = () => {
setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE);
- if (isAnyTransactionOnHold) {
+ if (isDelegateAccessRestricted) {
+ setIsNoDelegateAccessMenuVisible(true);
+ } else if (isAnyTransactionOnHold) {
setIsHoldMenuVisible(true);
} else {
IOU.approveMoneyRequest(moneyRequestReport, true);
@@ -402,6 +410,12 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
transactionCount={transactionIDs.length}
/>
)}
+ setIsNoDelegateAccessMenuVisible(false)}
+ delegatorEmail={delegatorEmail ?? ''}
+ />
+
PromotedAction> & {
message: (params: {reportID?: string; accountID?: number; login?: string}) => PromotedAction;
} & {
- hold: (params: {isTextHold: boolean; reportAction: ReportAction | undefined; reportID?: string}) => PromotedAction;
+ hold: (params: {
+ isTextHold: boolean;
+ reportAction: ReportAction | undefined;
+ reportID?: string;
+ isDelegateAccessRestricted: boolean;
+ setIsNoDelegateAccessMenuVisible: (isVisible: boolean) => void;
+ }) => PromotedAction;
};
const PromotedActions = {
@@ -70,11 +76,16 @@ const PromotedActions = {
}
},
}),
- hold: ({isTextHold, reportAction, reportID}) => ({
+ hold: ({isTextHold, reportAction, reportID, isDelegateAccessRestricted, setIsNoDelegateAccessMenuVisible}) => ({
key: CONST.PROMOTED_ACTIONS.HOLD,
icon: Expensicons.Stopwatch,
text: Localize.translateLocal(`iou.${isTextHold ? 'hold' : 'unhold'}`),
onSelected: () => {
+ if (isDelegateAccessRestricted) {
+ setIsNoDelegateAccessMenuVisible(true); // Show the menu
+ return;
+ }
+
if (!isTextHold) {
Navigation.goBack();
}
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index ad29a8d84141..9bb6709aa21c 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -5,6 +5,7 @@ import {View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {useOnyx, withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
+import DelegateNoAccessModal from '@components/DelegateNoAccessModal';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
@@ -14,6 +15,7 @@ import type {ActionHandledType} from '@components/ProcessMoneyReportHoldMenu';
import SettlementButton from '@components/SettlementButton';
import {showContextMenuForReport} from '@components/ShowContextMenuContext';
import Text from '@components/Text';
+import useDelegateUserDetails from '@hooks/useDelegateUserDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useTheme from '@hooks/useTheme';
@@ -191,13 +193,18 @@ function ReportPreview({
[chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled],
);
+ const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails();
+ const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
+
const confirmPayment = (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => {
if (!type) {
return;
}
setPaymentType(type);
setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY);
- if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
+ if (isDelegateAccessRestricted) {
+ setIsNoDelegateAccessMenuVisible(true);
+ } else if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
setIsHoldMenuVisible(true);
} else if (chatReport && iouReport) {
if (ReportUtils.isInvoiceReport(iouReport)) {
@@ -210,7 +217,9 @@ function ReportPreview({
const confirmApproval = () => {
setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE);
- if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
+ if (isDelegateAccessRestricted) {
+ setIsNoDelegateAccessMenuVisible(true);
+ } else if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
setIsHoldMenuVisible(true);
} else {
IOU.approveMoneyRequest(iouReport, true);
@@ -516,6 +525,12 @@ function ReportPreview({
+ setIsNoDelegateAccessMenuVisible(false)}
+ delegatorEmail={delegatorEmail ?? ''}
+ />
+
{isHoldMenuVisible && iouReport && requestType !== undefined && (
`You don't have permission to take this action for ${accountOwnerEmail} as a`,
+ notAllowedMessageHyperLinked: ' limited access',
+ notAllowedMessageEnd: ' copilot',
},
} satisfies TranslationBase;
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 24b44c72c483..53c13e94d3d0 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -3,6 +3,7 @@ import CONST from '@src/CONST';
import type {DelegateRole} from '@src/types/onyx/Account';
import type {ConnectionName, PolicyConnectionSyncStage, SageIntacctMappingName} from '@src/types/onyx/Policy';
import type {
+ AccountOwnerParams,
AddressLineParams,
AdminCanceledRequestParams,
AlreadySignedInParams,
@@ -5089,5 +5090,9 @@ export default {
}
},
genericError: '¡Ups! Ha ocurrido un error. Por favor, inténtalo de nuevo.',
+ notAllowed: 'No tan rápido...',
+ notAllowedMessageStart: ({accountOwnerEmail}: AccountOwnerParams) => `No tienes permiso para realizar esta acción para ${accountOwnerEmail}`,
+ notAllowedMessageHyperLinked: ' copiloto con acceso',
+ notAllowedMessageEnd: ' limitado',
},
} satisfies EnglishTranslation;
diff --git a/src/languages/types.ts b/src/languages/types.ts
index fb396a3f64ea..d061a7c24ea2 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -316,6 +316,8 @@ type ChangeTypeParams = {oldType: string; newType: string};
type DelegateSubmitParams = {delegateUser: string; originalManager: string};
+type AccountOwnerParams = {accountOwnerEmail: string};
+
type ExportedToIntegrationParams = {label: string; markedManually?: boolean; inProgress?: boolean; lastModified?: string};
type IntegrationsMessageParams = {
@@ -475,6 +477,7 @@ export type {
ChangeTypeParams,
ExportedToIntegrationParams,
DelegateSubmitParams,
+ AccountOwnerParams,
IntegrationsMessageParams,
MarkedReimbursedParams,
MarkReimbursedFromIntegrationParams,
diff --git a/src/libs/AccountUtils.ts b/src/libs/AccountUtils.ts
index b926e20ca59c..b5b58bfbf70c 100644
--- a/src/libs/AccountUtils.ts
+++ b/src/libs/AccountUtils.ts
@@ -8,4 +8,11 @@ const isValidateCodeFormSubmitting = (account: OnyxEntry) =>
/** Whether the accound ID is an odd number, useful for A/B testing. */
const isAccountIDOddNumber = (accountID: number) => accountID % 2 === 1;
-export default {isValidateCodeFormSubmitting, isAccountIDOddNumber};
+function isDelegateOnlySubmitter(account: OnyxEntry): boolean {
+ const delegateEmail = account?.delegatedAccess?.delegate;
+ const delegateRole = account?.delegatedAccess?.delegates?.find((delegate) => delegate.email === delegateEmail)?.role;
+
+ return delegateRole === CONST.DELEGATE_ROLE.SUBMITTER;
+}
+
+export default {isValidateCodeFormSubmitting, isAccountIDOddNumber, isDelegateOnlySubmitter};
diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index de93ed7a3ced..aa419faedc29 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -7,6 +7,7 @@ import {useOnyx, withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import AvatarWithImagePicker from '@components/AvatarWithImagePicker';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import DelegateNoAccessModal from '@components/DelegateNoAccessModal';
import DisplayNames from '@components/DisplayNames';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -22,6 +23,7 @@ import RoomHeaderAvatars from '@components/RoomHeaderAvatars';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
+import useDelegateUserDetails from '@hooks/useDelegateUserDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
@@ -234,15 +236,19 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
const [moneyRequestReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`);
const isMoneyRequestExported = ReportUtils.isExported(moneyRequestReportActions);
+ const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails();
+ const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
const unapproveExpenseReportOrShowModal = useCallback(() => {
- if (isMoneyRequestExported) {
+ if (isDelegateAccessRestricted) {
+ setIsNoDelegateAccessMenuVisible(true);
+ } else if (isMoneyRequestExported) {
setIsUnapproveModalVisible(true);
return;
}
Navigation.dismissModal();
IOU.unapproveExpenseReport(moneyRequestReport);
- }, [isMoneyRequestExported, moneyRequestReport]);
+ }, [isMoneyRequestExported, moneyRequestReport, isDelegateAccessRestricted]);
const shouldShowLeaveButton = ReportUtils.canLeaveChat(report, policy);
@@ -552,6 +558,8 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
isTextHold: canHoldUnholdReportAction.canHoldRequest,
reportAction: moneyRequestAction,
reportID: transactionThreadReportID ? report.reportID : moneyRequestAction?.childReportID ?? '-1',
+ isDelegateAccessRestricted,
+ setIsNoDelegateAccessMenuVisible,
}),
);
}
@@ -563,7 +571,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
result.push(PromotedActions.share(report));
return result;
- }, [report, moneyRequestAction, canJoin, isExpenseReport, shouldShowHoldAction, canHoldUnholdReportAction.canHoldRequest, transactionThreadReportID]);
+ }, [report, moneyRequestAction, canJoin, isExpenseReport, shouldShowHoldAction, canHoldUnholdReportAction.canHoldRequest, transactionThreadReportID, isDelegateAccessRestricted]);
const nameSectionExpenseIOU = (
@@ -813,6 +821,11 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
danger
shouldEnableNewFocusManagement
/>
+ setIsNoDelegateAccessMenuVisible(false)}
+ delegatorEmail={delegatorEmail ?? ''}
+ />