From 1f35b21573967fcf93e705e0489da6c803c5daa4 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Fri, 1 Aug 2025 00:27:56 +0500 Subject: [PATCH 1/5] Add isReportArchived param and pass the argument where getMoneyRequestOptions is called --- src/libs/ReportUtils.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d5a6faada348..abf7ddd6bfa3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8670,16 +8670,12 @@ function isGroupChatAdmin(report: OnyxEntry, accountID: number) { * None of the options should show in chat threads or if there is some special Expensify account * as a participant of the report. */ -function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[], filterDeprecatedTypes = false): IOUType[] { +function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[], filterDeprecatedTypes = false, isReportArchived = false): IOUType[] { const teacherUnitePolicyID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; const isTeachersUniteReport = report?.policyID === teacherUnitePolicyID; - // This will get removed as part of https://github.com/Expensify/App/issues/59961 - // eslint-disable-next-line deprecation/deprecation - const reportNameValuePairs = getReportNameValuePairs(report?.reportID); - // In any thread, task report or trip room, we do not allow any new expenses - if (isChatThread(report) || isTaskReport(report) || isInvoiceReport(report) || isSystemChat(report) || isArchivedReport(reportNameValuePairs) || isTripRoom(report)) { + if (isChatThread(report) || isTaskReport(report) || isInvoiceReport(report) || isSystemChat(report) || isReportArchived || isTripRoom(report)) { return []; } @@ -8766,8 +8762,9 @@ function temporary_getMoneyRequestOptions( report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[], + isReportArchived = false, ): Array> { - return getMoneyRequestOptions(report, policy, reportParticipants, true) as Array< + return getMoneyRequestOptions(report, policy, reportParticipants, true, isReportArchived) as Array< Exclude >; } @@ -8982,14 +8979,14 @@ function getReportOfflinePendingActionAndErrors(report: OnyxEntry): Repo /** * Check if the report can create the expense with type is iouType */ -function canCreateRequest(report: OnyxEntry, policy: OnyxEntry, iouType: ValueOf): boolean { +function canCreateRequest(report: OnyxEntry, policy: OnyxEntry, iouType: ValueOf, isReportArchived = false): boolean { const participantAccountIDs = Object.keys(report?.participants ?? {}).map(Number); if (!canUserPerformWriteAction(report)) { return false; } - const requestOptions = getMoneyRequestOptions(report, policy, participantAccountIDs); + const requestOptions = getMoneyRequestOptions(report, policy, participantAccountIDs, isReportArchived); requestOptions.push(CONST.IOU.TYPE.CREATE); return requestOptions.includes(iouType); From 336a265ad851018818919fa673b84dba96d4fcc7 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Fri, 1 Aug 2025 00:34:36 +0500 Subject: [PATCH 2/5] refactor: AccessOrNotFoundWrapper to pass isReportArchived to canCreateRequest --- src/libs/QuickActionUtils.ts | 4 ++-- .../workspace/AccessOrNotFoundWrapper.tsx | 21 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/libs/QuickActionUtils.ts b/src/libs/QuickActionUtils.ts index 12a44431a664..4f699037ac24 100644 --- a/src/libs/QuickActionUtils.ts +++ b/src/libs/QuickActionUtils.ts @@ -92,10 +92,10 @@ const getQuickActionTitle = (action: QuickActionName): TranslationPaths => { } }; -const isQuickActionAllowed = (quickAction: QuickAction, quickActionReport: Report | undefined, quickActionPolicy: Policy | undefined) => { +const isQuickActionAllowed = (quickAction: QuickAction, quickActionReport: Report | undefined, quickActionPolicy: Policy | undefined, isReportArchived = false) => { const iouType = getIOUType(quickAction?.action); if (iouType) { - return canCreateRequest(quickActionReport, quickActionPolicy, iouType); + return canCreateRequest(quickActionReport, quickActionPolicy, iouType, isReportArchived); } if (quickAction?.action === CONST.QUICK_ACTIONS.PER_DIEM) { return !!quickActionPolicy?.arePerDiemRatesEnabled; diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index eaeeb54f9bbb..ec084674ec1a 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -7,6 +7,7 @@ import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {openWorkspace} from '@libs/actions/Policy/Policy'; import {isValidMoneyRequestType} from '@libs/IOUUtils'; @@ -31,13 +32,24 @@ const ACCESS_VARIANTS = { [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry) => isPaidGroupPolicy(policy), [CONST.POLICY.ACCESS_VARIANTS.CONTROL]: (policy: OnyxEntry) => isControlPolicy(policy), [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry, login: string) => isPolicyAdmin(policy, login), - [CONST.IOU.ACCESS_VARIANTS.CREATE]: (policy: OnyxEntry, login: string, report: OnyxEntry, allPolicies: NonNullable> | null, iouType?: IOUType) => + [CONST.IOU.ACCESS_VARIANTS.CREATE]: ( + policy: OnyxEntry, + login: string, + report: OnyxEntry, + allPolicies: NonNullable> | null, + iouType?: IOUType, + isReportArchived?: boolean, + ) => !!iouType && isValidMoneyRequestType(iouType) && // Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the expense - (isEmptyObject(report?.reportID) || canCreateRequest(report, policy, iouType)) && + + (isEmptyObject(report?.reportID) || canCreateRequest(report, policy, iouType, isReportArchived)) && (iouType !== CONST.IOU.TYPE.INVOICE || canSendInvoice(allPolicies, login)), -} as const satisfies Record> | null, iouType?: IOUType) => boolean>; +} as const satisfies Record< + string, + (policy: Policy, login: string, report: Report, allPolicies: NonNullable> | null, iouType?: IOUType, isArchivedReport?: boolean) => boolean +>; type AccessVariant = keyof typeof ACCESS_VARIANTS; type AccessOrNotFoundWrapperChildrenProps = { @@ -155,9 +167,10 @@ function AccessOrNotFoundWrapper({ const {isOffline} = useNetwork(); + const isReportArchived = useReportIsArchived(report?.reportID); const isPageAccessible = accessVariants.reduce((acc, variant) => { const accessFunction = ACCESS_VARIANTS[variant]; - return acc && accessFunction(policy, login, report, allPolicies ?? null, iouType); + return acc && accessFunction(policy, login, report, allPolicies ?? null, iouType, isReportArchived); }, true); const isPolicyNotAccessible = !isPolicyAccessible(policy); From e9bad078325f1b2e1cb608cbe6965feabb4a4afb Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Fri, 1 Aug 2025 01:28:41 +0500 Subject: [PATCH 3/5] pass isReportArchived to isQuickActionAllowed for canCreateRequest --- .../home/sidebar/FloatingActionButtonAndPopover.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 9d7a488745b1..22d4ba5609d3 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -17,6 +17,7 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; +import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -42,7 +43,7 @@ import { shouldShowPolicy, } from '@libs/PolicyUtils'; import {getQuickActionIcon, getQuickActionTitle, isQuickActionAllowed} from '@libs/QuickActionUtils'; -import {generateReportID, getDisplayNameForParticipant, getIcons, getReportName, getWorkspaceChats, isArchivedReport, isPolicyExpenseChat} from '@libs/ReportUtils'; +import {generateReportID, getDisplayNameForParticipant, getIcons, getReportName, getWorkspaceChats, isPolicyExpenseChat} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import variables from '@styles/variables'; import {closeReactNativeApp} from '@userActions/HybridApp'; @@ -97,7 +98,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: (onyxSession) => ({email: onyxSession?.email, accountID: onyxSession?.accountID})}); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true}); const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, {canBeMissing: true}); - const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${quickActionReport?.reportID}`, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); @@ -120,6 +120,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT const {shouldUseNarrowLayout} = useResponsiveLayout(); const isFocused = useIsFocused(); const prevIsFocused = usePrevious(isFocused); + const isReportArchived = useReportIsArchived(quickActionReport?.reportID); const {isOffline} = useNetwork(); const {isBlockedFromSpotnanaTravel, isBetaEnabled} = usePermissions(); const isManualDistanceTrackingEnabled = isBetaEnabled(CONST.BETAS.MANUAL_DISTANCE); @@ -128,7 +129,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS, {canBeMissing: true}); const canSendInvoice = useMemo(() => canSendInvoicePolicyUtils(allPolicies as OnyxCollection, session?.email), [allPolicies, session?.email]); - const isValidReport = !(isEmptyObject(quickActionReport) || isArchivedReport(reportNameValuePairs)); + const isValidReport = !(isEmptyObject(quickActionReport) || isReportArchived); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { selector: hasSeenTourSelector, @@ -327,7 +328,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT }; if (quickAction?.action) { - if (!isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy)) { + if (!isQuickActionAllowed(quickAction, quickActionReport, quickActionPolicy, isReportArchived)) { return []; } const onSelected = () => { From d99543a0dacd4c708f84185fded171639bc2ca31 Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Fri, 1 Aug 2025 01:58:28 +0500 Subject: [PATCH 4/5] pass isReportArchived to temporary_getMoneyRequestOptions --- src/components/ReportWelcomeText.tsx | 2 +- .../ReportActionCompose/AttachmentPickerWithMenuItems.tsx | 4 +++- .../home/report/ReportActionCompose/ReportActionCompose.tsx | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 902ca4d7f98b..7834da589ce4 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -57,7 +57,7 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { const participantAccountIDs = getParticipantsAccountIDsForDisplay(report, undefined, true, true, reportMetadata); const isMultipleParticipant = participantAccountIDs.length > 1; const displayNamesWithTooltips = getDisplayNamesWithTooltips(getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); - const moneyRequestOptions = temporary_getMoneyRequestOptions(report, policy, participantAccountIDs); + const moneyRequestOptions = temporary_getMoneyRequestOptions(report, policy, participantAccountIDs, isReportArchived); const policyName = getPolicyName({report}); const filteredOptions = moneyRequestOptions.filter( diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index e986ad9345c4..14848187beae 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -16,6 +16,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; +import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -135,6 +136,7 @@ function AttachmentPickerWithMenuItems({ const {isProduction} = useEnvironment(); const {isBetaEnabled} = usePermissions(); const {setIsLoaderVisible} = useFullScreenLoader(); + const isReportArchived = useReportIsArchived(report?.reportID); const isManualDistanceTrackingEnabled = isBetaEnabled(CONST.BETAS.MANUAL_DISTANCE); @@ -228,7 +230,7 @@ function AttachmentPickerWithMenuItems({ ], }; - const moneyRequestOptionsList = temporary_getMoneyRequestOptions(report, policy, reportParticipantIDs ?? []).map((option) => options[option]); + const moneyRequestOptionsList = temporary_getMoneyRequestOptions(report, policy, reportParticipantIDs ?? []).map((option) => options[option], isReportArchived); return moneyRequestOptionsList.flat().filter((item, index, self) => index === self.findIndex((t) => t.text === item.text)); }, [translate, shouldUseNarrowLayout, report, policy, reportParticipantIDs, selectOption, isDelegateAccessRestricted, showDelegateNoAccessModal, isManualDistanceTrackingEnabled]); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 5a6cc51e4129..92b81c0cd7c4 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -27,6 +27,7 @@ import useHandleExceedMaxTaskTitleLength from '@hooks/useHandleExceedMaxTaskTitl import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -217,6 +218,7 @@ function ReportActionCompose({ const includesConcierge = useMemo(() => chatIncludesConcierge({participants: report?.participants}), [report?.participants]); const userBlockedFromConcierge = useMemo(() => isBlockedFromConciergeUserAction(blockedFromConcierge), [blockedFromConcierge]); const isBlockedFromConcierge = useMemo(() => includesConcierge && userBlockedFromConcierge, [includesConcierge, userBlockedFromConcierge]); + const isReportArchived = useReportIsArchived(report?.reportID); const isTransactionThreadView = useMemo(() => isReportTransactionThread(report), [report]); const isExpensesReport = useMemo(() => reportTransactions && reportTransactions.length > 1, [reportTransactions]); @@ -241,7 +243,7 @@ function ReportActionCompose({ const shouldDisplayDualDropZone = useMemo(() => { const parentReport = getParentReport(report); const isSettledOrApproved = isSettled(report) || isSettled(parentReport) || isReportApproved({report}) || isReportApproved({report: parentReport}); - return (shouldAddOrReplaceReceipt && !isSettledOrApproved) || !!temporary_getMoneyRequestOptions(report, policy, reportParticipantIDs).length; + return (shouldAddOrReplaceReceipt && !isSettledOrApproved) || !!temporary_getMoneyRequestOptions(report, policy, reportParticipantIDs, isReportArchived).length; }, [shouldAddOrReplaceReceipt, report, policy, reportParticipantIDs]); // Placeholder to display in the chat input. From 10a6c36239948dc22d4d2bba846cef951733f72b Mon Sep 17 00:00:00 2001 From: Samran Ahmed Date: Fri, 1 Aug 2025 02:15:58 +0500 Subject: [PATCH 5/5] add unit test for archived report in getMoneyRequestOptions --- tests/unit/ReportUtilsTest.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 208bc4fe2e1c..5a1d0aeea008 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -1167,6 +1167,16 @@ describe('ReportUtils', () => { expect(moneyRequestOptions.length).toBe(0); }); + it('its archived report', () => { + const report = { + ...LHNTestUtils.getFakeReport(), + type: CONST.REPORT.TYPE.EXPENSE, + }; + + const moneyRequestOptions = temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID], true); + expect(moneyRequestOptions.length).toBe(0); + }); + it('its trip room', () => { const report = { ...LHNTestUtils.getFakeReport(),