diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a0c8f81a4359..787b33bbb99e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8756,7 +8756,11 @@ function buildOptimisticResolvedDuplicatesReportAction(): OptimisticDismissedVio }; } -function buildOptimisticChangeApproverReportAction(managerID: number, actorAccountID: number): OptimisticChangedApproverReportAction { +function buildOptimisticChangeApproverReportAction( + managerID: number, + actorAccountID: number, + formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], +): OptimisticChangedApproverReportAction { const created = DateUtils.getDBTime(); return { actionName: managerID === actorAccountID ? CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL : CONST.REPORT.ACTIONS.TYPE.REROUTE, @@ -8766,7 +8770,7 @@ function buildOptimisticChangeApproverReportAction(managerID: number, actorAccou message: [ { type: CONST.REPORT.MESSAGE.TYPE.COMMENT, - text: `changed the approver to ${getDisplayNameForParticipant({accountID: managerID, formatPhoneNumber: formatPhoneNumberPhoneUtils})}`, + text: `changed the approver to ${getDisplayNameForParticipant({accountID: managerID, formatPhoneNumber})}`, html: `changed the approver to `, }, ], diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index fda76294ea2d..4ce049d684f6 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -1,3 +1,5 @@ +import type {LocaleContextProps} from '@components/LocaleContextProvider'; + import * as API from '@libs/API'; import type { AddReportApproverParams, @@ -1731,8 +1733,9 @@ function assignReportToMe( isASAPSubmitBetaEnabled: boolean, reportCurrentNextStepDeprecated: OnyxEntry, isTrackIntentUser: boolean | undefined, + formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], ) { - const takeControlReportAction = buildOptimisticChangeApproverReportAction(accountID, accountID); + const takeControlReportAction = buildOptimisticChangeApproverReportAction(accountID, accountID, formatPhoneNumber); // buildOptimisticNextStep is used in parallel const optimisticNextStepDeprecated = buildNextStepNew({ @@ -1838,19 +1841,55 @@ function assignReportToMe( API.write(WRITE_COMMANDS.ASSIGN_REPORT_TO_ME, params, onyxData); } -function addReportApprover( - report: OnyxTypes.Report, - newApproverEmail: string, - newApproverAccountID: number, - accountID: number, - email: string, - policy: OnyxEntry, - hasViolations: boolean, - isASAPSubmitBetaEnabled: boolean, - reportCurrentNextStepDeprecated: OnyxEntry, - isTrackIntentUser: boolean | undefined, -) { - const takeControlReportAction = buildOptimisticChangeApproverReportAction(newApproverAccountID, accountID); +type AddReportApproverOptions = { + /** Expense report whose approver is being changed. */ + report: OnyxTypes.Report; + + /** Email of the approver being added. */ + newApproverEmail: string; + + /** Account ID of the approver being added. */ + newApproverAccountID: number; + + /** Account ID of the user changing the approver. */ + accountID: number; + + /** Email of the user changing the approver. */ + email: string; + + /** Policy associated with the expense report. */ + policy: OnyxEntry; + + /** Whether the report currently has violations. */ + hasViolations: boolean; + + /** Whether ASAP Submit beta behavior is enabled. */ + isASAPSubmitBetaEnabled: boolean; + + /** Existing next-step data used to roll back optimistic updates on failure. */ + reportCurrentNextStepDeprecated: OnyxEntry; + + /** Whether the current user is in the track intent onboarding state. */ + isTrackIntentUser: boolean | undefined; + + /** Locale-aware formatter used for optimistic approver display names. */ + formatPhoneNumber: LocaleContextProps['formatPhoneNumber']; +}; + +function addReportApprover({ + report, + newApproverEmail, + newApproverAccountID, + accountID, + email, + policy, + hasViolations, + isASAPSubmitBetaEnabled, + reportCurrentNextStepDeprecated, + isTrackIntentUser, + formatPhoneNumber, +}: AddReportApproverOptions) { + const takeControlReportAction = buildOptimisticChangeApproverReportAction(newApproverAccountID, accountID, formatPhoneNumber); // buildOptimisticNextStep is used in parallel const optimisticNextStepDeprecated = buildNextStepNew({ diff --git a/src/pages/DynamicReportChangeApproverPage.tsx b/src/pages/DynamicReportChangeApproverPage.tsx index 866df5337ada..00283b354219 100644 --- a/src/pages/DynamicReportChangeApproverPage.tsx +++ b/src/pages/DynamicReportChangeApproverPage.tsx @@ -49,7 +49,7 @@ type DynamicReportChangeApproverPageProps = WithReportOrNotFoundProps & Platform function DynamicReportChangeApproverPage({report, policy, isLoadingReportData}: DynamicReportChangeApproverPageProps) { const reportID = report?.reportID; - const {translate} = useLocalize(); + const {translate, formatPhoneNumber} = useLocalize(); const styles = useThemeStyles(); const {environmentURL} = useEnvironment(); const currentUserDetails = useCurrentUserPersonalDetails(); @@ -89,9 +89,30 @@ function DynamicReportChangeApproverPage({report, policy, isLoadingReportData}: Navigation.navigate(ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.getRoute(report.reportID)); return; } - assignReportToMe(report, currentUserDetails.accountID, currentUserDetails.email ?? '', policy, hasViolations, isASAPSubmitBetaEnabled, reportNextStep, isTrackIntentUser); + assignReportToMe( + report, + currentUserDetails.accountID, + currentUserDetails.email ?? '', + policy, + hasViolations, + isASAPSubmitBetaEnabled, + reportNextStep, + isTrackIntentUser, + formatPhoneNumber, + ); Navigation.dismissToPreviousRHP(); - }, [selectedApproverType, report, currentUserDetails.accountID, currentUserDetails.email, policy, hasViolations, isASAPSubmitBetaEnabled, reportNextStep, isTrackIntentUser]); + }, [ + selectedApproverType, + report, + currentUserDetails.accountID, + currentUserDetails.email, + policy, + hasViolations, + isASAPSubmitBetaEnabled, + reportNextStep, + isTrackIntentUser, + formatPhoneNumber, + ]); const approverTypes = useMemo(() => { const data: Array> = [ diff --git a/src/pages/ReportAddApproverPage.tsx b/src/pages/ReportAddApproverPage.tsx index d53219ba6663..26c4dee4bbe5 100644 --- a/src/pages/ReportAddApproverPage.tsx +++ b/src/pages/ReportAddApproverPage.tsx @@ -101,18 +101,19 @@ function ReportAddApproverPage({report, isLoadingReportData, policy}: ReportAddA return; } startWithLoading(() => { - addReportApprover( + addReportApprover({ report, - selectedApproverEmail, - Number(employeeAccountID), - currentUserDetails.accountID, - currentUserDetails.email ?? '', + newApproverEmail: selectedApproverEmail, + newApproverAccountID: Number(employeeAccountID), + accountID: currentUserDetails.accountID, + email: currentUserDetails.email ?? '', policy, hasViolations, isASAPSubmitBetaEnabled, - reportNextStep, + reportCurrentNextStepDeprecated: reportNextStep, isTrackIntentUser, - ); + formatPhoneNumber, + }); Navigation.dismissToPreviousRHP(); }); }; diff --git a/src/pages/Search/SearchAddApproverPage.tsx b/src/pages/Search/SearchAddApproverPage.tsx index 02261cfd94a1..bbcd0cf0e236 100644 --- a/src/pages/Search/SearchAddApproverPage.tsx +++ b/src/pages/Search/SearchAddApproverPage.tsx @@ -140,18 +140,19 @@ function SearchAddApproverPage() { const hasViolations = hasViolationsReportUtils(report.reportID, transactionViolations, currentUserDetails.accountID, currentUserDetails.email ?? ''); const reportNextStep = allReportNextSteps?.[`${ONYXKEYS.COLLECTION.NEXT_STEP}${selectedReport.reportID}`]; - addReportApprover( + addReportApprover({ report, - selectedApproverEmail, - Number(employeeAccountID), - currentUserDetails.accountID, - currentUserDetails.email ?? '', + newApproverEmail: selectedApproverEmail, + newApproverAccountID: Number(employeeAccountID), + accountID: currentUserDetails.accountID, + email: currentUserDetails.email ?? '', policy, hasViolations, isASAPSubmitBetaEnabled, - reportNextStep, + reportCurrentNextStepDeprecated: reportNextStep, isTrackIntentUser, - ); + formatPhoneNumber, + }); } // Note: This clears both reports and transactions diff --git a/src/pages/Search/SearchChangeApproverPage.tsx b/src/pages/Search/SearchChangeApproverPage.tsx index 9a02523439cd..ec274094679e 100644 --- a/src/pages/Search/SearchChangeApproverPage.tsx +++ b/src/pages/Search/SearchChangeApproverPage.tsx @@ -73,7 +73,7 @@ function shouldAutoApplyApprover({ export {shouldAutoApplyApprover}; function SearchChangeApproverPage() { - const {translate} = useLocalize(); + const {translate, formatPhoneNumber} = useLocalize(); const styles = useThemeStyles(); const {environmentURL} = useEnvironment(); const currentUserDetails = useCurrentUserPersonalDetails(); @@ -175,7 +175,17 @@ function SearchChangeApproverPage() { if (report.managerID !== currentUserDetails.accountID) { const hasViolations = hasViolationsReportUtils(report.reportID, transactionViolations, currentUserDetails.accountID, currentUserDetails.email ?? ''); const reportNextStep = allReportNextSteps?.[`${ONYXKEYS.COLLECTION.NEXT_STEP}${selectedReport.reportID}`]; - assignReportToMe(report, currentUserDetails.accountID, currentUserDetails.email ?? '', policy, hasViolations, isASAPSubmitBetaEnabled, reportNextStep, isTrackIntentUser); + assignReportToMe( + report, + currentUserDetails.accountID, + currentUserDetails.email ?? '', + policy, + hasViolations, + isASAPSubmitBetaEnabled, + reportNextStep, + isTrackIntentUser, + formatPhoneNumber, + ); } } diff --git a/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index 8d8bb2131de4..e0564a9917a5 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -6,6 +6,7 @@ import OnyxListItemProvider from '@components/OnyxListItemProvider'; import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; import { + addReportApprover, approveMoneyRequest, canApproveIOU, canIOUBePaid, @@ -53,7 +54,7 @@ import createRandomReportAction from '../../utils/collections/reportActions'; import {createRandomReport} from '../../utils/collections/reports'; import createRandomTransaction from '../../utils/collections/transaction'; import getOnyxValue from '../../utils/getOnyxValue'; -import {createGlobalFetchMock, getOnyxData, getRequiredOnyxUpdate, getRequiredOnyxUpdates, getRequiredWriteCall, localeCompare} from '../../utils/TestHelper'; +import {createGlobalFetchMock, formatPhoneNumber, getOnyxData, getRequiredOnyxUpdate, getRequiredOnyxUpdates, getRequiredWriteCall, localeCompare} from '../../utils/TestHelper'; import {isObject} from '../../utils/typeGuards'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithAct'; @@ -3092,6 +3093,55 @@ describe('actions/IOU/ReportWorkflow', () => { }); }); + describe('change approver formatter forwarding', () => { + it('uses the injected formatter for the optimistic approver display name', async () => { + // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write optimistic data to verify formatter forwarding. + const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const approverAccountID = 8332403627; + const approverLogin = '+18332403627@expensify.sms'; + const formatPhoneNumberSpy = jest.fn(formatPhoneNumber); + const policy = createRandomPolicy(1); + const report: Report = { + ...createRandomReport(1, undefined), + reportID: 'change-approver-report', + type: CONST.REPORT.TYPE.EXPENSE, + policyID: policy.id, + managerID: CARLOS_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + }; + + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [approverAccountID]: { + accountID: approverAccountID, + login: approverLogin, + isOptimisticPersonalDetail: true, + }, + }); + await waitForBatchedUpdates(); + + addReportApprover({ + report, + newApproverEmail: approverLogin, + newApproverAccountID: approverAccountID, + accountID: RORY_ACCOUNT_ID, + email: RORY_EMAIL, + policy, + hasViolations: false, + isASAPSubmitBetaEnabled: false, + reportCurrentNextStepDeprecated: undefined, + isTrackIntentUser: false, + formatPhoneNumber: formatPhoneNumberSpy, + }); + + const [, , onyxData] = getRequiredWriteCall(apiWriteSpy.mock.calls, 0); + const reportActionsUpdate = getRequiredOnyxUpdate(onyxData, 'optimisticData', `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, Onyx.METHOD.MERGE, true); + const reportAction = getRequiredReportAction(reportActionsUpdate); + + expect(formatPhoneNumberSpy).toHaveBeenCalledWith(approverLogin); + expect(reportAction.message).toEqual([expect.objectContaining({text: `changed the approver to ${formatPhoneNumber(approverLogin)}`})]); + }); + }); + describe('approveMoneyRequest Submit workspace upgrade', () => { const submitPolicyID = 'submit-policy-id'; const teamPolicyID = 'team-policy-id';