From 5ec533a5d7d70138b6a46855968dd1bab0b1072a Mon Sep 17 00:00:00 2001 From: fahimj Date: Fri, 17 Oct 2025 21:46:50 +0700 Subject: [PATCH 01/16] add failing test reproducing the issue --- tests/actions/IOUTest.ts | 322 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index a5a820016e48..329a02096364 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -12,6 +12,7 @@ import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransact import type {PerDiemExpenseTransactionParams, RequestMoneyParticipantParams} from '@libs/actions/IOU'; import { addSplitExpenseField, + approveMoneyRequest, calculateDiffAmount, canApproveIOU, canCancelPayment, @@ -8903,4 +8904,325 @@ describe('actions/IOU', () => { ); }); }); + describe('approveMoneyRequest with take control', () => { + const adminAccountID = 1; + const managerAccountID = 2; + const employeeAccountID = 3; + const adminEmail = 'admin@test.com'; + const managerEmail = 'manager@test.com'; + const employeeEmail = 'employee@test.com'; + + let expenseReport: Report; + let policy: Policy; + + beforeEach(async () => { + await Onyx.clear(); + + // Set up personal details + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [adminAccountID]: { + accountID: adminAccountID, + login: adminEmail, + displayName: 'Admin User', + }, + [managerAccountID]: { + accountID: managerAccountID, + login: managerEmail, + displayName: 'Manager User', + }, + [employeeAccountID]: { + accountID: employeeAccountID, + login: employeeEmail, + displayName: 'Employee User', + }, + }); + + // Set up session as admin (who will approve) + await Onyx.set(ONYXKEYS.SESSION, { + email: adminEmail, + accountID: adminAccountID, + }); + + // Create policy with approval hierarchy + policy = { + id: '1', + name: 'Test Policy', + role: CONST.POLICY.ROLE.ADMIN, + owner: adminEmail, + ownerAccountID: adminAccountID, + outputCurrency: CONST.CURRENCY.USD, + isPolicyExpenseChatEnabled: true, + type: CONST.POLICY.TYPE.CORPORATE, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + employeeList: { + [employeeEmail]: { + email: employeeEmail, + role: CONST.POLICY.ROLE.USER, + submitsTo: managerEmail, + }, + [managerEmail]: { + email: managerEmail, + role: CONST.POLICY.ROLE.USER, + submitsTo: adminEmail, + forwardsTo: '', + }, + [adminEmail]: { + email: adminEmail, + role: CONST.POLICY.ROLE.ADMIN, + submitsTo: adminEmail, + forwardsTo: adminEmail, + }, + }, + }; + + // Create expense report + expenseReport = { + reportID: '123', + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: employeeAccountID, + managerID: managerAccountID, + policyID: policy.id, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + total: 1000, + currency: 'USD', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); + }); + + afterEach(async () => { + await Onyx.clear(); + }); + + it('should set report to approved when admin takes control and approves', async () => { + // Admin takes control + const takeControlAction = { + reportActionID: 'takeControl1', + actionName: CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL, + actorAccountID: adminAccountID, + created: '2023-01-01T10:00:00.000Z', + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, { + [takeControlAction.reportActionID]: takeControlAction, + }); + + // Admin approves the report + approveMoneyRequest(expenseReport); + await waitForBatchedUpdates(); + + // Should be approved since admin took control and is the last approver + const updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); + expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); + expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.APPROVED); + }); + + it('should invalidate take control when report is resubmitted after take control', async () => { + // Admin takes control first + const takeControlAction = { + reportActionID: 'takeControl3', + actionName: CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL, + actorAccountID: adminAccountID, + created: '2023-01-01T10:00:00.000Z', + }; + + // Employee resubmits after take control (invalidates it) + const submittedAction = { + reportActionID: 'submitted1', + actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, + actorAccountID: employeeAccountID, + created: '2023-01-01T11:00:00.000Z', // After take control + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, { + [takeControlAction.reportActionID]: takeControlAction, + [submittedAction.reportActionID]: submittedAction, + }); + + // Set session as manager (normal approver) + await Onyx.set(ONYXKEYS.SESSION, { + email: managerEmail, + accountID: managerAccountID, + }); + + // Manager approves the report + approveMoneyRequest(expenseReport); + await waitForBatchedUpdates(); + + // Should be submitted to admin (normal flow) since take control was invalidated + const updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); + expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED); + expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.SUBMITTED); + }); + + it('should show admin name in optimistic next step message when admin takes control and approves', async () => { + // Admin takes control + const takeControlAction = { + reportActionID: 'takeControl2', + actionName: CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL, + actorAccountID: adminAccountID, + created: '2023-01-01T10:00:00.000Z', + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, { + [takeControlAction.reportActionID]: takeControlAction, + }); + + // Admin approves the report + approveMoneyRequest(expenseReport); + await waitForBatchedUpdates(); + + // Get the optimistic next step + const nextStep = await getOnyxValue(`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`); + + // The next step message should be defined + expect(nextStep?.message).toBeDefined(); + + // Since the report is fully approved when admin takes control and approves, + // the next step should be about payment, which should mention the admin + // The message should equal "Waiting for Admin User to pay" + const fullMessage = nextStep?.message?.map((part) => part.text).join(''); + expect(fullMessage).toBe('Waiting for Admin User to pay'); + }); + + + }); + + describe('approveMoneyRequest with normal approval chain', () => { + const adminAccountID = 1; + const managerAccountID = 2; + const employeeAccountID = 3; + const adminEmail = 'admin@test.com'; + const managerEmail = 'manager@test.com'; + const employeeEmail = 'employee@test.com'; + + let expenseReport: Report; + let policy: Policy; + + beforeEach(async () => { + await Onyx.clear(); + + // Set up personal details + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [adminAccountID]: { + accountID: adminAccountID, + login: adminEmail, + displayName: 'Admin User', + }, + [managerAccountID]: { + accountID: managerAccountID, + login: managerEmail, + displayName: 'Manager User', + }, + [employeeAccountID]: { + accountID: employeeAccountID, + login: employeeEmail, + displayName: 'Employee User', + }, + }); + + // Create policy with approval hierarchy + policy = { + id: '1', + name: 'Test Policy', + role: CONST.POLICY.ROLE.ADMIN, + owner: adminEmail, + outputCurrency: CONST.CURRENCY.USD, + isPolicyExpenseChatEnabled: true, + type: CONST.POLICY.TYPE.CORPORATE, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + employeeList: { + [employeeEmail]: { + email: employeeEmail, + role: CONST.POLICY.ROLE.USER, + submitsTo: managerEmail, + }, + [managerEmail]: { + email: managerEmail, + role: CONST.POLICY.ROLE.USER, + submitsTo: adminEmail, + forwardsTo: adminEmail, + }, + [adminEmail]: { + email: adminEmail, + role: CONST.POLICY.ROLE.ADMIN, + submitsTo: '', + forwardsTo: '', + }, + }, + }; + + // Create expense report + expenseReport = { + reportID: '123', + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: employeeAccountID, + managerID: managerAccountID, + policyID: policy.id, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + total: 1000, + currency: 'USD', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); + }); + + afterEach(async () => { + await Onyx.clear(); + }); + + it('should follow normal approval chain when manager approves without take control', async () => { + // Set session as manager (first approver in the chain) + await Onyx.set(ONYXKEYS.SESSION, { + email: managerEmail, + accountID: managerAccountID, + }); + + // Manager approves the report (no take control actions) + approveMoneyRequest(expenseReport); + await waitForBatchedUpdates(); + + // Should be submitted to admin (next in approval chain) since manager is not the final approver + const updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); + expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED); + expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.SUBMITTED); + expect(updatedReport?.managerID).toBe(adminAccountID); // Should be forwarded to admin + }); + + it('should handle multi-step approval chain correctly', async () => { + // First, manager approves + await Onyx.set(ONYXKEYS.SESSION, { + email: managerEmail, + accountID: managerAccountID, + }); + + approveMoneyRequest(expenseReport); + await waitForBatchedUpdates(); + + // Should be submitted to admin + let updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); + expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED); + expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.SUBMITTED); + expect(updatedReport?.managerID).toBe(adminAccountID); + + // Then, admin approves + await Onyx.set(ONYXKEYS.SESSION, { + email: adminEmail, + accountID: adminAccountID, + }); + + approveMoneyRequest(updatedReport); + await waitForBatchedUpdates(); + + // Should be fully approved + updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); + expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); + expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.APPROVED); + }); + }); }); From af13b6b8ff91f6b878af8a807a19dbd54afac3cb Mon Sep 17 00:00:00 2001 From: fahimj Date: Fri, 17 Oct 2025 21:57:35 +0700 Subject: [PATCH 02/16] add unit test to verify issue 72742 --- tests/actions/IOUTest.ts | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 329a02096364..8c6b3e02bb82 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -9224,5 +9224,50 @@ describe('actions/IOU', () => { expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.APPROVED); }); + + it('should fully approve report when single approver approves', async () => { + // Create a policy with only one approver in the chain + const singleApproverPolicy: Policy = { + ...policy, + employeeList: { + [employeeEmail]: { + email: employeeEmail, + role: CONST.POLICY.ROLE.USER, + submitsTo: managerEmail, + }, + [managerEmail]: { + email: managerEmail, + role: CONST.POLICY.ROLE.ADMIN, + submitsTo: '', + forwardsTo: '', + }, + }, + }; + + // Create expense report with manager as the only approver + const singleApproverReport: Report = { + ...expenseReport, + reportID: '456', + managerID: managerAccountID, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${singleApproverPolicy.id}`, singleApproverPolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${singleApproverReport.reportID}`, singleApproverReport); + + // Set session as the single approver (manager) + await Onyx.set(ONYXKEYS.SESSION, { + email: managerEmail, + accountID: managerAccountID, + }); + + // Manager approves the report + approveMoneyRequest(singleApproverReport); + await waitForBatchedUpdates(); + + // Should be fully approved since manager is the final approver in the chain + const updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${singleApproverReport.reportID}`); + expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); + expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.APPROVED); + }); }); }); From bf12d368f0966f55364720152e6d626b13eb0ead Mon Sep 17 00:00:00 2001 From: fahimj Date: Fri, 17 Oct 2025 23:06:36 +0700 Subject: [PATCH 03/16] Enhance IOU tests by adding senior manager role and updating approval flow. Adjust optimistic next step messages to reflect new hierarchy and ensure clarity in approval process. --- tests/actions/IOUTest.ts | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 8c6b3e02bb82..422b9411950c 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -8908,9 +8908,11 @@ describe('actions/IOU', () => { const adminAccountID = 1; const managerAccountID = 2; const employeeAccountID = 3; + const seniorManagerAccountID = 4; const adminEmail = 'admin@test.com'; const managerEmail = 'manager@test.com'; const employeeEmail = 'employee@test.com'; + const seniorManagerEmail = 'seniormanager@test.com'; let expenseReport: Report; let policy: Policy; @@ -8925,6 +8927,11 @@ describe('actions/IOU', () => { login: adminEmail, displayName: 'Admin User', }, + [seniorManagerAccountID]: { + accountID: seniorManagerAccountID, + login: seniorManagerEmail, + displayName: 'Senior Manager User', + }, [managerAccountID]: { accountID: managerAccountID, login: managerEmail, @@ -8964,14 +8971,17 @@ describe('actions/IOU', () => { [managerEmail]: { email: managerEmail, role: CONST.POLICY.ROLE.USER, - submitsTo: adminEmail, - forwardsTo: '', + forwardsTo: seniorManagerEmail, + }, + [seniorManagerEmail]: { + email: seniorManagerEmail, + role: CONST.POLICY.ROLE.USER, + forwardsTo: adminEmail, }, [adminEmail]: { email: adminEmail, role: CONST.POLICY.ROLE.ADMIN, - submitsTo: adminEmail, - forwardsTo: adminEmail, + forwardsTo: '', }, }, }; @@ -9052,13 +9062,24 @@ describe('actions/IOU', () => { approveMoneyRequest(expenseReport); await waitForBatchedUpdates(); - // Should be submitted to admin (normal flow) since take control was invalidated + // Should be submitted to senior manager (normal flow) since take control was invalidated const updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED); expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.SUBMITTED); + + // Get the optimistic next step + const nextStep = await getOnyxValue(`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`); + + // The next step message should be defined + expect(nextStep?.message).toBeDefined(); + + // Since take control was invalidated by resubmission, the normal approval chain applies + // The next step should indicate waiting for the senior manager to approve + const fullMessage = nextStep?.message?.map((part) => part.text).join(''); + expect(fullMessage).toBe('Waiting for Senior Manager User to approve %expenses.'); }); - it('should show admin name in optimistic next step message when admin takes control and approves', async () => { + it('should mention an admin to pay expenses in optimistic next step message when admin takes control and approves', async () => { // Admin takes control const takeControlAction = { reportActionID: 'takeControl2', @@ -9085,7 +9106,7 @@ describe('actions/IOU', () => { // the next step should be about payment, which should mention the admin // The message should equal "Waiting for Admin User to pay" const fullMessage = nextStep?.message?.map((part) => part.text).join(''); - expect(fullMessage).toBe('Waiting for Admin User to pay'); + expect(fullMessage).toBe('Waiting for an admin to pay %expenses.'); }); From b47bbf3986f0eb74bd11784a7445974a73be512a Mon Sep 17 00:00:00 2001 From: fahimj Date: Fri, 17 Oct 2025 23:07:32 +0700 Subject: [PATCH 04/16] Implement getBypassApproverIfTakenControl function to enhance approval flow and add new utility for sorting report actions. Update approval logic in approveMoneyRequest to reflect changes in next approver determination. --- src/libs/ReportUtils.ts | 52 +++++++++++++++++++++++++++++++++++++++-- src/libs/actions/IOU.ts | 34 +++++++++++++++------------ 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 99363754316d..b2a90985397d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -186,6 +186,7 @@ import { getReportActionMessageText, getReportActionText, getRetractedMessage, + getSortedReportActions, getTravelUpdateMessage, getWorkspaceCurrencyUpdateMessage, getWorkspaceFrequencyUpdateMessage, @@ -4318,9 +4319,15 @@ function canEditMoneyRequest( function getNextApproverAccountID(report: OnyxEntry, isUnapproved = false) { // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated + // eslint-disable-next-line deprecation/deprecation const policy = getPolicy(report?.policyID); + // If the current user took control, then they are the final approver and we don't have a next approver + const bypassApprover = getBypassApproverIfTakenControl(report); + if (bypassApprover === currentUserAccountID) { + return undefined; + } + const approvalChain = getApprovalChain(policy, report); const submitToAccountID = getSubmitToAccountID(policy, report); @@ -4336,9 +4343,14 @@ function getNextApproverAccountID(report: OnyxEntry, isUnapproved = fals return submitToAccountID; } + if (approvalChain.length === 1 && approvalChain.at(0) === currentUserEmail) { + return undefined; + } + const nextApproverEmail = approvalChain.length === 1 ? approvalChain.at(0) : approvalChain.at(approvalChain.indexOf(currentUserEmail ?? '') + 1); if (!nextApproverEmail) { - return submitToAccountID; + // If there's no next approver in the chain, return undefined to indicate the current user is the final approver + return undefined; } return getAccountIDsByLogins([nextApproverEmail]).at(0); @@ -11462,6 +11474,42 @@ function isWorkspaceEligibleForReportChange(submitterEmail: string | undefined, return isPaidGroupPolicyPolicyUtils(newPolicy) && !!newPolicy.role; } +/** + * Checks if someone took control of the report and if that take control is still valid + * A take control is invalidated if there's a SUBMITTED action after it + */ +function getBypassApproverIfTakenControl(expenseReport: OnyxEntry): number | null { + if (!expenseReport?.reportID) { + return null; + } + + if (!isProcessingReport(expenseReport)) { + return null; + } + + const reportActions = getAllReportActions(expenseReport.reportID); + if (!reportActions) { + return null; + } + + // Sort actions by created timestamp to get chronological order + const sortedActions = getSortedReportActions(Object.values(reportActions ?? {}), true); + + // Look through actions in reverse chronological order (newest first) + // If we find a SUBMITTED action, there's no valid take control since any take control would be older + for (const action of sortedActions) { + if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.SUBMITTED)) { + return null; + } + + if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL)) { + return action.actorAccountID ?? null; + } + } + + return null; +} + function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry): string[] { const approvalChain: string[] = []; const fullApprovalChain: string[] = []; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 497218766a2d..db25cc2a6eaf 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -68,7 +68,7 @@ import Log from '@libs/Log'; import isReportOpenInRHP from '@libs/Navigation/helpers/isReportOpenInRHP'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; -import {buildNextStep} from '@libs/NextStepUtils'; +import {buildNextStep, buildNextStepNew} from '@libs/NextStepUtils'; import * as NumberUtils from '@libs/NumberUtils'; import {getManagerMcTestParticipant, getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; @@ -170,6 +170,7 @@ import { hasOutstandingChildRequest, hasReportBeenReopened, hasReportBeenRetracted, + hasViolations as hasViolationsReportUtils, isArchivedReport, isClosedReport as isClosedReportUtil, isDraftReport, @@ -10114,13 +10115,6 @@ function getIOUReportActionToApproveOrPay(chatReport: OnyxEntry, full?: boolean) { if (!expenseReport) { return; @@ -10141,14 +10135,24 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: const optimisticApprovedReportAction = buildOptimisticApprovedReportAction(total, expenseReport.currency ?? '', expenseReport.reportID); // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const approvalChain = getApprovalChain(getPolicy(expenseReport.policyID), expenseReport); - - const predictedNextStatus = isLastApprover(approvalChain) ? CONST.REPORT.STATUS_NUM.APPROVED : CONST.REPORT.STATUS_NUM.SUBMITTED; - const predictedNextState = isLastApprover(approvalChain) ? CONST.REPORT.STATE_NUM.APPROVED : CONST.REPORT.STATE_NUM.SUBMITTED; - const managerID = isLastApprover(approvalChain) ? expenseReport.managerID : getNextApproverAccountID(expenseReport); + // eslint-disable-next-line deprecation/deprecation + const policy = getPolicy(expenseReport.policyID); + const nextApproverAccountID = getNextApproverAccountID(expenseReport); + const predictedNextStatus = !nextApproverAccountID ? CONST.REPORT.STATUS_NUM.APPROVED : CONST.REPORT.STATUS_NUM.SUBMITTED; + const predictedNextState = !nextApproverAccountID ? CONST.REPORT.STATE_NUM.APPROVED : CONST.REPORT.STATE_NUM.SUBMITTED; + const managerID = !nextApproverAccountID ? expenseReport.managerID : nextApproverAccountID; - const optimisticNextStep = buildNextStep(expenseReport, predictedNextStatus); + const hasViolations = hasViolationsReportUtils(expenseReport.reportID, allTransactionViolations); + const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, allBetas); + const optimisticNextStep = buildNextStepNew({ + report: expenseReport, + policy, + currentUserAccountIDParam: userAccountID, + currentUserEmailParam: currentUserEmail, + hasViolations, + isASAPSubmitBetaEnabled, + predictedNextStatus, + }); const chatReport = getReportOrDraftReport(expenseReport.chatReportID); const optimisticReportActionsData: OnyxUpdate = { From 0d1f14e02cd13259b7d1f38ce214908ee00a5168 Mon Sep 17 00:00:00 2001 From: fahimj Date: Sat, 18 Oct 2025 00:17:55 +0700 Subject: [PATCH 05/16] add unit test to verify it should update next step mesage to show waiting for new approver to approve expenses. --- src/libs/ReportUtils.ts | 29 ++++++-- tests/actions/IOUTest.ts | 139 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b2a90985397d..9065fd7aace3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4323,9 +4323,17 @@ function getNextApproverAccountID(report: OnyxEntry, isUnapproved = fals const policy = getPolicy(report?.policyID); // If the current user took control, then they are the final approver and we don't have a next approver + // If someone else took control or rerouted, they are the next approver const bypassApprover = getBypassApproverIfTakenControl(report); - if (bypassApprover === currentUserAccountID) { - return undefined; + if (bypassApprover) { + return bypassApprover === currentUserAccountID && !isUnapproved ? undefined : bypassApprover; + } + + // Check if the report's managerID has been explicitly set (different from the normal approval chain) + // This handles cases where the approver was changed via addReportApprover/REROUTE action + const normalManagerID = getManagerAccountID(policy, report); + if (report?.managerID && report.managerID !== normalManagerID && report.managerID !== currentUserAccountID) { + return report.managerID; } const approvalChain = getApprovalChain(policy, report); @@ -4343,11 +4351,9 @@ function getNextApproverAccountID(report: OnyxEntry, isUnapproved = fals return submitToAccountID; } - if (approvalChain.length === 1 && approvalChain.at(0) === currentUserEmail) { - return undefined; - } - - const nextApproverEmail = approvalChain.length === 1 ? approvalChain.at(0) : approvalChain.at(approvalChain.indexOf(currentUserEmail ?? '') + 1); + const currentUserIndex = approvalChain.indexOf(currentUserEmail ?? ''); + const nextApproverEmail = currentUserIndex === -1 ? approvalChain.at(0) : approvalChain.at(currentUserIndex + 1); + if (!nextApproverEmail) { // If there's no next approver in the chain, return undefined to indicate the current user is the final approver return undefined; @@ -11505,6 +11511,15 @@ function getBypassApproverIfTakenControl(expenseReport: OnyxEntry): numb if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL)) { return action.actorAccountID ?? null; } + + // REROUTE means the approver was changed to someone else + // In this case, we should use the report's managerID as the next approver + if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.REROUTE)) { + console.log('action NYA GAN', action); + console.log('action.originalMessage?.mentionedAccountIDs NYA GAN', action.originalMessage?.mentionedAccountIDs); + return action.originalMessage?.mentionedAccountIDs?.at(0) ?? null; + // return expenseReport?.managerID ?? null; + } } return null; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 422b9411950c..a6cc13d64633 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -11,6 +11,7 @@ import type {SearchQueryJSON, SearchStatus} from '@components/Search/types'; import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; import type {PerDiemExpenseTransactionParams, RequestMoneyParticipantParams} from '@libs/actions/IOU'; import { + addReportApprover, addSplitExpenseField, approveMoneyRequest, calculateDiffAmount, @@ -9291,4 +9292,142 @@ describe('actions/IOU', () => { expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.APPROVED); }); }); + + describe('addReportApprovers', () => { + const adminAccountID = 1; + const managerAccountID = 2; + const employeeAccountID = 3; + const newApproverAccountID = 4; + const adminEmail = 'admin@test.com'; + const managerEmail = 'manager@test.com'; + const employeeEmail = 'employee@test.com'; + const newApproverEmail = 'newapprover@test.com'; + + let expenseReport: Report; + let policy: Policy; + + beforeEach(async () => { + await Onyx.clear(); + + // Set up personal details + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [adminAccountID]: { + accountID: adminAccountID, + login: adminEmail, + displayName: 'Admin User', + }, + [managerAccountID]: { + accountID: managerAccountID, + login: managerEmail, + displayName: 'Manager User', + }, + [employeeAccountID]: { + accountID: employeeAccountID, + login: employeeEmail, + displayName: 'Employee User', + }, + [newApproverAccountID]: { + accountID: newApproverAccountID, + login: newApproverEmail, + displayName: 'New Approver', + }, + }); + + // Set up session as admin + await Onyx.set(ONYXKEYS.SESSION, { + email: adminEmail, + accountID: adminAccountID, + }); + + // Create policy with ADVANCED approval mode + policy = { + id: '1', + name: 'Test Policy', + role: CONST.POLICY.ROLE.ADMIN, + owner: adminEmail, + outputCurrency: CONST.CURRENCY.USD, + isPolicyExpenseChatEnabled: true, + type: CONST.POLICY.TYPE.CORPORATE, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + employeeList: { + [employeeEmail]: { + email: employeeEmail, + role: CONST.POLICY.ROLE.USER, + submitsTo: managerEmail, + }, + [managerEmail]: { + email: managerEmail, + role: CONST.POLICY.ROLE.USER, + submitsTo: adminEmail, + forwardsTo: adminEmail, + }, + [adminEmail]: { + email: adminEmail, + role: CONST.POLICY.ROLE.ADMIN, + submitsTo: '', + forwardsTo: '', + }, + [newApproverEmail]: { + email: newApproverEmail, + role: CONST.POLICY.ROLE.USER, + submitsTo: adminEmail, + forwardsTo: adminEmail, + }, + }, + }; + + // Create expense report + expenseReport = { + reportID: '123', + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: employeeAccountID, + managerID: managerAccountID, + policyID: policy.id, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + total: 1000, + currency: 'USD', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); + }); + + afterEach(async () => { + await Onyx.clear(); + }); + + it('should update next step to show waiting for new approver to approve expenses', async () => { + // Admin adds a new approver to the report + addReportApprover(expenseReport, newApproverEmail, newApproverAccountID, adminAccountID); + await waitForBatchedUpdates(); + + // Check that managerID was updated to the new approver + const updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); + expect(updatedReport?.managerID).toBe(newApproverAccountID); + + // Check that next step message is updated correctly + const nextStep = await getOnyxValue(`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`); + expect(nextStep).toBeTruthy(); + expect(nextStep?.message).toEqual([ + { + text: 'Waiting for ', + }, + { + text: 'New Approver', + type: 'strong', + clickToCopyText: newApproverEmail, + }, + { + text: ' to ', + }, + { + text: 'approve', + }, + { + text: ' %expenses.', + }, + ]); + }); + }); }); From bdd7422f8b7657f553e104819050da937811b285 Mon Sep 17 00:00:00 2001 From: fahimj Date: Sat, 18 Oct 2025 17:19:06 +0700 Subject: [PATCH 06/16] revert attempt to fix optimistic next approver message --- src/libs/ReportUtils.ts | 18 +---- tests/actions/IOUTest.ts | 141 --------------------------------------- 2 files changed, 1 insertion(+), 158 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9065fd7aace3..84a6c9a4dd1c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4329,13 +4329,6 @@ function getNextApproverAccountID(report: OnyxEntry, isUnapproved = fals return bypassApprover === currentUserAccountID && !isUnapproved ? undefined : bypassApprover; } - // Check if the report's managerID has been explicitly set (different from the normal approval chain) - // This handles cases where the approver was changed via addReportApprover/REROUTE action - const normalManagerID = getManagerAccountID(policy, report); - if (report?.managerID && report.managerID !== normalManagerID && report.managerID !== currentUserAccountID) { - return report.managerID; - } - const approvalChain = getApprovalChain(policy, report); const submitToAccountID = getSubmitToAccountID(policy, report); @@ -4353,7 +4346,7 @@ function getNextApproverAccountID(report: OnyxEntry, isUnapproved = fals const currentUserIndex = approvalChain.indexOf(currentUserEmail ?? ''); const nextApproverEmail = currentUserIndex === -1 ? approvalChain.at(0) : approvalChain.at(currentUserIndex + 1); - + if (!nextApproverEmail) { // If there's no next approver in the chain, return undefined to indicate the current user is the final approver return undefined; @@ -11511,15 +11504,6 @@ function getBypassApproverIfTakenControl(expenseReport: OnyxEntry): numb if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL)) { return action.actorAccountID ?? null; } - - // REROUTE means the approver was changed to someone else - // In this case, we should use the report's managerID as the next approver - if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.REROUTE)) { - console.log('action NYA GAN', action); - console.log('action.originalMessage?.mentionedAccountIDs NYA GAN', action.originalMessage?.mentionedAccountIDs); - return action.originalMessage?.mentionedAccountIDs?.at(0) ?? null; - // return expenseReport?.managerID ?? null; - } } return null; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index a6cc13d64633..2acea8486a8a 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -11,7 +11,6 @@ import type {SearchQueryJSON, SearchStatus} from '@components/Search/types'; import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; import type {PerDiemExpenseTransactionParams, RequestMoneyParticipantParams} from '@libs/actions/IOU'; import { - addReportApprover, addSplitExpenseField, approveMoneyRequest, calculateDiffAmount, @@ -9109,8 +9108,6 @@ describe('actions/IOU', () => { const fullMessage = nextStep?.message?.map((part) => part.text).join(''); expect(fullMessage).toBe('Waiting for an admin to pay %expenses.'); }); - - }); describe('approveMoneyRequest with normal approval chain', () => { @@ -9292,142 +9289,4 @@ describe('actions/IOU', () => { expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.APPROVED); }); }); - - describe('addReportApprovers', () => { - const adminAccountID = 1; - const managerAccountID = 2; - const employeeAccountID = 3; - const newApproverAccountID = 4; - const adminEmail = 'admin@test.com'; - const managerEmail = 'manager@test.com'; - const employeeEmail = 'employee@test.com'; - const newApproverEmail = 'newapprover@test.com'; - - let expenseReport: Report; - let policy: Policy; - - beforeEach(async () => { - await Onyx.clear(); - - // Set up personal details - await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { - [adminAccountID]: { - accountID: adminAccountID, - login: adminEmail, - displayName: 'Admin User', - }, - [managerAccountID]: { - accountID: managerAccountID, - login: managerEmail, - displayName: 'Manager User', - }, - [employeeAccountID]: { - accountID: employeeAccountID, - login: employeeEmail, - displayName: 'Employee User', - }, - [newApproverAccountID]: { - accountID: newApproverAccountID, - login: newApproverEmail, - displayName: 'New Approver', - }, - }); - - // Set up session as admin - await Onyx.set(ONYXKEYS.SESSION, { - email: adminEmail, - accountID: adminAccountID, - }); - - // Create policy with ADVANCED approval mode - policy = { - id: '1', - name: 'Test Policy', - role: CONST.POLICY.ROLE.ADMIN, - owner: adminEmail, - outputCurrency: CONST.CURRENCY.USD, - isPolicyExpenseChatEnabled: true, - type: CONST.POLICY.TYPE.CORPORATE, - approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, - employeeList: { - [employeeEmail]: { - email: employeeEmail, - role: CONST.POLICY.ROLE.USER, - submitsTo: managerEmail, - }, - [managerEmail]: { - email: managerEmail, - role: CONST.POLICY.ROLE.USER, - submitsTo: adminEmail, - forwardsTo: adminEmail, - }, - [adminEmail]: { - email: adminEmail, - role: CONST.POLICY.ROLE.ADMIN, - submitsTo: '', - forwardsTo: '', - }, - [newApproverEmail]: { - email: newApproverEmail, - role: CONST.POLICY.ROLE.USER, - submitsTo: adminEmail, - forwardsTo: adminEmail, - }, - }, - }; - - // Create expense report - expenseReport = { - reportID: '123', - type: CONST.REPORT.TYPE.EXPENSE, - ownerAccountID: employeeAccountID, - managerID: managerAccountID, - policyID: policy.id, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, - statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, - total: 1000, - currency: 'USD', - }; - - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); - }); - - afterEach(async () => { - await Onyx.clear(); - }); - - it('should update next step to show waiting for new approver to approve expenses', async () => { - // Admin adds a new approver to the report - addReportApprover(expenseReport, newApproverEmail, newApproverAccountID, adminAccountID); - await waitForBatchedUpdates(); - - // Check that managerID was updated to the new approver - const updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); - expect(updatedReport?.managerID).toBe(newApproverAccountID); - - // Check that next step message is updated correctly - const nextStep = await getOnyxValue(`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`); - expect(nextStep).toBeTruthy(); - expect(nextStep?.message).toEqual([ - { - text: 'Waiting for ', - }, - { - text: 'New Approver', - type: 'strong', - clickToCopyText: newApproverEmail, - }, - { - text: ' to ', - }, - { - text: 'approve', - }, - { - text: ' %expenses.', - }, - ]); - }); - }); }); From 8b6fff352dafca39faaee75e88419ae37e2d4328 Mon Sep 17 00:00:00 2001 From: fahimj Date: Sat, 18 Oct 2025 20:53:54 +0700 Subject: [PATCH 07/16] Revert "Resolve merge conflict in import comments" This reverts commit 985de4add57d5916dcc41122bd22bcefb292d6d1, reversing changes made to d67dcd692a8cae2be1e0368f157b2d6349fde251. --- src/libs/actions/IOU.ts | 271 ++++++++++++++++++++++++---------------- 1 file changed, 161 insertions(+), 110 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index cfb8c56983a1..5df4fb590f02 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -560,6 +560,7 @@ type DistanceRequestTransactionParams = BaseTransactionParams & { validWaypoints?: WaypointCollection; splitShares?: SplitShares; distance?: number; + receipt?: Receipt; }; type CreateDistanceRequestInformation = { @@ -777,15 +778,6 @@ Onyx.connect({ }, }); -let allDraftSplitTransactions: NonNullable> = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT, - waitForCollectionCallback: true, - callback: (value) => { - allDraftSplitTransactions = value ?? {}; - }, -}); - let allNextSteps: NonNullable> = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.NEXT_STEP, @@ -2097,6 +2089,8 @@ function buildOnyxDataForMoneyRequest(moneyRequestParams: BuildOnyxDataForMoneyR optimisticData.push(violationsOnyxData, { key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iou.report.reportID}`, onyxMethod: Onyx.METHOD.SET, + // TODO: Replace onyx.connect with useOnyx hook (https://github.com/Expensify/App/issues/66365) + // eslint-disable-next-line @typescript-eslint/no-deprecated value: buildNextStep(iou.report, iou.report.statusNum ?? CONST.REPORT.STATE_NUM.OPEN, shouldFixViolations), }); failureData.push({ @@ -3579,6 +3573,8 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma : {}; const predictedNextStatus = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO ? CONST.REPORT.STATUS_NUM.CLOSED : CONST.REPORT.STATUS_NUM.OPEN; + // TODO: Replace onyx.connect with useOnyx hook (https://github.com/Expensify/App/issues/66365) + // eslint-disable-next-line @typescript-eslint/no-deprecated const optimisticNextStep = buildNextStep(iouReport, predictedNextStatus); // STEP 5: Build Onyx Data @@ -3830,6 +3826,8 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI : {}; const predictedNextStatus = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO ? CONST.REPORT.STATUS_NUM.CLOSED : CONST.REPORT.STATUS_NUM.OPEN; + // TODO: Replace onyx.connect with useOnyx hook (https://github.com/Expensify/App/issues/66365) + // eslint-disable-next-line @typescript-eslint/no-deprecated const optimisticNextStep = buildNextStep(iouReport, predictedNextStatus); // STEP 5: Build Onyx Data @@ -4609,7 +4607,6 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U policyCategories ?? {}, hasDependentTags(policy, policyTagList ?? {}), isInvoice, - isSelfDM(iouReport), ); optimisticData.push(violationsOnyxData); failureData.push({ @@ -4647,6 +4644,8 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport?.reportID}`, + // TODO: Replace onyx.connect with useOnyx hook (https://github.com/Expensify/App/issues/66365) + // eslint-disable-next-line @typescript-eslint/no-deprecated value: buildNextStep(updatedMoneyRequestReport ?? iouReport ?? undefined, iouReport?.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN, shouldFixViolations), }); failureData.push({ @@ -7857,11 +7856,16 @@ function completeSplitBill( notifyNewAction(chatReportID, sessionAccountID); } -function setDraftSplitTransaction(transactionID: string | undefined, transactionChanges: TransactionChanges = {}, policy?: OnyxEntry) { +function setDraftSplitTransaction( + transactionID: string | undefined, + splitTransactionDraft: OnyxEntry, + transactionChanges: TransactionChanges = {}, + policy?: OnyxEntry, +) { if (!transactionID) { return undefined; } - let draftSplitTransaction = allDraftSplitTransactions[`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`]; + let draftSplitTransaction = splitTransactionDraft; if (!draftSplitTransaction) { draftSplitTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; @@ -7913,6 +7917,7 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest customUnitRateID = '', splitShares = {}, attendees, + receipt, } = transactionParams; // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function @@ -8016,7 +8021,7 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest comment, created, merchant, - receipt: !isManualDistanceRequest ? optimisticReceipt : undefined, + receipt: receipt ?? optimisticReceipt, category, tag, taxCode, @@ -8048,6 +8053,7 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest reportPreviewReportActionID: reportPreviewAction.reportActionID, waypoints: JSON.stringify(sanitizedWaypoints), distance, + receipt, created, category, tag, @@ -8151,15 +8157,14 @@ function updateMoneyRequestAmountAndCurrency({ function prepareToCleanUpMoneyRequest( transactionID: string, reportAction: OnyxTypes.ReportAction, - iouReport: OnyxEntry, - chatReport: OnyxEntry, shouldRemoveIOUTransactionID = true, transactionIDsPendingDeletion?: string[], selectedTransactionIDs?: string[], - isChatReportArchived = false, ) { // STEP 1: Get all collections we're updating - const iouReportID = iouReport?.reportID; + const iouReportID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUReportID : undefined; + const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`] ?? null; + const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`]; const reportPreviewAction = getReportPreviewAction(iouReport?.chatReportID, iouReport?.reportID); const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const isTransactionOnHold = isOnHold(transaction); @@ -8200,7 +8205,7 @@ function prepareToCleanUpMoneyRequest( let canUserPerformWriteAction = true; if (chatReport) { - canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport, isChatReportArchived); + canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport); } // If we are deleting the last transaction on a report, then delete the report too const shouldDeleteIOUReport = getReportTransactions(iouReportID).filter((trans) => !transactionIDsPendingDeletion?.includes(trans.transactionID)).length === 1; @@ -8235,7 +8240,7 @@ function prepareToCleanUpMoneyRequest( } } // STEP 4: Update the iouReport and reportPreview with new totals and messages if it wasn't deleted - let updatedIOUReport; + let updatedIOUReport: OnyxInputValue; const currency = getCurrency(transaction); const updatedReportPreviewAction: Partial> = cloneDeep(reportPreviewAction ?? {}); updatedReportPreviewAction.pendingAction = shouldDeleteIOUReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; @@ -8320,9 +8325,11 @@ function prepareToCleanUpMoneyRequest( updatedReportPreviewAction, transactionThreadID, transactionThread, + chatReport, transaction, transactionViolations, reportPreviewAction, + iouReport, }; } @@ -8333,28 +8340,12 @@ function prepareToCleanUpMoneyRequest( * @param isSingleTransactionView - whether we are in the transaction thread report * @returns The URL to navigate to */ -function getNavigationUrlOnMoneyRequestDelete( - transactionID: string | undefined, - reportAction: OnyxTypes.ReportAction, - iouReport: OnyxEntry, - chatReport: OnyxEntry, - isSingleTransactionView = false, - isChatReportArchived = false, -): Route | undefined { +function getNavigationUrlOnMoneyRequestDelete(transactionID: string | undefined, reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false): Route | undefined { if (!transactionID) { return undefined; } - const {shouldDeleteTransactionThread, shouldDeleteIOUReport} = prepareToCleanUpMoneyRequest( - transactionID, - reportAction, - iouReport, - chatReport, - undefined, - undefined, - undefined, - isChatReportArchived, - ); + const {shouldDeleteTransactionThread, shouldDeleteIOUReport, iouReport} = prepareToCleanUpMoneyRequest(transactionID, reportAction); // Determine which report to navigate back to if (iouReport && isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { @@ -8377,22 +8368,20 @@ function getNavigationUrlOnMoneyRequestDelete( * @returns The URL to navigate to */ function getNavigationUrlAfterTrackExpenseDelete( - chatReport: OnyxEntry | undefined, + chatReportID: string | undefined, transactionID: string | undefined, reportAction: OnyxTypes.ReportAction, - iouReport: OnyxEntry, - chatIOUReport: OnyxEntry, isSingleTransactionView = false, - isChatReportArchived = false, ): Route | undefined { - const chatReportID = chatReport?.reportID; if (!chatReportID || !transactionID) { return undefined; } + const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; + // If not a self DM, handle it as a regular money request if (!isSelfDM(chatReport)) { - return getNavigationUrlOnMoneyRequestDelete(transactionID, reportAction, iouReport, chatIOUReport, isSingleTransactionView, isChatReportArchived); + return getNavigationUrlOnMoneyRequestDelete(transactionID, reportAction, isSingleTransactionView); } // Only navigate if in single transaction view and the thread will be deleted @@ -8411,19 +8400,20 @@ function getNavigationUrlAfterTrackExpenseDelete( * @param isSingleTransactionView - whether we are in the transaction thread report * @return the url to navigate back once the money request is deleted */ -function cleanUpMoneyRequest( - transactionID: string, - reportAction: OnyxTypes.ReportAction, - reportID: string, - iouReport: OnyxEntry, - chatReport: OnyxEntry, - isSingleTransactionView = false, - isChatIOUReportArchived = false, -) { - const {shouldDeleteTransactionThread, shouldDeleteIOUReport, updatedReportAction, updatedIOUReport, updatedReportPreviewAction, transactionThreadID, reportPreviewAction} = - prepareToCleanUpMoneyRequest(transactionID, reportAction, iouReport, chatReport, false, undefined, undefined, isChatIOUReportArchived); +function cleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.ReportAction, reportID: string, isSingleTransactionView = false) { + const { + shouldDeleteTransactionThread, + shouldDeleteIOUReport, + updatedReportAction, + updatedIOUReport, + updatedReportPreviewAction, + transactionThreadID, + chatReport, + iouReport, + reportPreviewAction, + } = prepareToCleanUpMoneyRequest(transactionID, reportAction, false); - const urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete(transactionID, reportAction, iouReport, chatReport, isSingleTransactionView, isChatIOUReportArchived); + const urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete(transactionID, reportAction, isSingleTransactionView); // build Onyx data // Onyx operations to delete the transaction, update the IOU report action and chat report action @@ -8516,7 +8506,7 @@ function cleanUpMoneyRequest( if (shouldDeleteIOUReport) { let canUserPerformWriteAction = true; if (chatReport) { - canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport, isChatIOUReportArchived); + canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport); } const lastMessageText = getLastVisibleMessage( @@ -8574,12 +8564,9 @@ function deleteMoneyRequest( reportAction: OnyxTypes.ReportAction, transactions: OnyxCollection, violations: OnyxCollection, - iouReport: OnyxEntry, - chatReport: OnyxEntry, isSingleTransactionView = false, transactionIDsPendingDeletion?: string[], selectedTransactionIDs?: string[], - isChatIOUReportArchived = false, ) { if (!transactionID) { return; @@ -8594,12 +8581,14 @@ function deleteMoneyRequest( updatedReportPreviewAction, transactionThreadID, transactionThread, + chatReport, transaction, transactionViolations, + iouReport, reportPreviewAction, - } = prepareToCleanUpMoneyRequest(transactionID, reportAction, iouReport, chatReport, false, transactionIDsPendingDeletion, selectedTransactionIDs, isChatIOUReportArchived); + } = prepareToCleanUpMoneyRequest(transactionID, reportAction, false, transactionIDsPendingDeletion, selectedTransactionIDs); - const urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete(transactionID, reportAction, iouReport, chatReport, isSingleTransactionView, isChatIOUReportArchived); + const urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete(transactionID, reportAction, isSingleTransactionView); // STEP 2: Build Onyx data // The logic mostly resembles the cleanUpMoneyRequest function @@ -8692,7 +8681,7 @@ function deleteMoneyRequest( if (shouldDeleteIOUReport) { let canUserPerformWriteAction = true; if (chatReport) { - canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport, isChatIOUReportArchived); + canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport); } const optimisticReportActions = reportPreviewAction?.reportActionID ? {[reportPreviewAction.reportActionID]: null} : {}; @@ -8865,27 +8854,24 @@ function deleteMoneyRequest( } function deleteTrackExpense( - chatReport: OnyxEntry | undefined, + chatReportID: string | undefined, transactionID: string | undefined, reportAction: OnyxTypes.ReportAction, - iouReport: OnyxEntry, - chatIOUReport: OnyxEntry, transactions: OnyxCollection, violations: OnyxCollection, isSingleTransactionView = false, isChatReportArchived = false, - isChatIOUReportArchived = false, ) { - const chatReportID = chatReport?.reportID; if (!chatReportID || !transactionID) { return; } - const urlToNavigateBack = getNavigationUrlAfterTrackExpenseDelete(chatReport, transactionID, reportAction, iouReport, chatIOUReport, isSingleTransactionView, isChatIOUReportArchived); + const urlToNavigateBack = getNavigationUrlAfterTrackExpenseDelete(chatReportID, transactionID, reportAction, isSingleTransactionView); // STEP 1: Get all collections we're updating + const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; if (!isSelfDM(chatReport)) { - deleteMoneyRequest(transactionID, reportAction, transactions, violations, iouReport, chatIOUReport, isSingleTransactionView, undefined, undefined, isChatIOUReportArchived); + deleteMoneyRequest(transactionID, reportAction, transactions, violations, isSingleTransactionView); return urlToNavigateBack; } @@ -9655,6 +9641,8 @@ function getPayMoneyRequestParams({ let optimisticNextStep = null; if (!isInvoiceReport) { currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport?.reportID}`] ?? null; + // TODO: Replace onyx.connect with useOnyx hook (https://github.com/Expensify/App/issues/66365) + // eslint-disable-next-line @typescript-eslint/no-deprecated optimisticNextStep = buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.REIMBURSED); } @@ -10335,7 +10323,8 @@ function reopenReport(expenseReport: OnyxEntry) { const optimisticReopenedReportAction = buildOptimisticReopenedReportAction(); const predictedNextState = CONST.REPORT.STATE_NUM.OPEN; const predictedNextStatus = CONST.REPORT.STATUS_NUM.OPEN; - + // TODO: Replace onyx.connect with useOnyx hook (https://github.com/Expensify/App/issues/66365) + // eslint-disable-next-line @typescript-eslint/no-deprecated const optimisticNextStep = buildNextStep(expenseReport, predictedNextStatus, undefined, undefined, true); const optimisticReportActionsData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, @@ -10459,7 +10448,8 @@ function retractReport(expenseReport: OnyxEntry) { const optimisticRetractReportAction = buildOptimisticRetractedReportAction(); const predictedNextState = CONST.REPORT.STATE_NUM.OPEN; const predictedNextStatus = CONST.REPORT.STATUS_NUM.OPEN; - + // TODO: Replace onyx.connect with useOnyx hook (https://github.com/Expensify/App/issues/66365) + // eslint-disable-next-line @typescript-eslint/no-deprecated const optimisticNextStep = buildNextStep(expenseReport, predictedNextStatus); const optimisticReportActionsData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, @@ -10582,6 +10572,8 @@ function unapproveExpenseReport(expenseReport: OnyxEntry) { const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; const optimisticUnapprovedReportAction = buildOptimisticUnapprovedReportAction(expenseReport.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID); + // TODO: Replace onyx.connect with useOnyx hook (https://github.com/Expensify/App/issues/66365) + // eslint-disable-next-line @typescript-eslint/no-deprecated const optimisticNextStep = buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.SUBMITTED, false, true); const optimisticReportActionData: OnyxUpdate = { @@ -10698,7 +10690,14 @@ function unapproveExpenseReport(expenseReport: OnyxEntry) { API.write(WRITE_COMMANDS.UNAPPROVE_EXPENSE_REPORT, parameters, {optimisticData, successData, failureData}); } -function submitReport(expenseReport?: OnyxTypes.Report) { +function submitReport( + expenseReport: OnyxEntry, + policy: OnyxEntry, + accountIDParam: number, + emailParam: string, + hasViolationsParam: boolean, + isASAPSubmitBetaEnabled: boolean, +) { if (!expenseReport) { return; } @@ -10709,14 +10708,19 @@ function submitReport(expenseReport?: OnyxTypes.Report) { const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; const parentReport = getReportOrDraftReport(expenseReport.parentReportID); - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(expenseReport.policyID); const isCurrentUserManager = currentUserPersonalDetails?.accountID === expenseReport.managerID; const isSubmitAndClosePolicy = isSubmitAndClose(policy); const adminAccountID = policy?.role === CONST.POLICY.ROLE.ADMIN ? currentUserPersonalDetails?.accountID : undefined; const optimisticSubmittedReportAction = buildOptimisticSubmittedReportAction(expenseReport?.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID, adminAccountID); - const optimisticNextStep = buildNextStep(expenseReport, isSubmitAndClosePolicy ? CONST.REPORT.STATUS_NUM.CLOSED : CONST.REPORT.STATUS_NUM.SUBMITTED); + const optimisticNextStep = buildNextStepNew({ + report: expenseReport, + predictedNextStatus: isSubmitAndClosePolicy ? CONST.REPORT.STATUS_NUM.CLOSED : CONST.REPORT.STATUS_NUM.SUBMITTED, + policy, + currentUserAccountIDParam: accountIDParam, + currentUserEmailParam: emailParam, + hasViolations: hasViolationsParam, + isASAPSubmitBetaEnabled, + }); const approvalChain = getApprovalChain(policy, expenseReport); const managerID = getAccountIDsByLogins(approvalChain).at(0); @@ -10836,7 +10840,15 @@ function submitReport(expenseReport?: OnyxTypes.Report) { API.write(WRITE_COMMANDS.SUBMIT_REPORT, parameters, {optimisticData, successData, failureData}); } -function cancelPayment(expenseReport: OnyxEntry, chatReport: OnyxTypes.Report) { +function cancelPayment( + expenseReport: OnyxEntry, + chatReport: OnyxTypes.Report, + policy: OnyxEntry, + isASAPSubmitBetaEnabled: boolean, + accountIDParam: number, + emailParam: string, + hasViolationsParam: boolean, +) { if (isEmptyObject(expenseReport)) { return; } @@ -10846,13 +10858,18 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O -((expenseReport.total ?? 0) - (expenseReport?.nonReimbursableTotal ?? 0)), expenseReport.currency ?? '', ); - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(chatReport.policyID); const approvalMode = policy?.approvalMode ?? CONST.POLICY.APPROVAL_MODE.BASIC; const stateNum: ValueOf = approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.APPROVED; const statusNum: ValueOf = approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.APPROVED; - const optimisticNextStep = buildNextStep(expenseReport, statusNum); + const optimisticNextStep = buildNextStepNew({ + report: expenseReport, + predictedNextStatus: statusNum, + policy, + currentUserAccountIDParam: accountIDParam, + currentUserEmailParam: emailParam, + hasViolations: hasViolationsParam, + isASAPSubmitBetaEnabled, + }); const iouReportActions = getAllReportActions(chatReport.iouReportID); const expenseReportActions = getAllReportActions(expenseReport.reportID); const iouCreatedAction = Object.values(iouReportActions).find((action) => isCreatedAction(action)); @@ -10975,7 +10992,15 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, - value: buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.REIMBURSED), + value: buildNextStepNew({ + report: expenseReport, + predictedNextStatus: CONST.REPORT.STATUS_NUM.REIMBURSED, + policy, + currentUserAccountIDParam: accountIDParam, + currentUserEmailParam: emailParam, + hasViolations: hasViolationsParam, + isASAPSubmitBetaEnabled, + }), }); API.write( @@ -12422,7 +12447,11 @@ function shouldOptimisticallyUpdateSearch( isInvoice: boolean | undefined, transaction?: OnyxEntry, ) { - if (currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.INVOICE && currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.EXPENSE) { + if ( + currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.INVOICE && + currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.EXPENSE && + currentSearchQueryJSON.type !== CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT + ) { return false; } let shouldOptimisticallyUpdateByStatus; @@ -12448,7 +12477,10 @@ function shouldOptimisticallyUpdateSearch( const unapprovedCashSimilarSearchHash = suggestedSearches[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH].similarSearchHash; const validSearchTypes = - (!isInvoice && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) || (isInvoice && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.INVOICE); + (!isInvoice && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE) || + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (isInvoice && currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.INVOICE) || + currentSearchQueryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; return ( shouldOptimisticallyUpdateByStatus && @@ -13238,16 +13270,18 @@ function removeSplitExpenseField(draftTransaction: OnyxEntry, splitExpenseTransactionID: string) { - if (!splitExpenseDraftTransaction || !splitExpenseTransactionID) { +function updateSplitExpenseField( + splitExpenseDraftTransaction: OnyxEntry, + originalTransactionDraft: OnyxEntry, + splitExpenseTransactionID: string, +) { + if (!splitExpenseDraftTransaction || !splitExpenseTransactionID || !originalTransactionDraft) { return; } const originalTransactionID = splitExpenseDraftTransaction?.comment?.originalTransactionID; - const draftTransaction = allDraftSplitTransactions[`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${originalTransactionID}`]; - - const splitExpenses = draftTransaction?.comment?.splitExpenses?.map((item) => { + const splitExpenses = originalTransactionDraft?.comment?.splitExpenses?.map((item) => { if (item.transactionID === splitExpenseTransactionID) { const transactionDetails = getTransactionDetails(splitExpenseDraftTransaction); @@ -13305,23 +13339,20 @@ function clearSplitTransactionDraftErrors(transactionID: string | undefined) { } function saveSplitTransactions( - originalTransactionID: string, draftTransaction: OnyxEntry, hash: number, policyCategories: OnyxTypes.PolicyCategories | undefined, policy: OnyxTypes.Policy | undefined, policyRecentlyUsedCategories: OnyxTypes.RecentlyUsedCategories | undefined, - iouReport: OnyxEntry, - chatReport: OnyxEntry, - firstIOU: OnyxEntry | undefined, - isChatReportArchived = false, ) { const transactionReport = getReportOrDraftReport(draftTransaction?.reportID); const parentTransactionReport = getReportOrDraftReport(transactionReport?.parentReportID); const expenseReport = transactionReport?.type === CONST.REPORT.TYPE.EXPENSE ? transactionReport : parentTransactionReport; + const originalTransactionID = draftTransaction?.comment?.originalTransactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID; const originalTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]; const originalTransactionDetails = getTransactionDetails(originalTransaction); + const iouActions = getIOUActionForTransactions([originalTransactionID], expenseReport?.reportID); const policyTags = getPolicyTagsData(expenseReport?.policyID); const participants = getMoneyRequestParticipantsFromReport(expenseReport); @@ -13552,17 +13583,9 @@ function saveSplitTransactions( value: originalTransaction, }); + const firstIOU = iouActions.at(0); if (firstIOU) { - const {updatedReportAction, transactionThread} = prepareToCleanUpMoneyRequest( - originalTransactionID, - firstIOU, - iouReport, - chatReport, - undefined, - undefined, - undefined, - isChatReportArchived, - ); + const {updatedReportAction, iouReport, transactionThread} = prepareToCleanUpMoneyRequest(originalTransactionID, firstIOU); optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${firstIOU?.childReportID}`, @@ -13643,7 +13666,7 @@ function saveSplitTransactions( InteractionManager.runAfterInteractions(() => removeDraftSplitTransaction(originalTransactionID)); const isSearchPageTopmostFullScreenRoute = isSearchTopmostFullScreenRoute(); - const transactionThreadReportID = firstIOU?.childReportID; + const transactionThreadReportID = iouActions.at(0)?.childReportID; const transactionThreadReportScreen = Navigation.getReportRouteByID(transactionThreadReportID); if (isSearchPageTopmostFullScreenRoute || !transactionReport?.parentReportID) { @@ -13674,10 +13697,20 @@ function saveSplitTransactions( }); } -function assignReportToMe(report: OnyxTypes.Report, accountID: number) { +function assignReportToMe(report: OnyxTypes.Report, accountID: number, email: string, policy: OnyxEntry, hasViolations: boolean, isASAPSubmitBetaEnabled: boolean) { const takeControlReportAction = buildOptimisticChangeApproverReportAction(accountID, accountID); const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${report?.reportID}`] ?? null; - const optimisticNextStep = buildNextStep({...report, managerID: accountID}, report.statusNum ?? CONST.REPORT.STATUS_NUM.SUBMITTED, false, true); + const optimisticNextStep = buildNextStepNew({ + report: {...report, managerID: accountID}, + predictedNextStatus: report.statusNum ?? CONST.REPORT.STATUS_NUM.SUBMITTED, + shouldFixViolations: false, + isUnapprove: true, + policy, + currentUserAccountIDParam: accountID, + currentUserEmailParam: email, + hasViolations, + isASAPSubmitBetaEnabled, + }); const onyxData: OnyxData = { optimisticData: [ @@ -13738,10 +13771,29 @@ function assignReportToMe(report: OnyxTypes.Report, accountID: number) { API.write(WRITE_COMMANDS.ASSIGN_REPORT_TO_ME, params, onyxData); } -function addReportApprover(report: OnyxTypes.Report, newApproverEmail: string, newApproverAccountID: number, accountID: number) { +function addReportApprover( + report: OnyxTypes.Report, + newApproverEmail: string, + newApproverAccountID: number, + accountID: number, + email: string, + policy: OnyxEntry, + hasViolations: boolean, + isASAPSubmitBetaEnabled: boolean, +) { const takeControlReportAction = buildOptimisticChangeApproverReportAction(newApproverAccountID, accountID); const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${report?.reportID}`] ?? null; - const optimisticNextStep = buildNextStep({...report, managerID: newApproverAccountID}, report.statusNum ?? CONST.REPORT.STATUS_NUM.SUBMITTED, false, true); + const optimisticNextStep = buildNextStepNew({ + report: {...report, managerID: newApproverAccountID}, + predictedNextStatus: report.statusNum ?? CONST.REPORT.STATUS_NUM.SUBMITTED, + shouldFixViolations: false, + isUnapprove: true, + policy, + currentUserAccountIDParam: accountID, + currentUserEmailParam: email, + hasViolations, + isASAPSubmitBetaEnabled, + }); const onyxData: OnyxData = { optimisticData: [ { @@ -13918,7 +13970,6 @@ export { assignReportToMe, getPerDiemExpenseInformation, getSendInvoiceInformation, - getIOUActionForTransactions, addReportApprover, hasOutstandingChildRequest, getUpdateMoneyRequestParams, From b8b84f361f7fbf18a6b4c695b89b35ed82c9d003 Mon Sep 17 00:00:00 2001 From: fahimj Date: Sat, 18 Oct 2025 21:03:34 +0700 Subject: [PATCH 08/16] eslint update --- src/libs/ReportUtils.ts | 2 +- src/libs/actions/IOU.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index de70031ac845..f686049a5154 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4319,7 +4319,7 @@ function canEditMoneyRequest( function getNextApproverAccountID(report: OnyxEntry, isUnapproved = false) { // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line deprecation/deprecation + // eslint-disable-next-line @typescript-eslint/no-deprecated const policy = getPolicy(report?.policyID); // If the current user took control, then they are the final approver and we don't have a next approver diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5df4fb590f02..8eb053f5d99d 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -10125,7 +10125,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: const optimisticApprovedReportAction = buildOptimisticApprovedReportAction(total, expenseReport.currency ?? '', expenseReport.reportID); // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line deprecation/deprecation + // eslint-disable-next-line @typescript-eslint/no-deprecated const policy = getPolicy(expenseReport.policyID); const nextApproverAccountID = getNextApproverAccountID(expenseReport); const predictedNextStatus = !nextApproverAccountID ? CONST.REPORT.STATUS_NUM.APPROVED : CONST.REPORT.STATUS_NUM.SUBMITTED; From 3a0d6dce027a8a3f49db7a284cde8d61dd786152 Mon Sep 17 00:00:00 2001 From: fahimj Date: Tue, 21 Oct 2025 11:12:50 +0700 Subject: [PATCH 09/16] Add isUnapprove parameter to submitReport function and update test case --- src/libs/actions/IOU.ts | 1 + tests/unit/NextStepUtilsTest.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8eb053f5d99d..0c580cb4ab3d 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -10720,6 +10720,7 @@ function submitReport( currentUserEmailParam: emailParam, hasViolations: hasViolationsParam, isASAPSubmitBetaEnabled, + isUnapprove: true, }); const approvalChain = getApprovalChain(policy, expenseReport); const managerID = getAccountIDsByLogins(approvalChain).at(0); diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index c49eb2bdd6ce..e1cb9d4e607f 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -615,7 +615,7 @@ describe('libs/NextStepUtils', () => { }).then(() => { // TODO: Replace onyx.connect with useOnyx hook (https://github.com/Expensify/App/issues/66365) // eslint-disable-next-line @typescript-eslint/no-deprecated - const result = buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED); + const result = buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED, undefined, true, undefined); expect(result).toMatchObject(optimisticNextStep); }); From d545287d616d6affe65582492d1e9680897707bd Mon Sep 17 00:00:00 2001 From: fahimj Date: Tue, 21 Oct 2025 15:58:14 +0700 Subject: [PATCH 10/16] remove deprecated translateLocal and use useLocalize --- src/libs/ReportUtils.ts | 458 ++++++++++++++++++++++++--------------- src/libs/actions/IOU.ts | 40 ++-- tests/actions/IOUTest.ts | 11 +- 3 files changed, 317 insertions(+), 192 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2a60bce2f51b..f8a140ac10bb 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -23,8 +23,9 @@ import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountI import type {ThemeColors} from '@styles/theme/types'; import type {IOUAction, IOUType, OnboardingAccounting} from '@src/CONST'; import CONST, {TASK_TO_FEATURE} from '@src/CONST'; +import IntlStore from '@src/languages/IntlStore'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; -import type {TranslationPaths} from '@src/languages/types'; +import type {TranslationParameters, TranslationPaths} from '@src/languages/types'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -104,7 +105,6 @@ import getAttachmentDetails from './fileDownload/getAttachmentDetails'; import getBase62ReportID from './getBase62ReportID'; import {isReportMessageAttachment} from './isReportMessageAttachment'; import {formatPhoneNumber} from './LocalePhoneNumber'; -import {translateLocal} from './Localize'; import Log from './Log'; import {isEmailPublicDomain} from './LoginUtils'; // eslint-disable-next-line import/no-cycle @@ -1153,8 +1153,9 @@ Onyx.connect({ }, }); -let hiddenTranslation = ''; -let unavailableTranslation = ''; +// Initialize translation values - these will be set once translations are loaded +let hiddenTranslation = '[hidden]'; +let unavailableTranslation = '[unavailable]'; Onyx.connect({ key: ONYXKEYS.ARE_TRANSLATIONS_LOADING, @@ -1163,8 +1164,14 @@ Onyx.connect({ if (value ?? true) { return; } - hiddenTranslation = translateLocal('common.hidden'); - unavailableTranslation = translateLocal('workspace.common.unavailable'); + // Use IntlStore directly to get translations without deprecated translateLocal + const currentLocale = IntlStore.getCurrentLocale(); + if (currentLocale) { + const hiddenPhrase = IntlStore.get('common.hidden', currentLocale); + const unavailablePhrase = IntlStore.get('workspace.common.unavailable', currentLocale); + hiddenTranslation = typeof hiddenPhrase === 'string' ? hiddenPhrase : 'common.hidden'; + unavailableTranslation = typeof unavailablePhrase === 'string' ? unavailablePhrase : 'workspace.common.unavailable'; + } }, }); @@ -3159,7 +3166,13 @@ const customCollator = new Intl.Collator('en', {usage: 'sort', sensitivity: 'var /** * Returns the report name if the report is a group chat */ -function getGroupChatName(participants?: SelectedParticipant[], shouldApplyLimit = false, report?: OnyxEntry, reportMetadataParam?: OnyxEntry): string | undefined { +function getGroupChatName( + participants?: SelectedParticipant[], + shouldApplyLimit = false, + report?: OnyxEntry, + reportMetadataParam?: OnyxEntry, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +): string | undefined { // If we have a report always try to get the name from the report. if (report?.reportName) { return report.reportName; @@ -3194,7 +3207,7 @@ function getGroupChatName(participants?: SelectedParticipant[], shouldApplyLimit .concat(shouldAddEllipsis ? '...' : ''); } - return translateLocal('groupChat.defaultReportName', {displayName: getDisplayNameForParticipant({accountID: participantAccountIDs.at(0)})}); + return translate?.('groupChat.defaultReportName', {displayName: getDisplayNameForParticipant({accountID: participantAccountIDs.at(0)})}) ?? 'groupChat.defaultReportName'; } function getParticipants(reportID: string) { @@ -3551,6 +3564,7 @@ function getDisplayNamesWithTooltips( localeCompare: LocaleContextProps['localeCompare'], shouldFallbackToHidden = true, shouldAddCurrentUserPostfix = false, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): DisplayNameWithTooltips { const personalDetailsListArray = Array.isArray(personalDetailsList) ? personalDetailsList : Object.values(personalDetailsList); @@ -3564,7 +3578,7 @@ function getDisplayNamesWithTooltips( let pronouns = user?.pronouns ?? undefined; if (pronouns?.startsWith(CONST.PRONOUNS.PREFIX)) { const pronounTranslationKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); - pronouns = translateLocal(`pronouns.${pronounTranslationKey}` as TranslationPaths); + pronouns = translate?.(`pronouns.${pronounTranslationKey}` as TranslationPaths) ?? `pronouns.${pronounTranslationKey}`; } return { @@ -3601,12 +3615,15 @@ function getUserDetailTooltipText(accountID: number, fallbackUserDisplayName = ' * * @param reportAction - The deleted report action of a chat report for which we need to return message. */ -function getDeletedParentActionMessageForChatReport(reportAction: OnyxEntry): string { +function getDeletedParentActionMessageForChatReport( + reportAction: OnyxEntry, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +): string { // By default, let us display [Deleted message] - let deletedMessageText = translateLocal('parentReportAction.deletedMessage'); + let deletedMessageText = translate?.('parentReportAction.deletedMessage') ?? 'parentReportAction.deletedMessage'; if (isCreatedTaskReportAction(reportAction)) { // For canceled task report, let us display [Deleted task] - deletedMessageText = translateLocal('parentReportAction.deletedTask'); + deletedMessageText = translate?.('parentReportAction.deletedTask') ?? 'parentReportAction.deletedTask'; } return deletedMessageText; } @@ -3620,12 +3637,14 @@ function getReimbursementQueuedActionMessage({ shouldUseShortDisplayName = true, reports, personalDetails, + translate, }: { reportAction: OnyxEntry>; reportOrID: OnyxEntry | string | SearchReport; shouldUseShortDisplayName?: boolean; reports?: SearchReport[]; personalDetails?: Partial; + translate?: (path: TPath, ...parameters: TranslationParameters) => string; }): string { const report = typeof reportOrID === 'string' ? getReport(reportOrID, reports ?? allReports) : reportOrID; const submitterDisplayName = getDisplayNameForParticipant({accountID: report?.ownerAccountID, shouldUseShortForm: shouldUseShortDisplayName, personalDetailsData: personalDetails}) ?? ''; @@ -3637,7 +3656,7 @@ function getReimbursementQueuedActionMessage({ messageKey = 'iou.waitingOnBankAccount'; } - return translateLocal(messageKey, {submitterDisplayName}); + return translate?.(messageKey, {submitterDisplayName}) ?? messageKey; } /** @@ -3646,6 +3665,7 @@ function getReimbursementQueuedActionMessage({ function getReimbursementDeQueuedOrCanceledActionMessage( reportAction: OnyxEntry>, reportOrID: OnyxEntry | string | SearchReport, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): string { const report = typeof reportOrID === 'string' ? getReport(reportOrID, allReports) : reportOrID; const originalMessage = getOriginalMessage(reportAction); @@ -3653,10 +3673,10 @@ function getReimbursementDeQueuedOrCanceledActionMessage( const currency = originalMessage?.currency; const formattedAmount = convertToDisplayString(amount, currency); if (originalMessage?.cancellationReason === CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN || originalMessage?.cancellationReason === CONST.REPORT.CANCEL_PAYMENT_REASONS.USER) { - return translateLocal('iou.adminCanceledRequest'); + return translate?.('iou.adminCanceledRequest') ?? 'iou.adminCanceledRequest'; } const submitterDisplayName = getDisplayNameForParticipant({accountID: report?.ownerAccountID, shouldUseShortForm: true}) ?? ''; - return translateLocal('iou.canceledRequest', {submitterDisplayName, amount: formattedAmount}); + return translate?.('iou.canceledRequest', {submitterDisplayName, amount: formattedAmount}) ?? 'iou.canceledRequest'; } /** @@ -3980,7 +4000,15 @@ function getMoneyRequestSpendBreakdown(report: OnyxInputOrEntry, searchR /** * Get the title for a policy expense chat */ -function getPolicyExpenseChatName({report, personalDetailsList}: {report: OnyxEntry; personalDetailsList?: Partial}): string | undefined { +function getPolicyExpenseChatName({ + report, + personalDetailsList, + translate, +}: { + report: OnyxEntry; + personalDetailsList?: Partial; + translate?: (path: TPath, ...parameters: TranslationParameters) => string; +}): string | undefined { const ownerAccountID = report?.ownerAccountID; const personalDetails = ownerAccountID ? personalDetailsList?.[ownerAccountID] : undefined; const login = personalDetails ? personalDetails.login : null; @@ -3988,7 +4016,7 @@ function getPolicyExpenseChatName({report, personalDetailsList}: {report: OnyxEn const reportOwnerDisplayName = getDisplayNameForParticipant({accountID: ownerAccountID, shouldRemoveDomain: true}) || login; if (reportOwnerDisplayName) { - return translateLocal('workspace.common.policyExpenseChatName', {displayName: reportOwnerDisplayName}); + return translate?.('workspace.common.policyExpenseChatName', {displayName: reportOwnerDisplayName}) ?? 'workspace.common.policyExpenseChatName'; } return report?.reportName; @@ -4122,10 +4150,12 @@ function getMoneyRequestReportName({ report, policy, invoiceReceiverPolicy, + translate, }: { report: OnyxEntry; policy?: OnyxEntry | SearchPolicy; invoiceReceiverPolicy?: OnyxEntry | SearchPolicy; + translate?: (path: TPath, ...parameters: TranslationParameters) => string; }): string { if (report?.reportName && isExpenseReport(report)) { return report.reportName; @@ -4145,29 +4175,32 @@ function getMoneyRequestReportName({ payerOrApproverName = getDisplayNameForParticipant({accountID: report?.managerID}) ?? ''; } - const payerPaidAmountMessage = translateLocal('iou.payerPaidAmount', { - payer: payerOrApproverName, - amount: formattedAmount, - }); + const payerPaidAmountMessage = + translate?.('iou.payerPaidAmount', { + payer: payerOrApproverName, + amount: formattedAmount, + }) ?? 'iou.payerPaidAmount'; if (isReportApproved({report})) { - return translateLocal('iou.managerApprovedAmount', { - manager: payerOrApproverName, - amount: formattedAmount, - }); + return ( + translate?.('iou.managerApprovedAmount', { + manager: payerOrApproverName, + amount: formattedAmount, + }) ?? 'iou.managerApprovedAmount' + ); } if (report?.isWaitingOnBankAccount) { - return `${payerPaidAmountMessage} ${CONST.DOT_SEPARATOR} ${translateLocal('iou.pending')}`; + return `${payerPaidAmountMessage} ${CONST.DOT_SEPARATOR} ${translate?.('iou.pending') ?? 'iou.pending'}`; } if (!isSettled(report?.reportID) && hasNonReimbursableTransactions(report?.reportID)) { payerOrApproverName = getDisplayNameForParticipant({accountID: report?.ownerAccountID}) ?? ''; - return translateLocal('iou.payerSpentAmount', {payer: payerOrApproverName, amount: formattedAmount}); + return translate?.('iou.payerSpentAmount', {payer: payerOrApproverName, amount: formattedAmount}) ?? 'iou.payerSpentAmount'; } if (isProcessingReport(report) || isOpenExpenseReport(report) || isOpenInvoiceReport(report) || moneyRequestTotal === 0) { - return translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount}); + return translate?.('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount}) ?? 'iou.payerOwesAmount'; } return payerPaidAmountMessage; @@ -4760,36 +4793,38 @@ function getTransactionReportName({ reportAction, transactions, reports, + translate, }: { reportAction: OnyxEntry; transactions?: SearchTransaction[]; reports?: SearchReport[]; + translate?: (path: TPath, ...parameters: TranslationParameters) => string; }): string { if (isReversedTransaction(reportAction)) { - return translateLocal('parentReportAction.reversedTransaction'); + return translate?.('parentReportAction.reversedTransaction') ?? 'parentReportAction.reversedTransaction'; } if (isDeletedAction(reportAction)) { - return translateLocal('parentReportAction.deletedExpense'); + return translate?.('parentReportAction.deletedExpense') ?? 'parentReportAction.deletedExpense'; } const transaction = getLinkedTransaction(reportAction, transactions); if (isEmptyObject(transaction)) { // Transaction data might be empty on app's first load, if so we fallback to Expense/Track Expense - return isTrackExpenseAction(reportAction) ? translateLocal('iou.createExpense') : translateLocal('iou.expense'); + return isTrackExpenseAction(reportAction) ? (translate?.('iou.createExpense') ?? 'iou.createExpense') : (translate?.('iou.expense') ?? 'iou.expense'); } if (isScanning(transaction)) { - return translateLocal('iou.receiptScanning', {count: 1}); + return translate?.('iou.receiptScanning', {count: 1}) ?? 'iou.receiptScanning'; } if (hasMissingSmartscanFieldsTransactionUtils(transaction)) { - return translateLocal('iou.receiptMissingDetails'); + return translate?.('iou.receiptMissingDetails') ?? 'iou.receiptMissingDetails'; } - if (isFetchingWaypointsFromServer(transaction) && getMerchant(transaction) === translateLocal('iou.fieldPending')) { - return translateLocal('iou.fieldPending'); + if (isFetchingWaypointsFromServer(transaction) && getMerchant(transaction) === (translate?.('iou.fieldPending') ?? 'iou.fieldPending')) { + return translate?.('iou.fieldPending') ?? 'iou.fieldPending'; } if (isSentMoneyReportAction(reportAction)) { @@ -4801,7 +4836,7 @@ function getTransactionReportName({ const formattedAmount = convertToDisplayString(amount, getCurrency(transaction)) ?? ''; const comment = getMerchantOrDescription(transaction); - return translateLocal('iou.threadExpenseReportName', {formattedAmount, comment: Parser.htmlToText(comment)}); + return translate?.('iou.threadExpenseReportName', {formattedAmount, comment: Parser.htmlToText(comment)}) ?? 'iou.threadExpenseReportName'; } /** @@ -4820,6 +4855,7 @@ function getReportPreviewMessage( policy?: OnyxInputOrEntry, isForListPreview = false, originalReportAction: OnyxInputOrEntry = iouReportAction, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): string { const report = typeof reportOrID === 'string' ? getReport(reportOrID, allReports) : reportOrID; const reportActionMessage = getReportActionHtml(iouReportAction); @@ -4845,16 +4881,16 @@ function getReportPreviewMessage( if (!isEmptyObject(linkedTransaction)) { if (isScanning(linkedTransaction)) { - return translateLocal('iou.receiptScanning', {count: 1}); + return translate?.('iou.receiptScanning', {count: 1}) ?? 'iou.receiptScanning'; } if (hasMissingSmartscanFieldsTransactionUtils(linkedTransaction)) { - return translateLocal('iou.receiptMissingDetails'); + return translate?.('iou.receiptMissingDetails') ?? 'iou.receiptMissingDetails'; } const amount = getTransactionAmount(linkedTransaction, !isEmptyObject(report) && isExpenseReport(report), linkedTransaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) ?? 0; const formattedAmount = convertToDisplayString(amount, getCurrency(linkedTransaction)) ?? ''; - return translateLocal('iou.didSplitAmount', {formattedAmount, comment: getMerchantOrDescription(linkedTransaction)}); + return translate?.('iou.didSplitAmount', {formattedAmount, comment: getMerchantOrDescription(linkedTransaction)}) ?? 'iou.didSplitAmount'; } } @@ -4867,16 +4903,16 @@ function getReportPreviewMessage( if (!isEmptyObject(linkedTransaction)) { if (isScanning(linkedTransaction)) { - return translateLocal('iou.receiptScanning', {count: 1}); + return translate?.('iou.receiptScanning', {count: 1}) ?? 'iou.receiptScanning'; } if (hasMissingSmartscanFieldsTransactionUtils(linkedTransaction)) { - return translateLocal('iou.receiptMissingDetails'); + return translate?.('iou.receiptMissingDetails') ?? 'iou.receiptMissingDetails'; } const amount = getTransactionAmount(linkedTransaction, !isEmptyObject(report) && isExpenseReport(report), linkedTransaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) ?? 0; const formattedAmount = convertToDisplayString(amount, getCurrency(linkedTransaction)) ?? ''; - return translateLocal('iou.trackedAmount', {formattedAmount, comment: getMerchantOrDescription(linkedTransaction)}); + return translate?.('iou.trackedAmount', {formattedAmount, comment: getMerchantOrDescription(linkedTransaction)}) ?? 'iou.trackedAmount'; } } @@ -4890,10 +4926,12 @@ function getReportPreviewMessage( const formattedAmount = convertToDisplayString(totalAmount, report.currency); if (isReportApproved({report}) && isPaidGroupPolicy(report)) { - return translateLocal('iou.managerApprovedAmount', { - manager: payerName ?? '', - amount: formattedAmount, - }); + return ( + translate?.('iou.managerApprovedAmount', { + manager: payerName ?? '', + amount: formattedAmount, + }) ?? 'iou.managerApprovedAmount' + ); } const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; @@ -4904,11 +4942,11 @@ function getReportPreviewMessage( } if (!isEmptyObject(linkedTransaction) && isScanning(linkedTransaction)) { - return translateLocal('iou.receiptScanning', {count: numberOfScanningReceipts}); + return translate?.('iou.receiptScanning', {count: numberOfScanningReceipts}) ?? 'iou.receiptScanning'; } if (!isEmptyObject(linkedTransaction) && isFetchingWaypointsFromServer(linkedTransaction) && !getTransactionAmount(linkedTransaction)) { - return translateLocal('iou.fieldPending'); + return translate?.('iou.fieldPending') ?? 'iou.fieldPending'; } const originalMessage = !isEmptyObject(iouReportAction) && isMoneyRequestAction(iouReportAction) ? getOriginalMessage(iouReportAction) : undefined; @@ -4939,16 +4977,18 @@ function getReportPreviewMessage( actualPayerName = actualPayerName && isForListPreview && !isPreviewMessageForParentChatReport ? `${actualPayerName}:` : actualPayerName; const payerDisplayName = isPreviewMessageForParentChatReport ? payerName : actualPayerName; - return translateLocal(translatePhraseKey, { - amount: '', - payer: payerDisplayName ?? '', - last4Digits: reportPolicy?.achAccount?.accountNumber?.slice(-4) ?? '', - }); + return ( + translate?.(translatePhraseKey, { + amount: '', + payer: payerDisplayName ?? '', + last4Digits: reportPolicy?.achAccount?.accountNumber?.slice(-4) ?? '', + }) ?? translatePhraseKey + ); } if (report.isWaitingOnBankAccount) { const submitterDisplayName = getDisplayNameForParticipant({accountID: report.ownerAccountID, shouldUseShortForm: true}) ?? ''; - return translateLocal('iou.waitingOnBankAccount', {submitterDisplayName}); + return translate?.('iou.waitingOnBankAccount', {submitterDisplayName}) ?? 'iou.waitingOnBankAccount'; } const lastActorID = iouReportAction?.actorAccountID; @@ -4976,14 +5016,14 @@ function getReportPreviewMessage( // We only want to show the actor name in the preview if it's not the current user who took the action const requestorName = lastActorID && lastActorID !== currentUserAccountID ? getDisplayNameForParticipant({accountID: lastActorID, shouldUseShortForm: !isPreviewMessageForParentChatReport}) : ''; - return `${requestorName ? `${requestorName}: ` : ''}${translateLocal('iou.expenseAmount', {formattedAmount: amountToDisplay, comment})}`; + return `${requestorName ? `${requestorName}: ` : ''}${translate?.('iou.expenseAmount', {formattedAmount: amountToDisplay, comment}) ?? 'iou.expenseAmount'}`; } if (containsNonReimbursable) { - return translateLocal('iou.payerSpentAmount', {payer: getDisplayNameForParticipant({accountID: report.ownerAccountID}) ?? '', amount: formattedAmount}); + return translate?.('iou.payerSpentAmount', {payer: getDisplayNameForParticipant({accountID: report.ownerAccountID}) ?? '', amount: formattedAmount}) ?? 'iou.payerSpentAmount'; } - return translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); + return translate?.('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}) ?? 'iou.payerOwesAmount'; } /** @@ -4999,6 +5039,7 @@ function getModifiedExpenseOriginalMessage( policy: OnyxInputOrEntry, updatedTransaction?: OnyxInputOrEntry, allowNegative = false, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): OriginalMessageModifiedExpense { const originalMessage: OriginalMessageModifiedExpense = {}; // Remark: Comment field is the only one which has new/old prefixes for the keys (newComment/ oldComment), @@ -5058,14 +5099,22 @@ function getModifiedExpenseOriginalMessage( if ('reimbursable' in transactionChanges) { const oldReimbursable = getReimbursable(oldTransaction); - originalMessage.oldReimbursable = oldReimbursable ? translateLocal('common.reimbursable').toLowerCase() : translateLocal('iou.nonReimbursable').toLowerCase(); - originalMessage.reimbursable = transactionChanges?.reimbursable ? translateLocal('common.reimbursable').toLowerCase() : translateLocal('iou.nonReimbursable').toLowerCase(); + originalMessage.oldReimbursable = oldReimbursable + ? (translate?.('common.reimbursable') ?? 'common.reimbursable').toLowerCase() + : (translate?.('iou.nonReimbursable') ?? 'iou.nonReimbursable').toLowerCase(); + originalMessage.reimbursable = transactionChanges?.reimbursable + ? (translate?.('common.reimbursable') ?? 'common.reimbursable').toLowerCase() + : (translate?.('iou.nonReimbursable') ?? 'iou.nonReimbursable').toLowerCase(); } if ('billable' in transactionChanges) { const oldBillable = getBillable(oldTransaction); - originalMessage.oldBillable = oldBillable ? translateLocal('common.billable').toLowerCase() : translateLocal('common.nonBillable').toLowerCase(); - originalMessage.billable = transactionChanges?.billable ? translateLocal('common.billable').toLowerCase() : translateLocal('common.nonBillable').toLowerCase(); + originalMessage.oldBillable = oldBillable + ? (translate?.('common.billable') ?? 'common.billable').toLowerCase() + : (translate?.('common.nonBillable') ?? 'common.nonBillable').toLowerCase(); + originalMessage.billable = transactionChanges?.billable + ? (translate?.('common.billable') ?? 'common.billable').toLowerCase() + : (translate?.('common.nonBillable') ?? 'common.nonBillable').toLowerCase(); } if ( @@ -5102,15 +5151,19 @@ function isChangeLogObject(originalMessage?: OriginalMessageChangeLog): Original * @param parentReportAction * @param parentReportActionMessage */ -function getAdminRoomInvitedParticipants(parentReportAction: OnyxEntry, parentReportActionMessage: string) { +function getAdminRoomInvitedParticipants( + parentReportAction: OnyxEntry, + parentReportActionMessage: string, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +) { if (isEmptyObject(parentReportAction)) { - return parentReportActionMessage || translateLocal('parentReportAction.deletedMessage'); + return parentReportActionMessage || (translate?.('parentReportAction.deletedMessage') ?? 'parentReportAction.deletedMessage'); } if (!getOriginalMessage(parentReportAction)) { - return parentReportActionMessage || translateLocal('parentReportAction.deletedMessage'); + return parentReportActionMessage || (translate?.('parentReportAction.deletedMessage') ?? 'parentReportAction.deletedMessage'); } if (!isPolicyChangeLogAction(parentReportAction) && !isRoomChangeLogAction(parentReportAction)) { - return parentReportActionMessage || translateLocal('parentReportAction.deletedMessage'); + return parentReportActionMessage || (translate?.('parentReportAction.deletedMessage') ?? 'parentReportAction.deletedMessage'); } const originalMessage = isChangeLogObject(getOriginalMessage(parentReportAction)); @@ -5121,9 +5174,9 @@ function getAdminRoomInvitedParticipants(parentReportAction: OnyxEntry 0) { return name; } - return translateLocal('common.hidden'); + return translate?.('common.hidden') ?? 'common.hidden'; }); - const users = participants.length > 1 ? participants.join(` ${translateLocal('common.and')} `) : participants.at(0); + const users = participants.length > 1 ? participants.join(` ${translate?.('common.and') ?? 'common.and'} `) : participants.at(0); if (!users) { return parentReportActionMessage; } @@ -5133,8 +5186,8 @@ function getAdminRoomInvitedParticipants(parentReportAction: OnyxEntry; reportID?: string; childReportID?: string; reports?: SearchReport[]; personalDetails?: Partial; + translate?: (path: TPath, ...parameters: TranslationParameters) => string; }) { if (isEmptyObject(reportAction)) { return ''; } if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { - return translateLocal('iou.heldExpense'); + return translate?.('iou.heldExpense') ?? 'iou.heldExpense'; } if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION) { @@ -5232,15 +5287,15 @@ function getReportActionMessage({ } if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { - return translateLocal('iou.unheldExpense'); + return translate?.('iou.unheldExpense') ?? 'iou.unheldExpense'; } if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTEDTRANSACTION_THREAD) { - return translateLocal('iou.reject.reportActions.rejectedExpense'); + return translate?.('iou.reject.reportActions.rejectedExpense') ?? 'iou.reject.reportActions.rejectedExpense'; } if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTED_TRANSACTION_MARKASRESOLVED) { - return translateLocal('iou.reject.reportActions.markedAsResolved'); + return translate?.('iou.reject.reportActions.markedAsResolved') ?? 'iou.reject.reportActions.markedAsResolved'; } if (isApprovedOrSubmittedReportAction(reportAction) || isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.REIMBURSED)) { @@ -5253,14 +5308,15 @@ function getReportActionMessage({ shouldUseShortDisplayName: false, reports, personalDetails, + translate, }); } if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.RECEIPT_SCAN_FAILED) { - return translateLocal('iou.receiptScanningFailed'); + return translate?.('iou.receiptScanningFailed') ?? 'iou.receiptScanningFailed'; } if (isReimbursementDeQueuedOrCanceledAction(reportAction)) { - return getReimbursementDeQueuedOrCanceledActionMessage(reportAction, getReportOrDraftReport(reportID, reports)); + return getReimbursementDeQueuedOrCanceledActionMessage(reportAction, getReportOrDraftReport(reportID, reports), translate); } return parseReportActionHtmlToText(reportAction, reportID, childReportID); @@ -5344,6 +5400,7 @@ function getReportName( isReportArchived?: boolean, reports?: SearchReport[], policies?: SearchPolicy[], + translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): string { // Check if we can use report name in derived values - only when we have report but no other params const canUseDerivedValue = @@ -5372,16 +5429,16 @@ function getReportName( ) { const harvesting = !isMarkAsClosedAction(parentReportAction) ? (getOriginalMessage(parentReportAction)?.harvesting ?? false) : false; if (harvesting) { - return translateLocal('iou.automaticallySubmitted'); + return translate?.('iou.automaticallySubmitted') ?? 'iou.automaticallySubmitted'; } - return translateLocal('iou.submitted', {memo: getOriginalMessage(parentReportAction)?.message}); + return translate?.('iou.submitted', {memo: getOriginalMessage(parentReportAction)?.message}) ?? 'iou.submitted'; } if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.FORWARDED)) { const {automaticAction} = getOriginalMessage(parentReportAction) ?? {}; if (automaticAction) { - return translateLocal('iou.automaticallyForwarded'); + return translate?.('iou.automaticallyForwarded') ?? 'iou.automaticallyForwarded'; } - return translateLocal('iou.forwarded'); + return translate?.('iou.forwarded') ?? 'iou.forwarded'; } if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTED) { return getRejectedReportMessage(); @@ -5405,7 +5462,7 @@ function getReportName( return getWorkspaceUpdateFieldMessage(parentReportAction); } if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.MERGED_WITH_CASH_TRANSACTION) { - return translateLocal('systemMessage.mergedWithCashTransaction'); + return translate?.('systemMessage.mergedWithCashTransaction') ?? 'systemMessage.mergedWithCashTransaction'; } if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_NAME) { return Str.htmlDecode(getWorkspaceNameUpdatedMessage(parentReportAction)); @@ -5446,7 +5503,7 @@ function getReportName( } if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED)) { - return translateLocal('iou.paidElsewhere'); + return translate?.('iou.paidElsewhere') ?? 'iou.paidElsewhere'; } if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY)) { @@ -5472,19 +5529,19 @@ function getReportName( if (originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { if (originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { - return translateLocal('iou.paidElsewhere'); + return translate?.('iou.paidElsewhere') ?? 'iou.paidElsewhere'; } if (originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.VBBA) { if (originalMessage.automaticAction) { - return translateLocal('iou.automaticallyPaidWithBusinessBankAccount', {last4Digits}); + return translate?.('iou.automaticallyPaidWithBusinessBankAccount', {last4Digits}) ?? 'iou.automaticallyPaidWithBusinessBankAccount'; } - return translateLocal('iou.businessBankAccount', {last4Digits}); + return translate?.('iou.businessBankAccount', {last4Digits}) ?? 'iou.businessBankAccount'; } if (originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { if (originalMessage.automaticAction) { - return translateLocal('iou.automaticallyPaidWithExpensify'); + return translate?.('iou.automaticallyPaidWithExpensify') ?? 'iou.automaticallyPaidWithExpensify'; } - return translateLocal('iou.paidWithExpensify'); + return translate?.('iou.paidWithExpensify') ?? 'iou.paidWithExpensify'; } } } @@ -5492,12 +5549,12 @@ function getReportName( if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.APPROVED)) { const {automaticAction} = getOriginalMessage(parentReportAction) ?? {}; if (automaticAction) { - return translateLocal('iou.automaticallyApproved'); + return translate?.('iou.automaticallyApproved') ?? 'iou.automaticallyApproved'; } - return translateLocal('iou.approvedMessage'); + return translate?.('iou.approvedMessage') ?? 'iou.approvedMessage'; } if (isUnapprovedAction(parentReportAction)) { - return translateLocal('iou.unapproved'); + return translate?.('iou.unapproved') ?? 'iou.unapproved'; } if (isActionableJoinRequest(parentReportAction)) { @@ -5505,7 +5562,7 @@ function getReportName( } if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { - return translateLocal('parentReportAction.deletedTask'); + return translate?.('parentReportAction.deletedTask') ?? 'parentReportAction.deletedTask'; } if (isTaskReport(report)) { @@ -5539,11 +5596,11 @@ function getReportName( } if (parentReportActionMessage?.isDeletedParentAction) { - return translateLocal('parentReportAction.deletedMessage'); + return translate?.('parentReportAction.deletedMessage') ?? 'parentReportAction.deletedMessage'; } if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RESOLVED_DUPLICATES) { - return translateLocal('violations.resolvedDuplicates'); + return translate?.('violations.resolvedDuplicates') ?? 'violations.resolvedDuplicates'; } const isAttachment = isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : undefined); @@ -5555,14 +5612,14 @@ function getReportName( personalDetails, }).replace(/(\n+|\r\n|\n|\r)/gm, ' '); if (isAttachment && reportActionMessage) { - return `[${translateLocal('common.attachment')}]`; + return `[${translate?.('common.attachment') ?? 'common.attachment'}]`; } if ( parentReportActionMessage?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || parentReportActionMessage?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN || parentReportActionMessage?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE ) { - return translateLocal('parentReportAction.hiddenMessage'); + return translate?.('parentReportAction.hiddenMessage') ?? 'parentReportAction.hiddenMessage'; } if (isAdminRoom(report) || isUserCreatedPolicyRoom(report)) { return getAdminRoomInvitedParticipants(parentReportAction, reportActionMessage); @@ -5594,7 +5651,7 @@ function getReportName( } if (isClosedExpenseReportWithNoExpenses(report, transactions)) { - return translateLocal('parentReportAction.deletedReport'); + return translate?.('parentReportAction.deletedReport') ?? 'parentReportAction.deletedReport'; } if (isGroupChat(report)) { @@ -5666,8 +5723,8 @@ function getInvoiceReportName(report: OnyxEntry, policy?: OnyxEntry(path: TPath, ...parameters: TranslationParameters) => string): string { + return `${reportName} (${translate?.('common.archived') ?? 'common.archived'}) `; } /** @@ -5708,18 +5765,23 @@ function getReportSubtitlePrefix(report: OnyxEntry): string { /** * Get either the policyName or domainName the chat is tied to */ -function getChatRoomSubtitle(report: OnyxEntry, isPolicyNamePreferred = false, isReportArchived = false): string | undefined { +function getChatRoomSubtitle( + report: OnyxEntry, + isPolicyNamePreferred = false, + isReportArchived = false, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +): string | undefined { if (isChatThread(report)) { return ''; } if (isSelfDM(report)) { - return translateLocal('reportActionsView.yourSpace'); + return translate?.('reportActionsView.yourSpace') ?? 'reportActionsView.yourSpace'; } if (isInvoiceRoom(report)) { - return translateLocal('workspace.common.invoices'); + return translate?.('workspace.common.invoices') ?? 'workspace.common.invoices'; } if (isConciergeChatReport(report)) { - return translateLocal('reportActionsView.conciergeSupport'); + return translate?.('reportActionsView.conciergeSupport') ?? 'reportActionsView.conciergeSupport'; } if (!isDefaultRoom(report) && !isUserCreatedPolicyRoom(report) && !isPolicyExpenseChat(report)) { return ''; @@ -5738,7 +5800,7 @@ function getChatRoomSubtitle(report: OnyxEntry, isPolicyNamePreferred = return getPolicyName({report}); } - return `${getReportSubtitlePrefix(report)}${translateLocal('iou.submitsTo', {name: subtitle ?? ''})}`; + return `${getReportSubtitlePrefix(report)}${translate?.('iou.submitsTo', {name: subtitle ?? ''}) ?? 'iou.submitsTo'}`; } if (isReportArchived) { @@ -5758,7 +5820,12 @@ function getPendingChatMembers(accountIDs: number[], previousPendingChatMembers: /** * Gets the parent navigation subtitle for the report */ -function getParentNavigationSubtitle(report: OnyxEntry, isParentReportArchived = false, reportAttributes?: ReportAttributesDerivedValue['reports']): ParentNavigationSummaryParams { +function getParentNavigationSubtitle( + report: OnyxEntry, + isParentReportArchived = false, + reportAttributes?: ReportAttributesDerivedValue['reports'], + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +): ParentNavigationSummaryParams { const parentReport = getParentReport(report); if (report?.hasParentAccess === false && !isReportManager(report)) { return {}; @@ -5773,7 +5840,7 @@ function getParentNavigationSubtitle(report: OnyxEntry, isParentReportAr if (isExpenseReport(report)) { return { - reportName: translateLocal('workspace.common.policyExpenseChatName', {displayName: reportOwnerDisplayName ?? ''}), + reportName: translate?.('workspace.common.policyExpenseChatName', {displayName: reportOwnerDisplayName ?? ''}) ?? 'workspace.common.policyExpenseChatName', workspaceName: getPolicyName({report}), }; } @@ -5787,7 +5854,7 @@ function getParentNavigationSubtitle(report: OnyxEntry, isParentReportAr let reportName = `${getPolicyName({report: parentReport})} & ${getInvoicePayerName(parentReport)}`; if (isArchivedNonExpenseReport(parentReport, isParentReportArchived)) { - reportName += ` (${translateLocal('common.archived')})`; + reportName += ` (${translate?.('common.archived') ?? 'common.archived'})`; } return { @@ -6251,10 +6318,16 @@ function getHumanReadableStatus(statusNum: number): string { * If after all replacements the formula is empty, the original formula is returned. * See {@link https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates} */ -function populateOptimisticReportFormula(formula: string, report: OptimisticExpenseReport | OptimisticNewReport, policy: OnyxEntry, isMoneyRequestConfirmation = false): string { +function populateOptimisticReportFormula( + formula: string, + report: OptimisticExpenseReport | OptimisticNewReport, + policy: OnyxEntry, + isMoneyRequestConfirmation = false, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +): string { // If this is a newly created report and it is from money request confirmation, we should use 'New report' as the report title if (!report.parentReportActionID && isMoneyRequestConfirmation) { - return translateLocal('iou.newReport'); + return translate?.('iou.newReport') ?? 'iou.newReport'; } const createdDate = report.lastVisibleActionCreated ? new Date(report.lastVisibleActionCreated) : undefined; @@ -6466,52 +6539,59 @@ function buildOptimisticEmptyReport(reportID: string, accountID: number, parentR return optimisticEmptyReport; } -function getRejectedReportMessage() { - return translateLocal('iou.rejectedThisReport'); +function getRejectedReportMessage(translate?: (path: TPath, ...parameters: TranslationParameters) => string) { + return translate?.('iou.rejectedThisReport') ?? 'iou.rejectedThisReport'; } -function getUpgradeWorkspaceMessage() { - return translateLocal('workspaceActions.upgradedWorkspace'); +function getUpgradeWorkspaceMessage(translate?: (path: TPath, ...parameters: TranslationParameters) => string) { + return translate?.('workspaceActions.upgradedWorkspace') ?? 'workspaceActions.upgradedWorkspace'; } -function getDowngradeWorkspaceMessage() { - return translateLocal('workspaceActions.downgradedWorkspace'); +function getDowngradeWorkspaceMessage(translate?: (path: TPath, ...parameters: TranslationParameters) => string) { + return translate?.('workspaceActions.downgradedWorkspace') ?? 'workspaceActions.downgradedWorkspace'; } -function getWorkspaceNameUpdatedMessage(action: ReportAction) { +function getWorkspaceNameUpdatedMessage(action: ReportAction, translate?: (path: TPath, ...parameters: TranslationParameters) => string) { const {oldName, newName} = getOriginalMessage(action as ReportAction) ?? {}; - const message = oldName && newName ? translateLocal('workspaceActions.renamedWorkspaceNameAction', {oldName, newName}) : getReportActionText(action); + const message = + oldName && newName ? (translate?.('workspaceActions.renamedWorkspaceNameAction', {oldName, newName}) ?? 'workspaceActions.renamedWorkspaceNameAction') : getReportActionText(action); return Str.htmlEncode(message); } -function getDeletedTransactionMessage(action: ReportAction) { +function getDeletedTransactionMessage(action: ReportAction, translate?: (path: TPath, ...parameters: TranslationParameters) => string) { const deletedTransactionOriginalMessage = getOriginalMessage(action as ReportAction) ?? {}; const amount = -(deletedTransactionOriginalMessage.amount ?? 0); const currency = deletedTransactionOriginalMessage.currency ?? ''; const formattedAmount = convertToDisplayString(amount, currency) ?? ''; - const message = translateLocal('iou.deletedTransaction', { - amount: formattedAmount, - merchant: deletedTransactionOriginalMessage.merchant ?? '', - }); + const message = + translate?.('iou.deletedTransaction', { + amount: formattedAmount, + merchant: deletedTransactionOriginalMessage.merchant ?? '', + }) ?? 'iou.deletedTransaction'; return message; } -function getMovedTransactionMessage(report: OnyxEntry) { +function getMovedTransactionMessage(report: OnyxEntry, translate?: (path: TPath, ...parameters: TranslationParameters) => string) { const reportName = getReportName(report) ?? report?.reportName ?? ''; const reportUrl = getReportURLForCurrentContext(report?.reportID); - return translateLocal('iou.movedTransaction', {reportUrl, reportName}); + return translate?.('iou.movedTransaction', {reportUrl, reportName}) ?? 'iou.movedTransaction'; } -function getUnreportedTransactionMessage() { +function getUnreportedTransactionMessage(translate?: (path: TPath, ...parameters: TranslationParameters) => string) { const selfDMReportID = findSelfDMReportID(); const reportUrl = `${environmentURL}/r/${selfDMReportID}`; - const message = translateLocal('iou.unreportedTransaction', { - reportUrl, - }); + const message = + translate?.('iou.unreportedTransaction', { + reportUrl, + }) ?? 'iou.unreportedTransaction'; return message; } -function getMovedActionMessage(action: ReportAction, report: OnyxEntry) { +function getMovedActionMessage( + action: ReportAction, + report: OnyxEntry, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +) { if (!isMovedAction(action)) { return ''; } @@ -6522,21 +6602,24 @@ function getMovedActionMessage(action: ReportAction, report: OnyxEntry) } const {toPolicyID, newParentReportID, movedReportID} = movedActionOriginalMessage; const toPolicyName = getPolicyNameByID(toPolicyID); - return translateLocal('iou.movedAction', { - shouldHideMovedReportUrl: !isDM(report), - movedReportUrl: getReportURLForCurrentContext(movedReportID), - newParentReportUrl: getReportURLForCurrentContext(newParentReportID), - toPolicyName, - }); + return ( + translate?.('iou.movedAction', { + shouldHideMovedReportUrl: !isDM(report), + movedReportUrl: getReportURLForCurrentContext(movedReportID), + newParentReportUrl: getReportURLForCurrentContext(newParentReportID), + toPolicyName, + }) ?? 'iou.movedAction' + ); } -function getPolicyChangeMessage(action: ReportAction) { +function getPolicyChangeMessage(action: ReportAction, translate?: (path: TPath, ...parameters: TranslationParameters) => string) { const PolicyChangeOriginalMessage = getOriginalMessage(action as ReportAction) ?? {}; const {fromPolicy: fromPolicyID, toPolicy: toPolicyID} = PolicyChangeOriginalMessage as OriginalMessageChangePolicy; - const message = translateLocal('report.actions.type.changeReportPolicy', { - fromPolicyName: fromPolicyID ? getPolicyNameByID(fromPolicyID) : undefined, - toPolicyName: getPolicyNameByID(toPolicyID), - }); + const message = + translate?.('report.actions.type.changeReportPolicy', { + fromPolicyName: fromPolicyID ? getPolicyNameByID(fromPolicyID) : undefined, + toPolicyName: getPolicyNameByID(toPolicyID), + }) ?? 'report.actions.type.changeReportPolicy'; return message; } @@ -6561,6 +6644,7 @@ function getIOUReportActionMessage( isSettlingUp = false, bankAccountID?: number | undefined, payAsBusiness = false, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): Message[] { const report = getReportOrDraftReport(iouReportID); const isInvoice = isInvoiceReport(report); @@ -6607,14 +6691,15 @@ function getIOUReportActionMessage( if (isInvoice && isSettlingUp) { iouMessage = paymentType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE - ? translateLocal('iou.payElsewhere', {formattedAmount: amount}) - : translateLocal(payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal', {amount, last4Digits: String(bankAccountID).slice(-4)}); + ? (translate?.('iou.payElsewhere', {formattedAmount: amount}) ?? 'iou.payElsewhere') + : (translate?.(payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal', {amount, last4Digits: String(bankAccountID).slice(-4)}) ?? + (payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal')); } else { iouMessage = isSettlingUp ? `paid ${amount}${paymentMethodMessage}` : `sent ${amount}${comment && ` for ${comment}`}${paymentMethodMessage}`; } break; case CONST.REPORT.ACTIONS.TYPE.SUBMITTED: - iouMessage = translateLocal('iou.expenseAmount', {formattedAmount: amount}); + iouMessage = translate?.('iou.expenseAmount', {formattedAmount: amount}) ?? 'iou.expenseAmount'; break; default: break; @@ -7528,7 +7613,10 @@ function buildOptimisticRoomDescriptionUpdatedReportAction(description: string): * Returns the necessary reportAction onyx data to indicate that the transaction has been put on hold optimistically * @param [created] - Action created time */ -function buildOptimisticHoldReportAction(created = DateUtils.getDBTime()): OptimisticHoldReportAction { +function buildOptimisticHoldReportAction( + created = DateUtils.getDBTime(), + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +): OptimisticHoldReportAction { return { reportActionID: rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.HOLD, @@ -7538,7 +7626,7 @@ function buildOptimisticHoldReportAction(created = DateUtils.getDBTime()): Optim { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: translateLocal('iou.heldExpense'), + text: translate?.('iou.heldExpense') ?? 'iou.heldExpense', }, ], person: [ @@ -7590,7 +7678,10 @@ function buildOptimisticHoldReportActionComment(comment: string, created = DateU * Returns the necessary reportAction onyx data to indicate that the transaction has been removed from hold optimistically * @param [created] - Action created time */ -function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): OptimisticHoldReportAction { +function buildOptimisticUnHoldReportAction( + created = DateUtils.getDBTime(), + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +): OptimisticHoldReportAction { return { reportActionID: rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.UNHOLD, @@ -7600,7 +7691,7 @@ function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): Opt { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: translateLocal('iou.unheldExpense'), + text: translate?.('iou.unheldExpense') ?? 'iou.unheldExpense', }, ], person: [ @@ -7847,7 +7938,9 @@ function buildOptimisticDismissedViolationReportAction( }; } -function buildOptimisticResolvedDuplicatesReportAction(): OptimisticDismissedViolationReportAction { +function buildOptimisticResolvedDuplicatesReportAction( + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +): OptimisticDismissedViolationReportAction { return { actionName: CONST.REPORT.ACTIONS.TYPE.RESOLVED_DUPLICATES, actorAccountID: currentUserAccountID, @@ -7857,7 +7950,7 @@ function buildOptimisticResolvedDuplicatesReportAction(): OptimisticDismissedVio { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: translateLocal('violations.resolvedDuplicates'), + text: translate?.('violations.resolvedDuplicates') ?? 'violations.resolvedDuplicates', }, ], pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, @@ -8620,6 +8713,7 @@ function hasReportErrorsOtherThanFailedReceipt( doesReportHaveViolations: boolean, transactionViolations: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], + translate?: (path: TPath, ...parameters: TranslationParameters) => string, ) { const allReportErrors = reportAttributes?.[report?.reportID]?.reportErrors ?? {}; const transactionReportActions = getAllReportActions(report.reportID); @@ -8632,7 +8726,7 @@ function hasReportErrorsOtherThanFailedReceipt( return ( doesTransactionThreadReportHasViolations || doesReportHaveViolations || - Object.values(allReportErrors).some((error) => error?.[0] !== translateLocal('iou.error.genericSmartscanFailureMessage')) + Object.values(allReportErrors).some((error) => error?.[0] !== (translate?.('iou.error.genericSmartscanFailureMessage') ?? 'iou.error.genericSmartscanFailureMessage')) ); } @@ -9269,12 +9363,15 @@ function isCurrentUserTheOnlyParticipant(participantAccountIDs?: number[]): bool * Returns display names for those that can see the whisper. * However, it returns "you" if the current user is the only one who can see it besides the person that sent it. */ -function getWhisperDisplayNames(participantAccountIDs?: number[]): string | undefined { +function getWhisperDisplayNames( + participantAccountIDs?: number[], + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +): string | undefined { const isWhisperOnlyVisibleToCurrentUser = isCurrentUserTheOnlyParticipant(participantAccountIDs); // When the current user is the only participant, the display name needs to be "you" because that's the only person reading it if (isWhisperOnlyVisibleToCurrentUser) { - return translateLocal('common.youAfterPreposition'); + return translate?.('common.youAfterPreposition') ?? 'common.youAfterPreposition'; } return participantAccountIDs?.map((accountID) => getDisplayNameForParticipant({accountID, shouldUseShortForm: !isWhisperOnlyVisibleToCurrentUser})).join(', '); @@ -9658,7 +9755,12 @@ function getTaskAssigneeChatOnyxData( /** * Return iou report action display message */ -function getIOUReportActionDisplayMessage(reportAction: OnyxEntry, transaction?: OnyxEntry, report?: Report): string { +function getIOUReportActionDisplayMessage( + reportAction: OnyxEntry, + transaction?: OnyxEntry, + report?: Report, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +): string { if (!isMoneyRequestAction(reportAction)) { return ''; } @@ -9679,7 +9781,10 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry, case CONST.IOU.PAYMENT_TYPE.EXPENSIFY: case CONST.IOU.PAYMENT_TYPE.VBBA: if (isInvoice) { - return translateLocal(payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal', {amount: '', last4Digits}); + return ( + translate?.(payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal', {amount: '', last4Digits}) ?? + (payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal') + ); } translationKey = 'iou.businessBankAccount'; @@ -9696,7 +9801,7 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry, break; } - return translateLocal(translationKey, {amount: '', payer: '', last4Digits}); + return translate?.(translationKey, {amount: '', payer: '', last4Digits}) ?? translationKey; } const amount = getTransactionAmount(transaction, !isEmptyObject(iouReport) && isExpenseReport(iouReport), transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) ?? 0; @@ -9704,14 +9809,18 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry, const isRequestSettled = isSettled(IOUReportID); const isApproved = isReportApproved({report: iouReport}); if (isRequestSettled) { - return translateLocal('iou.payerSettled', { - amount: formattedAmount, - }); + return ( + translate?.('iou.payerSettled', { + amount: formattedAmount, + }) ?? 'iou.payerSettled' + ); } if (isApproved) { - return translateLocal('iou.approvedAmount', { - amount: formattedAmount, - }); + return ( + translate?.('iou.approvedAmount', { + amount: formattedAmount, + }) ?? 'iou.approvedAmount' + ); } if (isSplitBillReportAction(reportAction)) { translationKey = 'iou.didSplitAmount'; @@ -9720,10 +9829,12 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry, } else { translationKey = 'iou.expenseAmount'; } - return translateLocal(translationKey, { - formattedAmount, - comment: getMerchantOrDescription(transaction), - }); + return ( + translate?.(translationKey, { + formattedAmount, + comment: getMerchantOrDescription(transaction), + }) ?? translationKey + ); } /** @@ -10760,8 +10871,7 @@ function prepareOnboardingOnyxData( }); // Sign-off welcome message - const welcomeSignOffText = - engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ? translateLocal('onboarding.welcomeSignOffTitleManageTeam') : translateLocal('onboarding.welcomeSignOffTitle'); + const welcomeSignOffText = engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ? 'onboarding.welcomeSignOffTitleManageTeam' : 'onboarding.welcomeSignOffTitle'; const welcomeSignOffComment = buildOptimisticAddCommentReportAction(welcomeSignOffText, undefined, actorAccountID, tasksData.length + 3); const welcomeSignOffCommentAction: OptimisticAddCommentReportAction = welcomeSignOffComment.reportAction; const welcomeSignOffMessage = { @@ -11259,7 +11369,7 @@ function getFieldViolationTranslation(reportField: PolicyReportField, violation? switch (violation) { case 'fieldRequired': - return translateLocal('reportViolations.fieldRequired', {fieldName: reportField.name}); + return 'reportViolations.fieldRequired'; default: return ''; } @@ -11783,7 +11893,7 @@ function hasReportBeenRetracted(report: OnyxEntry, reportActions?: OnyxE function getMoneyReportPreviewName(action: ReportAction, iouReport: OnyxEntry, isInvoice?: boolean, reportAttributes?: ReportAttributesDerivedValue['reports']) { if (isInvoice && isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW)) { const originalMessage = getOriginalMessage(action); - return originalMessage && translateLocal('iou.invoiceReportName', originalMessage); + return originalMessage && 'iou.invoiceReportName'; } return getReportName(iouReport, undefined, undefined, undefined, undefined, reportAttributes) || action.childReportName; } @@ -11818,7 +11928,7 @@ function buildOptimisticRejectReportAction(created = DateUtils.getDBTime()): Opt { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: translateLocal('iou.reject.reportActions.rejectedExpense'), + text: 'iou.reject.reportActions.rejectedExpense', }, ], person: [ @@ -11880,7 +11990,7 @@ function buildOptimisticMarkedAsResolvedReportAction(created = DateUtils.getDBTi { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: translateLocal('iou.reject.reportActions.markedAsResolved'), + text: 'iou.reject.reportActions.markedAsResolved', }, ], person: [ @@ -11919,23 +12029,23 @@ function getReportStatusTranslation(stateNum?: number, statusNum?: number): stri } if (stateNum === CONST.REPORT.STATE_NUM.OPEN && statusNum === CONST.REPORT.STATUS_NUM.OPEN) { - return translateLocal('common.draft'); + return 'common.draft'; } if (stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED) { - return translateLocal('common.outstanding'); + return 'common.outstanding'; } if (stateNum === CONST.REPORT.STATE_NUM.APPROVED && statusNum === CONST.REPORT.STATUS_NUM.CLOSED) { - return translateLocal('common.done'); + return 'common.done'; } if (stateNum === CONST.REPORT.STATE_NUM.APPROVED && statusNum === CONST.REPORT.STATUS_NUM.APPROVED) { - return translateLocal('iou.approved'); + return 'iou.approved'; } if ( (stateNum === CONST.REPORT.STATE_NUM.APPROVED && statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED) || (stateNum === CONST.REPORT.STATE_NUM.BILLING && statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED) || (stateNum === CONST.REPORT.STATE_NUM.AUTOREIMBURSED && statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED) ) { - return translateLocal('iou.settledExpensify'); + return 'iou.settledExpensify'; } return ''; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c7b0edd6d3b3..3b4dd1943994 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -237,6 +237,7 @@ import { import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import type {IOUAction, IOUActionParams, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; +import type {TranslationParameters, TranslationPaths} from '@src/languages/types'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -1464,7 +1465,10 @@ type BuildOnyxDataForTestDriveIOUParams = { testDriveCommentReportActionID?: string; }; -function buildOnyxDataForTestDriveIOU(testDriveIOUParams: BuildOnyxDataForTestDriveIOUParams): OnyxData { +function buildOnyxDataForTestDriveIOU( + testDriveIOUParams: BuildOnyxDataForTestDriveIOUParams, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +): OnyxData { const optimisticData: OnyxUpdate[] = []; const successData: OnyxUpdate[] = []; const failureData: OnyxUpdate[] = []; @@ -1481,7 +1485,7 @@ function buildOnyxDataForTestDriveIOU(testDriveIOUParams: BuildOnyxDataForTestDr reportActionID: testDriveIOUParams.iouOptimisticParams.action.reportActionID, }); - const text = Localize.translateLocal('testDrive.employeeInviteMessage', {name: personalDetailsList?.[userAccountID]?.firstName ?? ''}); + const text = translate?.('testDrive.employeeInviteMessage', {name: personalDetailsList?.[userAccountID]?.firstName ?? ''}) ?? 'testDrive.employeeInviteMessage'; const textComment = buildOptimisticAddCommentReportAction(text, undefined, userAccountID, undefined, undefined, undefined, testDriveIOUParams.testDriveCommentReportActionID); textComment.reportAction.created = DateUtils.subtractMillisecondsFromDateTime(testDriveIOUParams.iouOptimisticParams.createdAction.created, 1); @@ -4220,7 +4224,10 @@ type GetUpdateMoneyRequestParamsType = { shouldBuildOptimisticModifiedExpenseReportAction?: boolean; }; -function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): UpdateMoneyRequestData { +function getUpdateMoneyRequestParams( + params: GetUpdateMoneyRequestParamsType, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, +): UpdateMoneyRequestData { const { transactionID, transactionThreadReportID, @@ -4242,7 +4249,9 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U // Step 1: Set any "pending fields" (ones updated while the user was offline) to have error messages in the failureData const pendingFields: OnyxTypes.Transaction['pendingFields'] = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE])); const clearedPendingFields = getClearedPendingFields(transactionChanges); - const errorFields = Object.fromEntries(Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericEditFailureMessage')}])); + const errorFields = Object.fromEntries( + Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: translate?.('iou.error.genericEditFailureMessage') ?? 'iou.error.genericEditFailureMessage'}]), + ); // Step 2: Get all the collections being updated const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; @@ -4696,6 +4705,7 @@ function getUpdateTrackExpenseParams( transactionChanges: TransactionChanges, policy: OnyxEntry, shouldBuildOptimisticModifiedExpenseReportAction = true, + translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): UpdateMoneyRequestData { const optimisticData: OnyxUpdate[] = []; const successData: OnyxUpdate[] = []; @@ -4704,7 +4714,9 @@ function getUpdateTrackExpenseParams( // Step 1: Set any "pending fields" (ones updated while the user was offline) to have error messages in the failureData const pendingFields = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE])); const clearedPendingFields = getClearedPendingFields(transactionChanges); - const errorFields = Object.fromEntries(Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericEditFailureMessage')}])); + const errorFields = Object.fromEntries( + Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: translate?.('iou.error.genericEditFailureMessage') ?? 'iou.error.genericEditFailureMessage'}]), + ); // Step 2: Get all the collections being updated const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; @@ -6609,7 +6621,8 @@ function createSplitsAndOnyxData({ attendees, }, policyRecentlyUsedCategories, -}: CreateSplitsAndOnyxDataParams): SplitsAndOnyxData { + translate, +}: CreateSplitsAndOnyxDataParams & {translate?: (path: TPath, ...parameters: TranslationParameters) => string}): SplitsAndOnyxData { const currentUserEmailForIOUSplit = addSMSDomainIfPhoneNumber(currentUserLogin); const participantAccountIDs = participants.map((participant) => Number(participant.accountID)); @@ -6629,7 +6642,7 @@ function createSplitsAndOnyxData({ reportID: CONST.REPORT.SPLIT_REPORT_ID, comment, created, - merchant: merchant || Localize.translateLocal('iou.expense'), + merchant: merchant || (translate?.('iou.expense') ?? 'iou.expense'), receipt, category, tag, @@ -6913,7 +6926,7 @@ function createSplitsAndOnyxData({ reportID: oneOnOneIOUReport.reportID, comment, created, - merchant: merchant || Localize.translateLocal('iou.expense'), + merchant: merchant || (translate?.('iou.expense') ?? 'iou.expense'), category, tag, taxCode, @@ -8307,10 +8320,8 @@ function prepareToCleanUpMoneyRequest( } const hasNonReimbursableTransactions = hasNonReimbursableTransactionsReportUtils(iouReport?.reportID); - const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { - payer: getPersonalDetailsForAccountID(updatedIOUReport?.managerID ?? CONST.DEFAULT_NUMBER_ID).login ?? '', - amount: convertToDisplayString(updatedIOUReport?.total, updatedIOUReport?.currency), - }); + const messageKey = hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount'; + const messageText = messageKey; // TODO: Add translate parameter when calling this function if (getReportActionMessage(updatedReportPreviewAction)) { if (Array.isArray(updatedReportPreviewAction?.message)) { @@ -8598,6 +8609,7 @@ function deleteMoneyRequest( isSingleTransactionView = false, transactionIDsPendingDeletion?: string[], selectedTransactionIDs?: string[], + translate?: (path: TPath, ...parameters: TranslationParameters) => string, ) { if (!transactionID) { return; @@ -8818,7 +8830,7 @@ function deleteMoneyRequest( ...reportAction, pendingAction: null, errors: { - [errorKey]: Localize.translateLocal('iou.error.genericDeleteFailureMessage'), + [errorKey]: translate?.('iou.error.genericDeleteFailureMessage') ?? 'iou.error.genericDeleteFailureMessage', }, }, }, @@ -8845,7 +8857,7 @@ function deleteMoneyRequest( ...reportPreviewAction, pendingAction: null, errors: { - [errorKey]: Localize.translateLocal('iou.error.genericDeleteFailureMessage'), + [errorKey]: translate?.('iou.error.genericDeleteFailureMessage') ?? 'iou.error.genericDeleteFailureMessage', }, }, }, diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index a9c85a9bc59d..9b64df0c908b 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -8,6 +8,7 @@ import type {OnyxCollection, OnyxEntry, OnyxInputValue} from 'react-native-onyx' import Onyx from 'react-native-onyx'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; import type {SearchQueryJSON, SearchStatus} from '@components/Search/types'; +import useLocalize from '@hooks/useLocalize'; import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; import type {PerDiemExpenseTransactionParams, RequestMoneyParticipantParams} from '@libs/actions/IOU'; import { @@ -59,7 +60,6 @@ import {subscribeToUserEvents} from '@libs/actions/User'; import type {ApiCommand} from '@libs/API/types'; import {WRITE_COMMANDS} from '@libs/API/types'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; -import {translateLocal} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import {rand64} from '@libs/NumberUtils'; import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; @@ -1642,7 +1642,8 @@ describe('actions/IOU', () => { Onyx.disconnect(connection); expect(transaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(transaction?.errors).toBeTruthy(); - expect(Object.values(transaction?.errors ?? {}).at(0)).toEqual(translateLocal('iou.error.genericCreateFailureMessage')); + const {result} = renderHook(() => useLocalize()); + expect(Object.values(transaction?.errors ?? {}).at(0)).toEqual(result.current.translate('iou.error.genericCreateFailureMessage')); resolve(); }, }); @@ -1944,7 +1945,8 @@ describe('actions/IOU', () => { const accountantEmployee = policyData?.employeeList?.[accountant.email]; expect(accountantEmployee).toBeTruthy(); expect(accountantEmployee?.errors).toBeTruthy(); - expect(Object.values(accountantEmployee?.errors ?? {}).at(0)).toEqual(translateLocal('workspace.people.error.genericAdd')); + const {result} = renderHook(() => useLocalize()); + expect(Object.values(accountantEmployee?.errors ?? {}).at(0)).toEqual(result.current.translate('workspace.people.error.genericAdd')); // Cleanup mockFetch?.succeed?.(); @@ -3460,7 +3462,8 @@ describe('actions/IOU', () => { callback: (allActions) => { Onyx.disconnect(connection); const erroredAction = Object.values(allActions ?? {}).find((action) => !isEmptyObject(action?.errors)); - expect(Object.values(erroredAction?.errors ?? {}).at(0)).toEqual(translateLocal('iou.error.other')); + const {result} = renderHook(() => useLocalize()); + expect(Object.values(erroredAction?.errors ?? {}).at(0)).toEqual(result.current.translate('iou.error.other')); resolve(); }, }); From 6d2078c5245a9636547199277a5a3f28a116b36a Mon Sep 17 00:00:00 2001 From: fahimj Date: Wed, 22 Oct 2025 07:13:14 +0700 Subject: [PATCH 11/16] Revert "remove deprecated translateLocal and use useLocalize" This reverts commit d545287d616d6affe65582492d1e9680897707bd. --- src/libs/ReportUtils.ts | 458 +++++++++++++++------------------------ src/libs/actions/IOU.ts | 40 ++-- tests/actions/IOUTest.ts | 11 +- 3 files changed, 192 insertions(+), 317 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f8a140ac10bb..2a60bce2f51b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -23,9 +23,8 @@ import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountI import type {ThemeColors} from '@styles/theme/types'; import type {IOUAction, IOUType, OnboardingAccounting} from '@src/CONST'; import CONST, {TASK_TO_FEATURE} from '@src/CONST'; -import IntlStore from '@src/languages/IntlStore'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; -import type {TranslationParameters, TranslationPaths} from '@src/languages/types'; +import type {TranslationPaths} from '@src/languages/types'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -105,6 +104,7 @@ import getAttachmentDetails from './fileDownload/getAttachmentDetails'; import getBase62ReportID from './getBase62ReportID'; import {isReportMessageAttachment} from './isReportMessageAttachment'; import {formatPhoneNumber} from './LocalePhoneNumber'; +import {translateLocal} from './Localize'; import Log from './Log'; import {isEmailPublicDomain} from './LoginUtils'; // eslint-disable-next-line import/no-cycle @@ -1153,9 +1153,8 @@ Onyx.connect({ }, }); -// Initialize translation values - these will be set once translations are loaded -let hiddenTranslation = '[hidden]'; -let unavailableTranslation = '[unavailable]'; +let hiddenTranslation = ''; +let unavailableTranslation = ''; Onyx.connect({ key: ONYXKEYS.ARE_TRANSLATIONS_LOADING, @@ -1164,14 +1163,8 @@ Onyx.connect({ if (value ?? true) { return; } - // Use IntlStore directly to get translations without deprecated translateLocal - const currentLocale = IntlStore.getCurrentLocale(); - if (currentLocale) { - const hiddenPhrase = IntlStore.get('common.hidden', currentLocale); - const unavailablePhrase = IntlStore.get('workspace.common.unavailable', currentLocale); - hiddenTranslation = typeof hiddenPhrase === 'string' ? hiddenPhrase : 'common.hidden'; - unavailableTranslation = typeof unavailablePhrase === 'string' ? unavailablePhrase : 'workspace.common.unavailable'; - } + hiddenTranslation = translateLocal('common.hidden'); + unavailableTranslation = translateLocal('workspace.common.unavailable'); }, }); @@ -3166,13 +3159,7 @@ const customCollator = new Intl.Collator('en', {usage: 'sort', sensitivity: 'var /** * Returns the report name if the report is a group chat */ -function getGroupChatName( - participants?: SelectedParticipant[], - shouldApplyLimit = false, - report?: OnyxEntry, - reportMetadataParam?: OnyxEntry, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -): string | undefined { +function getGroupChatName(participants?: SelectedParticipant[], shouldApplyLimit = false, report?: OnyxEntry, reportMetadataParam?: OnyxEntry): string | undefined { // If we have a report always try to get the name from the report. if (report?.reportName) { return report.reportName; @@ -3207,7 +3194,7 @@ function getGroupChatName( .concat(shouldAddEllipsis ? '...' : ''); } - return translate?.('groupChat.defaultReportName', {displayName: getDisplayNameForParticipant({accountID: participantAccountIDs.at(0)})}) ?? 'groupChat.defaultReportName'; + return translateLocal('groupChat.defaultReportName', {displayName: getDisplayNameForParticipant({accountID: participantAccountIDs.at(0)})}); } function getParticipants(reportID: string) { @@ -3564,7 +3551,6 @@ function getDisplayNamesWithTooltips( localeCompare: LocaleContextProps['localeCompare'], shouldFallbackToHidden = true, shouldAddCurrentUserPostfix = false, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): DisplayNameWithTooltips { const personalDetailsListArray = Array.isArray(personalDetailsList) ? personalDetailsList : Object.values(personalDetailsList); @@ -3578,7 +3564,7 @@ function getDisplayNamesWithTooltips( let pronouns = user?.pronouns ?? undefined; if (pronouns?.startsWith(CONST.PRONOUNS.PREFIX)) { const pronounTranslationKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); - pronouns = translate?.(`pronouns.${pronounTranslationKey}` as TranslationPaths) ?? `pronouns.${pronounTranslationKey}`; + pronouns = translateLocal(`pronouns.${pronounTranslationKey}` as TranslationPaths); } return { @@ -3615,15 +3601,12 @@ function getUserDetailTooltipText(accountID: number, fallbackUserDisplayName = ' * * @param reportAction - The deleted report action of a chat report for which we need to return message. */ -function getDeletedParentActionMessageForChatReport( - reportAction: OnyxEntry, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -): string { +function getDeletedParentActionMessageForChatReport(reportAction: OnyxEntry): string { // By default, let us display [Deleted message] - let deletedMessageText = translate?.('parentReportAction.deletedMessage') ?? 'parentReportAction.deletedMessage'; + let deletedMessageText = translateLocal('parentReportAction.deletedMessage'); if (isCreatedTaskReportAction(reportAction)) { // For canceled task report, let us display [Deleted task] - deletedMessageText = translate?.('parentReportAction.deletedTask') ?? 'parentReportAction.deletedTask'; + deletedMessageText = translateLocal('parentReportAction.deletedTask'); } return deletedMessageText; } @@ -3637,14 +3620,12 @@ function getReimbursementQueuedActionMessage({ shouldUseShortDisplayName = true, reports, personalDetails, - translate, }: { reportAction: OnyxEntry>; reportOrID: OnyxEntry | string | SearchReport; shouldUseShortDisplayName?: boolean; reports?: SearchReport[]; personalDetails?: Partial; - translate?: (path: TPath, ...parameters: TranslationParameters) => string; }): string { const report = typeof reportOrID === 'string' ? getReport(reportOrID, reports ?? allReports) : reportOrID; const submitterDisplayName = getDisplayNameForParticipant({accountID: report?.ownerAccountID, shouldUseShortForm: shouldUseShortDisplayName, personalDetailsData: personalDetails}) ?? ''; @@ -3656,7 +3637,7 @@ function getReimbursementQueuedActionMessage({ messageKey = 'iou.waitingOnBankAccount'; } - return translate?.(messageKey, {submitterDisplayName}) ?? messageKey; + return translateLocal(messageKey, {submitterDisplayName}); } /** @@ -3665,7 +3646,6 @@ function getReimbursementQueuedActionMessage({ function getReimbursementDeQueuedOrCanceledActionMessage( reportAction: OnyxEntry>, reportOrID: OnyxEntry | string | SearchReport, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): string { const report = typeof reportOrID === 'string' ? getReport(reportOrID, allReports) : reportOrID; const originalMessage = getOriginalMessage(reportAction); @@ -3673,10 +3653,10 @@ function getReimbursementDeQueuedOrCanceledActionMessage( const currency = originalMessage?.currency; const formattedAmount = convertToDisplayString(amount, currency); if (originalMessage?.cancellationReason === CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN || originalMessage?.cancellationReason === CONST.REPORT.CANCEL_PAYMENT_REASONS.USER) { - return translate?.('iou.adminCanceledRequest') ?? 'iou.adminCanceledRequest'; + return translateLocal('iou.adminCanceledRequest'); } const submitterDisplayName = getDisplayNameForParticipant({accountID: report?.ownerAccountID, shouldUseShortForm: true}) ?? ''; - return translate?.('iou.canceledRequest', {submitterDisplayName, amount: formattedAmount}) ?? 'iou.canceledRequest'; + return translateLocal('iou.canceledRequest', {submitterDisplayName, amount: formattedAmount}); } /** @@ -4000,15 +3980,7 @@ function getMoneyRequestSpendBreakdown(report: OnyxInputOrEntry, searchR /** * Get the title for a policy expense chat */ -function getPolicyExpenseChatName({ - report, - personalDetailsList, - translate, -}: { - report: OnyxEntry; - personalDetailsList?: Partial; - translate?: (path: TPath, ...parameters: TranslationParameters) => string; -}): string | undefined { +function getPolicyExpenseChatName({report, personalDetailsList}: {report: OnyxEntry; personalDetailsList?: Partial}): string | undefined { const ownerAccountID = report?.ownerAccountID; const personalDetails = ownerAccountID ? personalDetailsList?.[ownerAccountID] : undefined; const login = personalDetails ? personalDetails.login : null; @@ -4016,7 +3988,7 @@ function getPolicyExpenseChatName({ const reportOwnerDisplayName = getDisplayNameForParticipant({accountID: ownerAccountID, shouldRemoveDomain: true}) || login; if (reportOwnerDisplayName) { - return translate?.('workspace.common.policyExpenseChatName', {displayName: reportOwnerDisplayName}) ?? 'workspace.common.policyExpenseChatName'; + return translateLocal('workspace.common.policyExpenseChatName', {displayName: reportOwnerDisplayName}); } return report?.reportName; @@ -4150,12 +4122,10 @@ function getMoneyRequestReportName({ report, policy, invoiceReceiverPolicy, - translate, }: { report: OnyxEntry; policy?: OnyxEntry | SearchPolicy; invoiceReceiverPolicy?: OnyxEntry | SearchPolicy; - translate?: (path: TPath, ...parameters: TranslationParameters) => string; }): string { if (report?.reportName && isExpenseReport(report)) { return report.reportName; @@ -4175,32 +4145,29 @@ function getMoneyRequestReportName({ payerOrApproverName = getDisplayNameForParticipant({accountID: report?.managerID}) ?? ''; } - const payerPaidAmountMessage = - translate?.('iou.payerPaidAmount', { - payer: payerOrApproverName, - amount: formattedAmount, - }) ?? 'iou.payerPaidAmount'; + const payerPaidAmountMessage = translateLocal('iou.payerPaidAmount', { + payer: payerOrApproverName, + amount: formattedAmount, + }); if (isReportApproved({report})) { - return ( - translate?.('iou.managerApprovedAmount', { - manager: payerOrApproverName, - amount: formattedAmount, - }) ?? 'iou.managerApprovedAmount' - ); + return translateLocal('iou.managerApprovedAmount', { + manager: payerOrApproverName, + amount: formattedAmount, + }); } if (report?.isWaitingOnBankAccount) { - return `${payerPaidAmountMessage} ${CONST.DOT_SEPARATOR} ${translate?.('iou.pending') ?? 'iou.pending'}`; + return `${payerPaidAmountMessage} ${CONST.DOT_SEPARATOR} ${translateLocal('iou.pending')}`; } if (!isSettled(report?.reportID) && hasNonReimbursableTransactions(report?.reportID)) { payerOrApproverName = getDisplayNameForParticipant({accountID: report?.ownerAccountID}) ?? ''; - return translate?.('iou.payerSpentAmount', {payer: payerOrApproverName, amount: formattedAmount}) ?? 'iou.payerSpentAmount'; + return translateLocal('iou.payerSpentAmount', {payer: payerOrApproverName, amount: formattedAmount}); } if (isProcessingReport(report) || isOpenExpenseReport(report) || isOpenInvoiceReport(report) || moneyRequestTotal === 0) { - return translate?.('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount}) ?? 'iou.payerOwesAmount'; + return translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount}); } return payerPaidAmountMessage; @@ -4793,38 +4760,36 @@ function getTransactionReportName({ reportAction, transactions, reports, - translate, }: { reportAction: OnyxEntry; transactions?: SearchTransaction[]; reports?: SearchReport[]; - translate?: (path: TPath, ...parameters: TranslationParameters) => string; }): string { if (isReversedTransaction(reportAction)) { - return translate?.('parentReportAction.reversedTransaction') ?? 'parentReportAction.reversedTransaction'; + return translateLocal('parentReportAction.reversedTransaction'); } if (isDeletedAction(reportAction)) { - return translate?.('parentReportAction.deletedExpense') ?? 'parentReportAction.deletedExpense'; + return translateLocal('parentReportAction.deletedExpense'); } const transaction = getLinkedTransaction(reportAction, transactions); if (isEmptyObject(transaction)) { // Transaction data might be empty on app's first load, if so we fallback to Expense/Track Expense - return isTrackExpenseAction(reportAction) ? (translate?.('iou.createExpense') ?? 'iou.createExpense') : (translate?.('iou.expense') ?? 'iou.expense'); + return isTrackExpenseAction(reportAction) ? translateLocal('iou.createExpense') : translateLocal('iou.expense'); } if (isScanning(transaction)) { - return translate?.('iou.receiptScanning', {count: 1}) ?? 'iou.receiptScanning'; + return translateLocal('iou.receiptScanning', {count: 1}); } if (hasMissingSmartscanFieldsTransactionUtils(transaction)) { - return translate?.('iou.receiptMissingDetails') ?? 'iou.receiptMissingDetails'; + return translateLocal('iou.receiptMissingDetails'); } - if (isFetchingWaypointsFromServer(transaction) && getMerchant(transaction) === (translate?.('iou.fieldPending') ?? 'iou.fieldPending')) { - return translate?.('iou.fieldPending') ?? 'iou.fieldPending'; + if (isFetchingWaypointsFromServer(transaction) && getMerchant(transaction) === translateLocal('iou.fieldPending')) { + return translateLocal('iou.fieldPending'); } if (isSentMoneyReportAction(reportAction)) { @@ -4836,7 +4801,7 @@ function getTransactionReportName({ const formattedAmount = convertToDisplayString(amount, getCurrency(transaction)) ?? ''; const comment = getMerchantOrDescription(transaction); - return translate?.('iou.threadExpenseReportName', {formattedAmount, comment: Parser.htmlToText(comment)}) ?? 'iou.threadExpenseReportName'; + return translateLocal('iou.threadExpenseReportName', {formattedAmount, comment: Parser.htmlToText(comment)}); } /** @@ -4855,7 +4820,6 @@ function getReportPreviewMessage( policy?: OnyxInputOrEntry, isForListPreview = false, originalReportAction: OnyxInputOrEntry = iouReportAction, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): string { const report = typeof reportOrID === 'string' ? getReport(reportOrID, allReports) : reportOrID; const reportActionMessage = getReportActionHtml(iouReportAction); @@ -4881,16 +4845,16 @@ function getReportPreviewMessage( if (!isEmptyObject(linkedTransaction)) { if (isScanning(linkedTransaction)) { - return translate?.('iou.receiptScanning', {count: 1}) ?? 'iou.receiptScanning'; + return translateLocal('iou.receiptScanning', {count: 1}); } if (hasMissingSmartscanFieldsTransactionUtils(linkedTransaction)) { - return translate?.('iou.receiptMissingDetails') ?? 'iou.receiptMissingDetails'; + return translateLocal('iou.receiptMissingDetails'); } const amount = getTransactionAmount(linkedTransaction, !isEmptyObject(report) && isExpenseReport(report), linkedTransaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) ?? 0; const formattedAmount = convertToDisplayString(amount, getCurrency(linkedTransaction)) ?? ''; - return translate?.('iou.didSplitAmount', {formattedAmount, comment: getMerchantOrDescription(linkedTransaction)}) ?? 'iou.didSplitAmount'; + return translateLocal('iou.didSplitAmount', {formattedAmount, comment: getMerchantOrDescription(linkedTransaction)}); } } @@ -4903,16 +4867,16 @@ function getReportPreviewMessage( if (!isEmptyObject(linkedTransaction)) { if (isScanning(linkedTransaction)) { - return translate?.('iou.receiptScanning', {count: 1}) ?? 'iou.receiptScanning'; + return translateLocal('iou.receiptScanning', {count: 1}); } if (hasMissingSmartscanFieldsTransactionUtils(linkedTransaction)) { - return translate?.('iou.receiptMissingDetails') ?? 'iou.receiptMissingDetails'; + return translateLocal('iou.receiptMissingDetails'); } const amount = getTransactionAmount(linkedTransaction, !isEmptyObject(report) && isExpenseReport(report), linkedTransaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) ?? 0; const formattedAmount = convertToDisplayString(amount, getCurrency(linkedTransaction)) ?? ''; - return translate?.('iou.trackedAmount', {formattedAmount, comment: getMerchantOrDescription(linkedTransaction)}) ?? 'iou.trackedAmount'; + return translateLocal('iou.trackedAmount', {formattedAmount, comment: getMerchantOrDescription(linkedTransaction)}); } } @@ -4926,12 +4890,10 @@ function getReportPreviewMessage( const formattedAmount = convertToDisplayString(totalAmount, report.currency); if (isReportApproved({report}) && isPaidGroupPolicy(report)) { - return ( - translate?.('iou.managerApprovedAmount', { - manager: payerName ?? '', - amount: formattedAmount, - }) ?? 'iou.managerApprovedAmount' - ); + return translateLocal('iou.managerApprovedAmount', { + manager: payerName ?? '', + amount: formattedAmount, + }); } const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; @@ -4942,11 +4904,11 @@ function getReportPreviewMessage( } if (!isEmptyObject(linkedTransaction) && isScanning(linkedTransaction)) { - return translate?.('iou.receiptScanning', {count: numberOfScanningReceipts}) ?? 'iou.receiptScanning'; + return translateLocal('iou.receiptScanning', {count: numberOfScanningReceipts}); } if (!isEmptyObject(linkedTransaction) && isFetchingWaypointsFromServer(linkedTransaction) && !getTransactionAmount(linkedTransaction)) { - return translate?.('iou.fieldPending') ?? 'iou.fieldPending'; + return translateLocal('iou.fieldPending'); } const originalMessage = !isEmptyObject(iouReportAction) && isMoneyRequestAction(iouReportAction) ? getOriginalMessage(iouReportAction) : undefined; @@ -4977,18 +4939,16 @@ function getReportPreviewMessage( actualPayerName = actualPayerName && isForListPreview && !isPreviewMessageForParentChatReport ? `${actualPayerName}:` : actualPayerName; const payerDisplayName = isPreviewMessageForParentChatReport ? payerName : actualPayerName; - return ( - translate?.(translatePhraseKey, { - amount: '', - payer: payerDisplayName ?? '', - last4Digits: reportPolicy?.achAccount?.accountNumber?.slice(-4) ?? '', - }) ?? translatePhraseKey - ); + return translateLocal(translatePhraseKey, { + amount: '', + payer: payerDisplayName ?? '', + last4Digits: reportPolicy?.achAccount?.accountNumber?.slice(-4) ?? '', + }); } if (report.isWaitingOnBankAccount) { const submitterDisplayName = getDisplayNameForParticipant({accountID: report.ownerAccountID, shouldUseShortForm: true}) ?? ''; - return translate?.('iou.waitingOnBankAccount', {submitterDisplayName}) ?? 'iou.waitingOnBankAccount'; + return translateLocal('iou.waitingOnBankAccount', {submitterDisplayName}); } const lastActorID = iouReportAction?.actorAccountID; @@ -5016,14 +4976,14 @@ function getReportPreviewMessage( // We only want to show the actor name in the preview if it's not the current user who took the action const requestorName = lastActorID && lastActorID !== currentUserAccountID ? getDisplayNameForParticipant({accountID: lastActorID, shouldUseShortForm: !isPreviewMessageForParentChatReport}) : ''; - return `${requestorName ? `${requestorName}: ` : ''}${translate?.('iou.expenseAmount', {formattedAmount: amountToDisplay, comment}) ?? 'iou.expenseAmount'}`; + return `${requestorName ? `${requestorName}: ` : ''}${translateLocal('iou.expenseAmount', {formattedAmount: amountToDisplay, comment})}`; } if (containsNonReimbursable) { - return translate?.('iou.payerSpentAmount', {payer: getDisplayNameForParticipant({accountID: report.ownerAccountID}) ?? '', amount: formattedAmount}) ?? 'iou.payerSpentAmount'; + return translateLocal('iou.payerSpentAmount', {payer: getDisplayNameForParticipant({accountID: report.ownerAccountID}) ?? '', amount: formattedAmount}); } - return translate?.('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}) ?? 'iou.payerOwesAmount'; + return translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); } /** @@ -5039,7 +4999,6 @@ function getModifiedExpenseOriginalMessage( policy: OnyxInputOrEntry, updatedTransaction?: OnyxInputOrEntry, allowNegative = false, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): OriginalMessageModifiedExpense { const originalMessage: OriginalMessageModifiedExpense = {}; // Remark: Comment field is the only one which has new/old prefixes for the keys (newComment/ oldComment), @@ -5099,22 +5058,14 @@ function getModifiedExpenseOriginalMessage( if ('reimbursable' in transactionChanges) { const oldReimbursable = getReimbursable(oldTransaction); - originalMessage.oldReimbursable = oldReimbursable - ? (translate?.('common.reimbursable') ?? 'common.reimbursable').toLowerCase() - : (translate?.('iou.nonReimbursable') ?? 'iou.nonReimbursable').toLowerCase(); - originalMessage.reimbursable = transactionChanges?.reimbursable - ? (translate?.('common.reimbursable') ?? 'common.reimbursable').toLowerCase() - : (translate?.('iou.nonReimbursable') ?? 'iou.nonReimbursable').toLowerCase(); + originalMessage.oldReimbursable = oldReimbursable ? translateLocal('common.reimbursable').toLowerCase() : translateLocal('iou.nonReimbursable').toLowerCase(); + originalMessage.reimbursable = transactionChanges?.reimbursable ? translateLocal('common.reimbursable').toLowerCase() : translateLocal('iou.nonReimbursable').toLowerCase(); } if ('billable' in transactionChanges) { const oldBillable = getBillable(oldTransaction); - originalMessage.oldBillable = oldBillable - ? (translate?.('common.billable') ?? 'common.billable').toLowerCase() - : (translate?.('common.nonBillable') ?? 'common.nonBillable').toLowerCase(); - originalMessage.billable = transactionChanges?.billable - ? (translate?.('common.billable') ?? 'common.billable').toLowerCase() - : (translate?.('common.nonBillable') ?? 'common.nonBillable').toLowerCase(); + originalMessage.oldBillable = oldBillable ? translateLocal('common.billable').toLowerCase() : translateLocal('common.nonBillable').toLowerCase(); + originalMessage.billable = transactionChanges?.billable ? translateLocal('common.billable').toLowerCase() : translateLocal('common.nonBillable').toLowerCase(); } if ( @@ -5151,19 +5102,15 @@ function isChangeLogObject(originalMessage?: OriginalMessageChangeLog): Original * @param parentReportAction * @param parentReportActionMessage */ -function getAdminRoomInvitedParticipants( - parentReportAction: OnyxEntry, - parentReportActionMessage: string, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -) { +function getAdminRoomInvitedParticipants(parentReportAction: OnyxEntry, parentReportActionMessage: string) { if (isEmptyObject(parentReportAction)) { - return parentReportActionMessage || (translate?.('parentReportAction.deletedMessage') ?? 'parentReportAction.deletedMessage'); + return parentReportActionMessage || translateLocal('parentReportAction.deletedMessage'); } if (!getOriginalMessage(parentReportAction)) { - return parentReportActionMessage || (translate?.('parentReportAction.deletedMessage') ?? 'parentReportAction.deletedMessage'); + return parentReportActionMessage || translateLocal('parentReportAction.deletedMessage'); } if (!isPolicyChangeLogAction(parentReportAction) && !isRoomChangeLogAction(parentReportAction)) { - return parentReportActionMessage || (translate?.('parentReportAction.deletedMessage') ?? 'parentReportAction.deletedMessage'); + return parentReportActionMessage || translateLocal('parentReportAction.deletedMessage'); } const originalMessage = isChangeLogObject(getOriginalMessage(parentReportAction)); @@ -5174,9 +5121,9 @@ function getAdminRoomInvitedParticipants( if (name && name?.length > 0) { return name; } - return translate?.('common.hidden') ?? 'common.hidden'; + return translateLocal('common.hidden'); }); - const users = participants.length > 1 ? participants.join(` ${translate?.('common.and') ?? 'common.and'} `) : participants.at(0); + const users = participants.length > 1 ? participants.join(` ${translateLocal('common.and')} `) : participants.at(0); if (!users) { return parentReportActionMessage; } @@ -5186,8 +5133,8 @@ function getAdminRoomInvitedParticipants( const verbKey = isInviteAction ? 'workspace.invite.invited' : 'workspace.invite.removed'; const prepositionKey = isInviteAction ? 'workspace.invite.to' : 'workspace.invite.from'; - const verb = translate?.(verbKey) ?? verbKey; - const preposition = translate?.(prepositionKey) ?? prepositionKey; + const verb = translateLocal(verbKey); + const preposition = translateLocal(prepositionKey); const roomName = originalMessage?.roomName ?? ''; @@ -5266,20 +5213,18 @@ function getReportActionMessage({ childReportID, reports, personalDetails, - translate, }: { reportAction: OnyxEntry; reportID?: string; childReportID?: string; reports?: SearchReport[]; personalDetails?: Partial; - translate?: (path: TPath, ...parameters: TranslationParameters) => string; }) { if (isEmptyObject(reportAction)) { return ''; } if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { - return translate?.('iou.heldExpense') ?? 'iou.heldExpense'; + return translateLocal('iou.heldExpense'); } if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION) { @@ -5287,15 +5232,15 @@ function getReportActionMessage({ } if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { - return translate?.('iou.unheldExpense') ?? 'iou.unheldExpense'; + return translateLocal('iou.unheldExpense'); } if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTEDTRANSACTION_THREAD) { - return translate?.('iou.reject.reportActions.rejectedExpense') ?? 'iou.reject.reportActions.rejectedExpense'; + return translateLocal('iou.reject.reportActions.rejectedExpense'); } if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTED_TRANSACTION_MARKASRESOLVED) { - return translate?.('iou.reject.reportActions.markedAsResolved') ?? 'iou.reject.reportActions.markedAsResolved'; + return translateLocal('iou.reject.reportActions.markedAsResolved'); } if (isApprovedOrSubmittedReportAction(reportAction) || isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.REIMBURSED)) { @@ -5308,15 +5253,14 @@ function getReportActionMessage({ shouldUseShortDisplayName: false, reports, personalDetails, - translate, }); } if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.RECEIPT_SCAN_FAILED) { - return translate?.('iou.receiptScanningFailed') ?? 'iou.receiptScanningFailed'; + return translateLocal('iou.receiptScanningFailed'); } if (isReimbursementDeQueuedOrCanceledAction(reportAction)) { - return getReimbursementDeQueuedOrCanceledActionMessage(reportAction, getReportOrDraftReport(reportID, reports), translate); + return getReimbursementDeQueuedOrCanceledActionMessage(reportAction, getReportOrDraftReport(reportID, reports)); } return parseReportActionHtmlToText(reportAction, reportID, childReportID); @@ -5400,7 +5344,6 @@ function getReportName( isReportArchived?: boolean, reports?: SearchReport[], policies?: SearchPolicy[], - translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): string { // Check if we can use report name in derived values - only when we have report but no other params const canUseDerivedValue = @@ -5429,16 +5372,16 @@ function getReportName( ) { const harvesting = !isMarkAsClosedAction(parentReportAction) ? (getOriginalMessage(parentReportAction)?.harvesting ?? false) : false; if (harvesting) { - return translate?.('iou.automaticallySubmitted') ?? 'iou.automaticallySubmitted'; + return translateLocal('iou.automaticallySubmitted'); } - return translate?.('iou.submitted', {memo: getOriginalMessage(parentReportAction)?.message}) ?? 'iou.submitted'; + return translateLocal('iou.submitted', {memo: getOriginalMessage(parentReportAction)?.message}); } if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.FORWARDED)) { const {automaticAction} = getOriginalMessage(parentReportAction) ?? {}; if (automaticAction) { - return translate?.('iou.automaticallyForwarded') ?? 'iou.automaticallyForwarded'; + return translateLocal('iou.automaticallyForwarded'); } - return translate?.('iou.forwarded') ?? 'iou.forwarded'; + return translateLocal('iou.forwarded'); } if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTED) { return getRejectedReportMessage(); @@ -5462,7 +5405,7 @@ function getReportName( return getWorkspaceUpdateFieldMessage(parentReportAction); } if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.MERGED_WITH_CASH_TRANSACTION) { - return translate?.('systemMessage.mergedWithCashTransaction') ?? 'systemMessage.mergedWithCashTransaction'; + return translateLocal('systemMessage.mergedWithCashTransaction'); } if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_NAME) { return Str.htmlDecode(getWorkspaceNameUpdatedMessage(parentReportAction)); @@ -5503,7 +5446,7 @@ function getReportName( } if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED)) { - return translate?.('iou.paidElsewhere') ?? 'iou.paidElsewhere'; + return translateLocal('iou.paidElsewhere'); } if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY)) { @@ -5529,19 +5472,19 @@ function getReportName( if (originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { if (originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { - return translate?.('iou.paidElsewhere') ?? 'iou.paidElsewhere'; + return translateLocal('iou.paidElsewhere'); } if (originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.VBBA) { if (originalMessage.automaticAction) { - return translate?.('iou.automaticallyPaidWithBusinessBankAccount', {last4Digits}) ?? 'iou.automaticallyPaidWithBusinessBankAccount'; + return translateLocal('iou.automaticallyPaidWithBusinessBankAccount', {last4Digits}); } - return translate?.('iou.businessBankAccount', {last4Digits}) ?? 'iou.businessBankAccount'; + return translateLocal('iou.businessBankAccount', {last4Digits}); } if (originalMessage.paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { if (originalMessage.automaticAction) { - return translate?.('iou.automaticallyPaidWithExpensify') ?? 'iou.automaticallyPaidWithExpensify'; + return translateLocal('iou.automaticallyPaidWithExpensify'); } - return translate?.('iou.paidWithExpensify') ?? 'iou.paidWithExpensify'; + return translateLocal('iou.paidWithExpensify'); } } } @@ -5549,12 +5492,12 @@ function getReportName( if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.APPROVED)) { const {automaticAction} = getOriginalMessage(parentReportAction) ?? {}; if (automaticAction) { - return translate?.('iou.automaticallyApproved') ?? 'iou.automaticallyApproved'; + return translateLocal('iou.automaticallyApproved'); } - return translate?.('iou.approvedMessage') ?? 'iou.approvedMessage'; + return translateLocal('iou.approvedMessage'); } if (isUnapprovedAction(parentReportAction)) { - return translate?.('iou.unapproved') ?? 'iou.unapproved'; + return translateLocal('iou.unapproved'); } if (isActionableJoinRequest(parentReportAction)) { @@ -5562,7 +5505,7 @@ function getReportName( } if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { - return translate?.('parentReportAction.deletedTask') ?? 'parentReportAction.deletedTask'; + return translateLocal('parentReportAction.deletedTask'); } if (isTaskReport(report)) { @@ -5596,11 +5539,11 @@ function getReportName( } if (parentReportActionMessage?.isDeletedParentAction) { - return translate?.('parentReportAction.deletedMessage') ?? 'parentReportAction.deletedMessage'; + return translateLocal('parentReportAction.deletedMessage'); } if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RESOLVED_DUPLICATES) { - return translate?.('violations.resolvedDuplicates') ?? 'violations.resolvedDuplicates'; + return translateLocal('violations.resolvedDuplicates'); } const isAttachment = isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : undefined); @@ -5612,14 +5555,14 @@ function getReportName( personalDetails, }).replace(/(\n+|\r\n|\n|\r)/gm, ' '); if (isAttachment && reportActionMessage) { - return `[${translate?.('common.attachment') ?? 'common.attachment'}]`; + return `[${translateLocal('common.attachment')}]`; } if ( parentReportActionMessage?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || parentReportActionMessage?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN || parentReportActionMessage?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE ) { - return translate?.('parentReportAction.hiddenMessage') ?? 'parentReportAction.hiddenMessage'; + return translateLocal('parentReportAction.hiddenMessage'); } if (isAdminRoom(report) || isUserCreatedPolicyRoom(report)) { return getAdminRoomInvitedParticipants(parentReportAction, reportActionMessage); @@ -5651,7 +5594,7 @@ function getReportName( } if (isClosedExpenseReportWithNoExpenses(report, transactions)) { - return translate?.('parentReportAction.deletedReport') ?? 'parentReportAction.deletedReport'; + return translateLocal('parentReportAction.deletedReport'); } if (isGroupChat(report)) { @@ -5723,8 +5666,8 @@ function getInvoiceReportName(report: OnyxEntry, policy?: OnyxEntry(path: TPath, ...parameters: TranslationParameters) => string): string { - return `${reportName} (${translate?.('common.archived') ?? 'common.archived'}) `; +function generateArchivedReportName(reportName: string): string { + return `${reportName} (${translateLocal('common.archived')}) `; } /** @@ -5765,23 +5708,18 @@ function getReportSubtitlePrefix(report: OnyxEntry): string { /** * Get either the policyName or domainName the chat is tied to */ -function getChatRoomSubtitle( - report: OnyxEntry, - isPolicyNamePreferred = false, - isReportArchived = false, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -): string | undefined { +function getChatRoomSubtitle(report: OnyxEntry, isPolicyNamePreferred = false, isReportArchived = false): string | undefined { if (isChatThread(report)) { return ''; } if (isSelfDM(report)) { - return translate?.('reportActionsView.yourSpace') ?? 'reportActionsView.yourSpace'; + return translateLocal('reportActionsView.yourSpace'); } if (isInvoiceRoom(report)) { - return translate?.('workspace.common.invoices') ?? 'workspace.common.invoices'; + return translateLocal('workspace.common.invoices'); } if (isConciergeChatReport(report)) { - return translate?.('reportActionsView.conciergeSupport') ?? 'reportActionsView.conciergeSupport'; + return translateLocal('reportActionsView.conciergeSupport'); } if (!isDefaultRoom(report) && !isUserCreatedPolicyRoom(report) && !isPolicyExpenseChat(report)) { return ''; @@ -5800,7 +5738,7 @@ function getChatRoomSubtitle( return getPolicyName({report}); } - return `${getReportSubtitlePrefix(report)}${translate?.('iou.submitsTo', {name: subtitle ?? ''}) ?? 'iou.submitsTo'}`; + return `${getReportSubtitlePrefix(report)}${translateLocal('iou.submitsTo', {name: subtitle ?? ''})}`; } if (isReportArchived) { @@ -5820,12 +5758,7 @@ function getPendingChatMembers(accountIDs: number[], previousPendingChatMembers: /** * Gets the parent navigation subtitle for the report */ -function getParentNavigationSubtitle( - report: OnyxEntry, - isParentReportArchived = false, - reportAttributes?: ReportAttributesDerivedValue['reports'], - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -): ParentNavigationSummaryParams { +function getParentNavigationSubtitle(report: OnyxEntry, isParentReportArchived = false, reportAttributes?: ReportAttributesDerivedValue['reports']): ParentNavigationSummaryParams { const parentReport = getParentReport(report); if (report?.hasParentAccess === false && !isReportManager(report)) { return {}; @@ -5840,7 +5773,7 @@ function getParentNavigationSubtitle( if (isExpenseReport(report)) { return { - reportName: translate?.('workspace.common.policyExpenseChatName', {displayName: reportOwnerDisplayName ?? ''}) ?? 'workspace.common.policyExpenseChatName', + reportName: translateLocal('workspace.common.policyExpenseChatName', {displayName: reportOwnerDisplayName ?? ''}), workspaceName: getPolicyName({report}), }; } @@ -5854,7 +5787,7 @@ function getParentNavigationSubtitle( let reportName = `${getPolicyName({report: parentReport})} & ${getInvoicePayerName(parentReport)}`; if (isArchivedNonExpenseReport(parentReport, isParentReportArchived)) { - reportName += ` (${translate?.('common.archived') ?? 'common.archived'})`; + reportName += ` (${translateLocal('common.archived')})`; } return { @@ -6318,16 +6251,10 @@ function getHumanReadableStatus(statusNum: number): string { * If after all replacements the formula is empty, the original formula is returned. * See {@link https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates} */ -function populateOptimisticReportFormula( - formula: string, - report: OptimisticExpenseReport | OptimisticNewReport, - policy: OnyxEntry, - isMoneyRequestConfirmation = false, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -): string { +function populateOptimisticReportFormula(formula: string, report: OptimisticExpenseReport | OptimisticNewReport, policy: OnyxEntry, isMoneyRequestConfirmation = false): string { // If this is a newly created report and it is from money request confirmation, we should use 'New report' as the report title if (!report.parentReportActionID && isMoneyRequestConfirmation) { - return translate?.('iou.newReport') ?? 'iou.newReport'; + return translateLocal('iou.newReport'); } const createdDate = report.lastVisibleActionCreated ? new Date(report.lastVisibleActionCreated) : undefined; @@ -6539,59 +6466,52 @@ function buildOptimisticEmptyReport(reportID: string, accountID: number, parentR return optimisticEmptyReport; } -function getRejectedReportMessage(translate?: (path: TPath, ...parameters: TranslationParameters) => string) { - return translate?.('iou.rejectedThisReport') ?? 'iou.rejectedThisReport'; +function getRejectedReportMessage() { + return translateLocal('iou.rejectedThisReport'); } -function getUpgradeWorkspaceMessage(translate?: (path: TPath, ...parameters: TranslationParameters) => string) { - return translate?.('workspaceActions.upgradedWorkspace') ?? 'workspaceActions.upgradedWorkspace'; +function getUpgradeWorkspaceMessage() { + return translateLocal('workspaceActions.upgradedWorkspace'); } -function getDowngradeWorkspaceMessage(translate?: (path: TPath, ...parameters: TranslationParameters) => string) { - return translate?.('workspaceActions.downgradedWorkspace') ?? 'workspaceActions.downgradedWorkspace'; +function getDowngradeWorkspaceMessage() { + return translateLocal('workspaceActions.downgradedWorkspace'); } -function getWorkspaceNameUpdatedMessage(action: ReportAction, translate?: (path: TPath, ...parameters: TranslationParameters) => string) { +function getWorkspaceNameUpdatedMessage(action: ReportAction) { const {oldName, newName} = getOriginalMessage(action as ReportAction) ?? {}; - const message = - oldName && newName ? (translate?.('workspaceActions.renamedWorkspaceNameAction', {oldName, newName}) ?? 'workspaceActions.renamedWorkspaceNameAction') : getReportActionText(action); + const message = oldName && newName ? translateLocal('workspaceActions.renamedWorkspaceNameAction', {oldName, newName}) : getReportActionText(action); return Str.htmlEncode(message); } -function getDeletedTransactionMessage(action: ReportAction, translate?: (path: TPath, ...parameters: TranslationParameters) => string) { +function getDeletedTransactionMessage(action: ReportAction) { const deletedTransactionOriginalMessage = getOriginalMessage(action as ReportAction) ?? {}; const amount = -(deletedTransactionOriginalMessage.amount ?? 0); const currency = deletedTransactionOriginalMessage.currency ?? ''; const formattedAmount = convertToDisplayString(amount, currency) ?? ''; - const message = - translate?.('iou.deletedTransaction', { - amount: formattedAmount, - merchant: deletedTransactionOriginalMessage.merchant ?? '', - }) ?? 'iou.deletedTransaction'; + const message = translateLocal('iou.deletedTransaction', { + amount: formattedAmount, + merchant: deletedTransactionOriginalMessage.merchant ?? '', + }); return message; } -function getMovedTransactionMessage(report: OnyxEntry, translate?: (path: TPath, ...parameters: TranslationParameters) => string) { +function getMovedTransactionMessage(report: OnyxEntry) { const reportName = getReportName(report) ?? report?.reportName ?? ''; const reportUrl = getReportURLForCurrentContext(report?.reportID); - return translate?.('iou.movedTransaction', {reportUrl, reportName}) ?? 'iou.movedTransaction'; + return translateLocal('iou.movedTransaction', {reportUrl, reportName}); } -function getUnreportedTransactionMessage(translate?: (path: TPath, ...parameters: TranslationParameters) => string) { +function getUnreportedTransactionMessage() { const selfDMReportID = findSelfDMReportID(); const reportUrl = `${environmentURL}/r/${selfDMReportID}`; - const message = - translate?.('iou.unreportedTransaction', { - reportUrl, - }) ?? 'iou.unreportedTransaction'; + const message = translateLocal('iou.unreportedTransaction', { + reportUrl, + }); return message; } -function getMovedActionMessage( - action: ReportAction, - report: OnyxEntry, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -) { +function getMovedActionMessage(action: ReportAction, report: OnyxEntry) { if (!isMovedAction(action)) { return ''; } @@ -6602,24 +6522,21 @@ function getMovedActionMessage( } const {toPolicyID, newParentReportID, movedReportID} = movedActionOriginalMessage; const toPolicyName = getPolicyNameByID(toPolicyID); - return ( - translate?.('iou.movedAction', { - shouldHideMovedReportUrl: !isDM(report), - movedReportUrl: getReportURLForCurrentContext(movedReportID), - newParentReportUrl: getReportURLForCurrentContext(newParentReportID), - toPolicyName, - }) ?? 'iou.movedAction' - ); + return translateLocal('iou.movedAction', { + shouldHideMovedReportUrl: !isDM(report), + movedReportUrl: getReportURLForCurrentContext(movedReportID), + newParentReportUrl: getReportURLForCurrentContext(newParentReportID), + toPolicyName, + }); } -function getPolicyChangeMessage(action: ReportAction, translate?: (path: TPath, ...parameters: TranslationParameters) => string) { +function getPolicyChangeMessage(action: ReportAction) { const PolicyChangeOriginalMessage = getOriginalMessage(action as ReportAction) ?? {}; const {fromPolicy: fromPolicyID, toPolicy: toPolicyID} = PolicyChangeOriginalMessage as OriginalMessageChangePolicy; - const message = - translate?.('report.actions.type.changeReportPolicy', { - fromPolicyName: fromPolicyID ? getPolicyNameByID(fromPolicyID) : undefined, - toPolicyName: getPolicyNameByID(toPolicyID), - }) ?? 'report.actions.type.changeReportPolicy'; + const message = translateLocal('report.actions.type.changeReportPolicy', { + fromPolicyName: fromPolicyID ? getPolicyNameByID(fromPolicyID) : undefined, + toPolicyName: getPolicyNameByID(toPolicyID), + }); return message; } @@ -6644,7 +6561,6 @@ function getIOUReportActionMessage( isSettlingUp = false, bankAccountID?: number | undefined, payAsBusiness = false, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): Message[] { const report = getReportOrDraftReport(iouReportID); const isInvoice = isInvoiceReport(report); @@ -6691,15 +6607,14 @@ function getIOUReportActionMessage( if (isInvoice && isSettlingUp) { iouMessage = paymentType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE - ? (translate?.('iou.payElsewhere', {formattedAmount: amount}) ?? 'iou.payElsewhere') - : (translate?.(payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal', {amount, last4Digits: String(bankAccountID).slice(-4)}) ?? - (payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal')); + ? translateLocal('iou.payElsewhere', {formattedAmount: amount}) + : translateLocal(payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal', {amount, last4Digits: String(bankAccountID).slice(-4)}); } else { iouMessage = isSettlingUp ? `paid ${amount}${paymentMethodMessage}` : `sent ${amount}${comment && ` for ${comment}`}${paymentMethodMessage}`; } break; case CONST.REPORT.ACTIONS.TYPE.SUBMITTED: - iouMessage = translate?.('iou.expenseAmount', {formattedAmount: amount}) ?? 'iou.expenseAmount'; + iouMessage = translateLocal('iou.expenseAmount', {formattedAmount: amount}); break; default: break; @@ -7613,10 +7528,7 @@ function buildOptimisticRoomDescriptionUpdatedReportAction(description: string): * Returns the necessary reportAction onyx data to indicate that the transaction has been put on hold optimistically * @param [created] - Action created time */ -function buildOptimisticHoldReportAction( - created = DateUtils.getDBTime(), - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -): OptimisticHoldReportAction { +function buildOptimisticHoldReportAction(created = DateUtils.getDBTime()): OptimisticHoldReportAction { return { reportActionID: rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.HOLD, @@ -7626,7 +7538,7 @@ function buildOptimisticHoldReportAction( { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: translate?.('iou.heldExpense') ?? 'iou.heldExpense', + text: translateLocal('iou.heldExpense'), }, ], person: [ @@ -7678,10 +7590,7 @@ function buildOptimisticHoldReportActionComment(comment: string, created = DateU * Returns the necessary reportAction onyx data to indicate that the transaction has been removed from hold optimistically * @param [created] - Action created time */ -function buildOptimisticUnHoldReportAction( - created = DateUtils.getDBTime(), - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -): OptimisticHoldReportAction { +function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): OptimisticHoldReportAction { return { reportActionID: rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.UNHOLD, @@ -7691,7 +7600,7 @@ function buildOptimisticUnHoldReportAction( { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: translate?.('iou.unheldExpense') ?? 'iou.unheldExpense', + text: translateLocal('iou.unheldExpense'), }, ], person: [ @@ -7938,9 +7847,7 @@ function buildOptimisticDismissedViolationReportAction( }; } -function buildOptimisticResolvedDuplicatesReportAction( - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -): OptimisticDismissedViolationReportAction { +function buildOptimisticResolvedDuplicatesReportAction(): OptimisticDismissedViolationReportAction { return { actionName: CONST.REPORT.ACTIONS.TYPE.RESOLVED_DUPLICATES, actorAccountID: currentUserAccountID, @@ -7950,7 +7857,7 @@ function buildOptimisticResolvedDuplicatesReportAction( { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: translate?.('violations.resolvedDuplicates') ?? 'violations.resolvedDuplicates', + text: translateLocal('violations.resolvedDuplicates'), }, ], pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, @@ -8713,7 +8620,6 @@ function hasReportErrorsOtherThanFailedReceipt( doesReportHaveViolations: boolean, transactionViolations: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], - translate?: (path: TPath, ...parameters: TranslationParameters) => string, ) { const allReportErrors = reportAttributes?.[report?.reportID]?.reportErrors ?? {}; const transactionReportActions = getAllReportActions(report.reportID); @@ -8726,7 +8632,7 @@ function hasReportErrorsOtherThanFailedReceipt( return ( doesTransactionThreadReportHasViolations || doesReportHaveViolations || - Object.values(allReportErrors).some((error) => error?.[0] !== (translate?.('iou.error.genericSmartscanFailureMessage') ?? 'iou.error.genericSmartscanFailureMessage')) + Object.values(allReportErrors).some((error) => error?.[0] !== translateLocal('iou.error.genericSmartscanFailureMessage')) ); } @@ -9363,15 +9269,12 @@ function isCurrentUserTheOnlyParticipant(participantAccountIDs?: number[]): bool * Returns display names for those that can see the whisper. * However, it returns "you" if the current user is the only one who can see it besides the person that sent it. */ -function getWhisperDisplayNames( - participantAccountIDs?: number[], - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -): string | undefined { +function getWhisperDisplayNames(participantAccountIDs?: number[]): string | undefined { const isWhisperOnlyVisibleToCurrentUser = isCurrentUserTheOnlyParticipant(participantAccountIDs); // When the current user is the only participant, the display name needs to be "you" because that's the only person reading it if (isWhisperOnlyVisibleToCurrentUser) { - return translate?.('common.youAfterPreposition') ?? 'common.youAfterPreposition'; + return translateLocal('common.youAfterPreposition'); } return participantAccountIDs?.map((accountID) => getDisplayNameForParticipant({accountID, shouldUseShortForm: !isWhisperOnlyVisibleToCurrentUser})).join(', '); @@ -9755,12 +9658,7 @@ function getTaskAssigneeChatOnyxData( /** * Return iou report action display message */ -function getIOUReportActionDisplayMessage( - reportAction: OnyxEntry, - transaction?: OnyxEntry, - report?: Report, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -): string { +function getIOUReportActionDisplayMessage(reportAction: OnyxEntry, transaction?: OnyxEntry, report?: Report): string { if (!isMoneyRequestAction(reportAction)) { return ''; } @@ -9781,10 +9679,7 @@ function getIOUReportActionDisplayMessage( case CONST.IOU.PAYMENT_TYPE.EXPENSIFY: case CONST.IOU.PAYMENT_TYPE.VBBA: if (isInvoice) { - return ( - translate?.(payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal', {amount: '', last4Digits}) ?? - (payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal') - ); + return translateLocal(payAsBusiness ? 'iou.settleInvoiceBusiness' : 'iou.settleInvoicePersonal', {amount: '', last4Digits}); } translationKey = 'iou.businessBankAccount'; @@ -9801,7 +9696,7 @@ function getIOUReportActionDisplayMessage( break; } - return translate?.(translationKey, {amount: '', payer: '', last4Digits}) ?? translationKey; + return translateLocal(translationKey, {amount: '', payer: '', last4Digits}); } const amount = getTransactionAmount(transaction, !isEmptyObject(iouReport) && isExpenseReport(iouReport), transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) ?? 0; @@ -9809,18 +9704,14 @@ function getIOUReportActionDisplayMessage( const isRequestSettled = isSettled(IOUReportID); const isApproved = isReportApproved({report: iouReport}); if (isRequestSettled) { - return ( - translate?.('iou.payerSettled', { - amount: formattedAmount, - }) ?? 'iou.payerSettled' - ); + return translateLocal('iou.payerSettled', { + amount: formattedAmount, + }); } if (isApproved) { - return ( - translate?.('iou.approvedAmount', { - amount: formattedAmount, - }) ?? 'iou.approvedAmount' - ); + return translateLocal('iou.approvedAmount', { + amount: formattedAmount, + }); } if (isSplitBillReportAction(reportAction)) { translationKey = 'iou.didSplitAmount'; @@ -9829,12 +9720,10 @@ function getIOUReportActionDisplayMessage( } else { translationKey = 'iou.expenseAmount'; } - return ( - translate?.(translationKey, { - formattedAmount, - comment: getMerchantOrDescription(transaction), - }) ?? translationKey - ); + return translateLocal(translationKey, { + formattedAmount, + comment: getMerchantOrDescription(transaction), + }); } /** @@ -10871,7 +10760,8 @@ function prepareOnboardingOnyxData( }); // Sign-off welcome message - const welcomeSignOffText = engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ? 'onboarding.welcomeSignOffTitleManageTeam' : 'onboarding.welcomeSignOffTitle'; + const welcomeSignOffText = + engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ? translateLocal('onboarding.welcomeSignOffTitleManageTeam') : translateLocal('onboarding.welcomeSignOffTitle'); const welcomeSignOffComment = buildOptimisticAddCommentReportAction(welcomeSignOffText, undefined, actorAccountID, tasksData.length + 3); const welcomeSignOffCommentAction: OptimisticAddCommentReportAction = welcomeSignOffComment.reportAction; const welcomeSignOffMessage = { @@ -11369,7 +11259,7 @@ function getFieldViolationTranslation(reportField: PolicyReportField, violation? switch (violation) { case 'fieldRequired': - return 'reportViolations.fieldRequired'; + return translateLocal('reportViolations.fieldRequired', {fieldName: reportField.name}); default: return ''; } @@ -11893,7 +11783,7 @@ function hasReportBeenRetracted(report: OnyxEntry, reportActions?: OnyxE function getMoneyReportPreviewName(action: ReportAction, iouReport: OnyxEntry, isInvoice?: boolean, reportAttributes?: ReportAttributesDerivedValue['reports']) { if (isInvoice && isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW)) { const originalMessage = getOriginalMessage(action); - return originalMessage && 'iou.invoiceReportName'; + return originalMessage && translateLocal('iou.invoiceReportName', originalMessage); } return getReportName(iouReport, undefined, undefined, undefined, undefined, reportAttributes) || action.childReportName; } @@ -11928,7 +11818,7 @@ function buildOptimisticRejectReportAction(created = DateUtils.getDBTime()): Opt { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: 'iou.reject.reportActions.rejectedExpense', + text: translateLocal('iou.reject.reportActions.rejectedExpense'), }, ], person: [ @@ -11990,7 +11880,7 @@ function buildOptimisticMarkedAsResolvedReportAction(created = DateUtils.getDBTi { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: 'iou.reject.reportActions.markedAsResolved', + text: translateLocal('iou.reject.reportActions.markedAsResolved'), }, ], person: [ @@ -12029,23 +11919,23 @@ function getReportStatusTranslation(stateNum?: number, statusNum?: number): stri } if (stateNum === CONST.REPORT.STATE_NUM.OPEN && statusNum === CONST.REPORT.STATUS_NUM.OPEN) { - return 'common.draft'; + return translateLocal('common.draft'); } if (stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED) { - return 'common.outstanding'; + return translateLocal('common.outstanding'); } if (stateNum === CONST.REPORT.STATE_NUM.APPROVED && statusNum === CONST.REPORT.STATUS_NUM.CLOSED) { - return 'common.done'; + return translateLocal('common.done'); } if (stateNum === CONST.REPORT.STATE_NUM.APPROVED && statusNum === CONST.REPORT.STATUS_NUM.APPROVED) { - return 'iou.approved'; + return translateLocal('iou.approved'); } if ( (stateNum === CONST.REPORT.STATE_NUM.APPROVED && statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED) || (stateNum === CONST.REPORT.STATE_NUM.BILLING && statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED) || (stateNum === CONST.REPORT.STATE_NUM.AUTOREIMBURSED && statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED) ) { - return 'iou.settledExpensify'; + return translateLocal('iou.settledExpensify'); } return ''; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 3b4dd1943994..c7b0edd6d3b3 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -237,7 +237,6 @@ import { import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import type {IOUAction, IOUActionParams, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; -import type {TranslationParameters, TranslationPaths} from '@src/languages/types'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -1465,10 +1464,7 @@ type BuildOnyxDataForTestDriveIOUParams = { testDriveCommentReportActionID?: string; }; -function buildOnyxDataForTestDriveIOU( - testDriveIOUParams: BuildOnyxDataForTestDriveIOUParams, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -): OnyxData { +function buildOnyxDataForTestDriveIOU(testDriveIOUParams: BuildOnyxDataForTestDriveIOUParams): OnyxData { const optimisticData: OnyxUpdate[] = []; const successData: OnyxUpdate[] = []; const failureData: OnyxUpdate[] = []; @@ -1485,7 +1481,7 @@ function buildOnyxDataForTestDriveIOU( reportActionID: testDriveIOUParams.iouOptimisticParams.action.reportActionID, }); - const text = translate?.('testDrive.employeeInviteMessage', {name: personalDetailsList?.[userAccountID]?.firstName ?? ''}) ?? 'testDrive.employeeInviteMessage'; + const text = Localize.translateLocal('testDrive.employeeInviteMessage', {name: personalDetailsList?.[userAccountID]?.firstName ?? ''}); const textComment = buildOptimisticAddCommentReportAction(text, undefined, userAccountID, undefined, undefined, undefined, testDriveIOUParams.testDriveCommentReportActionID); textComment.reportAction.created = DateUtils.subtractMillisecondsFromDateTime(testDriveIOUParams.iouOptimisticParams.createdAction.created, 1); @@ -4224,10 +4220,7 @@ type GetUpdateMoneyRequestParamsType = { shouldBuildOptimisticModifiedExpenseReportAction?: boolean; }; -function getUpdateMoneyRequestParams( - params: GetUpdateMoneyRequestParamsType, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, -): UpdateMoneyRequestData { +function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): UpdateMoneyRequestData { const { transactionID, transactionThreadReportID, @@ -4249,9 +4242,7 @@ function getUpdateMoneyRequestParams( // Step 1: Set any "pending fields" (ones updated while the user was offline) to have error messages in the failureData const pendingFields: OnyxTypes.Transaction['pendingFields'] = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE])); const clearedPendingFields = getClearedPendingFields(transactionChanges); - const errorFields = Object.fromEntries( - Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: translate?.('iou.error.genericEditFailureMessage') ?? 'iou.error.genericEditFailureMessage'}]), - ); + const errorFields = Object.fromEntries(Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericEditFailureMessage')}])); // Step 2: Get all the collections being updated const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; @@ -4705,7 +4696,6 @@ function getUpdateTrackExpenseParams( transactionChanges: TransactionChanges, policy: OnyxEntry, shouldBuildOptimisticModifiedExpenseReportAction = true, - translate?: (path: TPath, ...parameters: TranslationParameters) => string, ): UpdateMoneyRequestData { const optimisticData: OnyxUpdate[] = []; const successData: OnyxUpdate[] = []; @@ -4714,9 +4704,7 @@ function getUpdateTrackExpenseParams( // Step 1: Set any "pending fields" (ones updated while the user was offline) to have error messages in the failureData const pendingFields = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE])); const clearedPendingFields = getClearedPendingFields(transactionChanges); - const errorFields = Object.fromEntries( - Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: translate?.('iou.error.genericEditFailureMessage') ?? 'iou.error.genericEditFailureMessage'}]), - ); + const errorFields = Object.fromEntries(Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericEditFailureMessage')}])); // Step 2: Get all the collections being updated const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; @@ -6621,8 +6609,7 @@ function createSplitsAndOnyxData({ attendees, }, policyRecentlyUsedCategories, - translate, -}: CreateSplitsAndOnyxDataParams & {translate?: (path: TPath, ...parameters: TranslationParameters) => string}): SplitsAndOnyxData { +}: CreateSplitsAndOnyxDataParams): SplitsAndOnyxData { const currentUserEmailForIOUSplit = addSMSDomainIfPhoneNumber(currentUserLogin); const participantAccountIDs = participants.map((participant) => Number(participant.accountID)); @@ -6642,7 +6629,7 @@ function createSplitsAndOnyxData({ reportID: CONST.REPORT.SPLIT_REPORT_ID, comment, created, - merchant: merchant || (translate?.('iou.expense') ?? 'iou.expense'), + merchant: merchant || Localize.translateLocal('iou.expense'), receipt, category, tag, @@ -6926,7 +6913,7 @@ function createSplitsAndOnyxData({ reportID: oneOnOneIOUReport.reportID, comment, created, - merchant: merchant || (translate?.('iou.expense') ?? 'iou.expense'), + merchant: merchant || Localize.translateLocal('iou.expense'), category, tag, taxCode, @@ -8320,8 +8307,10 @@ function prepareToCleanUpMoneyRequest( } const hasNonReimbursableTransactions = hasNonReimbursableTransactionsReportUtils(iouReport?.reportID); - const messageKey = hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount'; - const messageText = messageKey; // TODO: Add translate parameter when calling this function + const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { + payer: getPersonalDetailsForAccountID(updatedIOUReport?.managerID ?? CONST.DEFAULT_NUMBER_ID).login ?? '', + amount: convertToDisplayString(updatedIOUReport?.total, updatedIOUReport?.currency), + }); if (getReportActionMessage(updatedReportPreviewAction)) { if (Array.isArray(updatedReportPreviewAction?.message)) { @@ -8609,7 +8598,6 @@ function deleteMoneyRequest( isSingleTransactionView = false, transactionIDsPendingDeletion?: string[], selectedTransactionIDs?: string[], - translate?: (path: TPath, ...parameters: TranslationParameters) => string, ) { if (!transactionID) { return; @@ -8830,7 +8818,7 @@ function deleteMoneyRequest( ...reportAction, pendingAction: null, errors: { - [errorKey]: translate?.('iou.error.genericDeleteFailureMessage') ?? 'iou.error.genericDeleteFailureMessage', + [errorKey]: Localize.translateLocal('iou.error.genericDeleteFailureMessage'), }, }, }, @@ -8857,7 +8845,7 @@ function deleteMoneyRequest( ...reportPreviewAction, pendingAction: null, errors: { - [errorKey]: translate?.('iou.error.genericDeleteFailureMessage') ?? 'iou.error.genericDeleteFailureMessage', + [errorKey]: Localize.translateLocal('iou.error.genericDeleteFailureMessage'), }, }, }, diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 9b64df0c908b..a9c85a9bc59d 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -8,7 +8,6 @@ import type {OnyxCollection, OnyxEntry, OnyxInputValue} from 'react-native-onyx' import Onyx from 'react-native-onyx'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; import type {SearchQueryJSON, SearchStatus} from '@components/Search/types'; -import useLocalize from '@hooks/useLocalize'; import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; import type {PerDiemExpenseTransactionParams, RequestMoneyParticipantParams} from '@libs/actions/IOU'; import { @@ -60,6 +59,7 @@ import {subscribeToUserEvents} from '@libs/actions/User'; import type {ApiCommand} from '@libs/API/types'; import {WRITE_COMMANDS} from '@libs/API/types'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; +import {translateLocal} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import {rand64} from '@libs/NumberUtils'; import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; @@ -1642,8 +1642,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connection); expect(transaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(transaction?.errors).toBeTruthy(); - const {result} = renderHook(() => useLocalize()); - expect(Object.values(transaction?.errors ?? {}).at(0)).toEqual(result.current.translate('iou.error.genericCreateFailureMessage')); + expect(Object.values(transaction?.errors ?? {}).at(0)).toEqual(translateLocal('iou.error.genericCreateFailureMessage')); resolve(); }, }); @@ -1945,8 +1944,7 @@ describe('actions/IOU', () => { const accountantEmployee = policyData?.employeeList?.[accountant.email]; expect(accountantEmployee).toBeTruthy(); expect(accountantEmployee?.errors).toBeTruthy(); - const {result} = renderHook(() => useLocalize()); - expect(Object.values(accountantEmployee?.errors ?? {}).at(0)).toEqual(result.current.translate('workspace.people.error.genericAdd')); + expect(Object.values(accountantEmployee?.errors ?? {}).at(0)).toEqual(translateLocal('workspace.people.error.genericAdd')); // Cleanup mockFetch?.succeed?.(); @@ -3462,8 +3460,7 @@ describe('actions/IOU', () => { callback: (allActions) => { Onyx.disconnect(connection); const erroredAction = Object.values(allActions ?? {}).find((action) => !isEmptyObject(action?.errors)); - const {result} = renderHook(() => useLocalize()); - expect(Object.values(erroredAction?.errors ?? {}).at(0)).toEqual(result.current.translate('iou.error.other')); + expect(Object.values(erroredAction?.errors ?? {}).at(0)).toEqual(translateLocal('iou.error.other')); resolve(); }, }); From 0f799065662efd9e4bfe433427a8ef8306e159ab Mon Sep 17 00:00:00 2001 From: fahimj Date: Sat, 25 Oct 2025 20:43:59 +0700 Subject: [PATCH 12/16] Refactor IOU actions to include policy and account details in approveMoneyRequest calls --- src/libs/actions/IOU.ts | 1 - tests/actions/IOUTest.ts | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 776bb3cb9699..4a832a5c3c70 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -173,7 +173,6 @@ import { hasOutstandingChildRequest, hasReportBeenReopened, hasReportBeenRetracted, - hasViolations as hasViolationsReportUtils, isArchivedReport, isClosedReport as isClosedReportUtil, isDraftReport, diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 180193ee8a2d..dc8afbdff1fc 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -9341,7 +9341,7 @@ describe('actions/IOU', () => { }); // Admin approves the report - approveMoneyRequest(expenseReport); + approveMoneyRequest(expenseReport, policy, adminAccountID, adminEmail, false, false); await waitForBatchedUpdates(); // Should be approved since admin took control and is the last approver @@ -9379,7 +9379,7 @@ describe('actions/IOU', () => { }); // Manager approves the report - approveMoneyRequest(expenseReport); + approveMoneyRequest(expenseReport, policy, managerAccountID, managerEmail, false, false); await waitForBatchedUpdates(); // Should be submitted to senior manager (normal flow) since take control was invalidated @@ -9413,7 +9413,7 @@ describe('actions/IOU', () => { }); // Admin approves the report - approveMoneyRequest(expenseReport); + approveMoneyRequest(expenseReport, policy, adminAccountID, adminEmail, false, false); await waitForBatchedUpdates(); // Get the optimistic next step @@ -9523,7 +9523,7 @@ describe('actions/IOU', () => { }); // Manager approves the report (no take control actions) - approveMoneyRequest(expenseReport); + approveMoneyRequest(expenseReport, policy, managerAccountID, managerEmail, false, false); await waitForBatchedUpdates(); // Should be submitted to admin (next in approval chain) since manager is not the final approver @@ -9540,7 +9540,7 @@ describe('actions/IOU', () => { accountID: managerAccountID, }); - approveMoneyRequest(expenseReport); + approveMoneyRequest(expenseReport, policy, managerAccountID, managerEmail, false, false); await waitForBatchedUpdates(); // Should be submitted to admin @@ -9555,7 +9555,7 @@ describe('actions/IOU', () => { accountID: adminAccountID, }); - approveMoneyRequest(updatedReport); + approveMoneyRequest(updatedReport, policy, adminAccountID, adminEmail, false, false); await waitForBatchedUpdates(); // Should be fully approved @@ -9600,7 +9600,7 @@ describe('actions/IOU', () => { }); // Manager approves the report - approveMoneyRequest(singleApproverReport); + approveMoneyRequest(singleApproverReport, singleApproverPolicy, managerAccountID, managerEmail, false, false); await waitForBatchedUpdates(); // Should be fully approved since manager is the final approver in the chain From 48e869d026e7c21bff9598cc7886e69be3eec608 Mon Sep 17 00:00:00 2001 From: fahimj Date: Sat, 25 Oct 2025 20:58:53 +0700 Subject: [PATCH 13/16] Refactor approveMoneyRequest function to simplify parameter passing for current user account and email --- src/libs/actions/IOU.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 4a832a5c3c70..b52a13e72e50 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -10210,8 +10210,8 @@ function approveMoneyRequest( const optimisticNextStep = buildNextStepNew({ report: expenseReport, policy, - currentUserAccountIDParam: userAccountID, - currentUserEmailParam: currentUserEmail, + currentUserAccountIDParam, + currentUserEmailParam, hasViolations, isASAPSubmitBetaEnabled, predictedNextStatus, From ebf8ae54b81df8e60725026f0d33977884cc9a5c Mon Sep 17 00:00:00 2001 From: fahimj Date: Mon, 27 Oct 2025 16:10:31 +0700 Subject: [PATCH 14/16] Update getNextApproverAccountID function to return submitToAccountID as a fallback for managerID --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ab06dd28c58a..ccd26a3e9df9 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4407,7 +4407,7 @@ function getNextApproverAccountID(report: OnyxEntry, isUnapproved = fals return currentUserAccountID; } - return report?.managerID; + return report?.managerID ?? submitToAccountID; } if (approvalChain.length === 0) { From 0d0f1b83fc9430dfe9b8170d868f40d7d3d18808 Mon Sep 17 00:00:00 2001 From: fahimj Date: Thu, 30 Oct 2025 12:58:41 +0700 Subject: [PATCH 15/16] fix: Disable ESLint warning for deprecated InteractionManager usage in ReportUtils --- src/libs/ReportUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b8689d2f0a21..b6735c77cbbb 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1005,6 +1005,7 @@ Onyx.connect({ return acc; } + // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { handlePreexistingReport(report); }); From a6dba16ca8e2a7435b027e0ef7392b5e01c0ba2f Mon Sep 17 00:00:00 2001 From: fahimj Date: Tue, 4 Nov 2025 03:48:49 +0700 Subject: [PATCH 16/16] refactor: Rename function and variable name --- src/libs/ReportUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b6735c77cbbb..8a1f8642e0b7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4396,9 +4396,9 @@ function getNextApproverAccountID(report: OnyxEntry, isUnapproved = fals // If the current user took control, then they are the final approver and we don't have a next approver // If someone else took control or rerouted, they are the next approver - const bypassApprover = getBypassApproverIfTakenControl(report); - if (bypassApprover) { - return bypassApprover === currentUserAccountID && !isUnapproved ? undefined : bypassApprover; + const bypassApproverAccountID = getBypassApproverAccountIDIfTakenControl(report); + if (bypassApproverAccountID) { + return bypassApproverAccountID === currentUserAccountID && !isUnapproved ? undefined : bypassApproverAccountID; } const approvalChain = getApprovalChain(policy, report); @@ -11670,7 +11670,7 @@ function isWorkspaceEligibleForReportChange(submitterEmail: string | undefined, * Checks if someone took control of the report and if that take control is still valid * A take control is invalidated if there's a SUBMITTED action after it */ -function getBypassApproverIfTakenControl(expenseReport: OnyxEntry): number | null { +function getBypassApproverAccountIDIfTakenControl(expenseReport: OnyxEntry): number | null { if (!expenseReport?.reportID) { return null; }