From 4e03c74a1b422ab85a9f54987c36f9d3b6ed158e Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Wed, 29 Jul 2026 11:43:07 +0430 Subject: [PATCH 1/4] Migrate change approver formatting --- src/libs/ReportUtils.ts | 8 +++- src/libs/actions/IOU/ReportWorkflow.ts | 8 +++- src/pages/DynamicReportChangeApproverPage.tsx | 27 +++++++++++-- src/pages/ReportAddApproverPage.tsx | 1 + src/pages/Search/SearchAddApproverPage.tsx | 1 + src/pages/Search/SearchChangeApproverPage.tsx | 14 ++++++- tests/actions/IOUTest/ReportWorkflowTest.ts | 40 ++++++++++++++++++- 7 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9f35983d5118..07e362479124 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8751,7 +8751,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, @@ -8761,7 +8765,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..56d74e4beac1 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({ @@ -1849,8 +1852,9 @@ function addReportApprover( isASAPSubmitBetaEnabled: boolean, reportCurrentNextStepDeprecated: OnyxEntry, isTrackIntentUser: boolean | undefined, + formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], ) { - const takeControlReportAction = buildOptimisticChangeApproverReportAction(newApproverAccountID, accountID); + 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..61cd35b66162 100644 --- a/src/pages/ReportAddApproverPage.tsx +++ b/src/pages/ReportAddApproverPage.tsx @@ -112,6 +112,7 @@ function ReportAddApproverPage({report, isLoadingReportData, policy}: ReportAddA isASAPSubmitBetaEnabled, reportNextStep, isTrackIntentUser, + formatPhoneNumber, ); Navigation.dismissToPreviousRHP(); }); diff --git a/src/pages/Search/SearchAddApproverPage.tsx b/src/pages/Search/SearchAddApproverPage.tsx index 02261cfd94a1..3b3b56ee845f 100644 --- a/src/pages/Search/SearchAddApproverPage.tsx +++ b/src/pages/Search/SearchAddApproverPage.tsx @@ -151,6 +151,7 @@ function SearchAddApproverPage() { isASAPSubmitBetaEnabled, reportNextStep, isTrackIntentUser, + formatPhoneNumber, ); } 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 8287df07d1b7..05e019312128 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'; @@ -3086,6 +3087,43 @@ 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, approverLogin, approverAccountID, RORY_ACCOUNT_ID, RORY_EMAIL, policy, false, false, undefined, false, 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'; From b257c8715ce572601de477afeae1697e56faf45a Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Wed, 29 Jul 2026 12:07:51 +0430 Subject: [PATCH 2/4] fixed lint failure --- src/libs/actions/IOU/ReportWorkflow.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index 56d74e4beac1..b5b70e445021 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -1841,6 +1841,7 @@ function assignReportToMe( API.write(WRITE_COMMANDS.ASSIGN_REPORT_TO_ME, params, onyxData); } +// eslint-disable-next-line @typescript-eslint/max-params function addReportApprover( report: OnyxTypes.Report, newApproverEmail: string, From 1cf2d6094361d662a78a99bfb9b01367a70c11d3 Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Thu, 30 Jul 2026 10:59:14 +0430 Subject: [PATCH 3/4] Fix add approver formatter params lint --- src/libs/actions/IOU/ReportWorkflow.ts | 41 ++++++++++++++------- src/pages/ReportAddApproverPage.tsx | 14 +++---- src/pages/Search/SearchAddApproverPage.tsx | 14 +++---- tests/actions/IOUTest/ReportWorkflowTest.ts | 14 ++++++- 4 files changed, 54 insertions(+), 29 deletions(-) diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index b5b70e445021..905211604f84 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -1841,20 +1841,33 @@ function assignReportToMe( API.write(WRITE_COMMANDS.ASSIGN_REPORT_TO_ME, params, onyxData); } -// eslint-disable-next-line @typescript-eslint/max-params -function addReportApprover( - report: OnyxTypes.Report, - newApproverEmail: string, - newApproverAccountID: number, - accountID: number, - email: string, - policy: OnyxEntry, - hasViolations: boolean, - isASAPSubmitBetaEnabled: boolean, - reportCurrentNextStepDeprecated: OnyxEntry, - isTrackIntentUser: boolean | undefined, - formatPhoneNumber: LocaleContextProps['formatPhoneNumber'], -) { +type AddReportApproverOptions = { + report: OnyxTypes.Report; + newApproverEmail: string; + newApproverAccountID: number; + accountID: number; + email: string; + policy: OnyxEntry; + hasViolations: boolean; + isASAPSubmitBetaEnabled: boolean; + reportCurrentNextStepDeprecated: OnyxEntry; + isTrackIntentUser: boolean | undefined; + 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 diff --git a/src/pages/ReportAddApproverPage.tsx b/src/pages/ReportAddApproverPage.tsx index 61cd35b66162..26c4dee4bbe5 100644 --- a/src/pages/ReportAddApproverPage.tsx +++ b/src/pages/ReportAddApproverPage.tsx @@ -101,19 +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 3b3b56ee845f..bbcd0cf0e236 100644 --- a/src/pages/Search/SearchAddApproverPage.tsx +++ b/src/pages/Search/SearchAddApproverPage.tsx @@ -140,19 +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/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index 05e019312128..86ce07a8eb74 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -3113,7 +3113,19 @@ describe('actions/IOU/ReportWorkflow', () => { }); await waitForBatchedUpdates(); - addReportApprover(report, approverLogin, approverAccountID, RORY_ACCOUNT_ID, RORY_EMAIL, policy, false, false, undefined, false, formatPhoneNumberSpy); + 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); From d23a34246a2d95cc4e974152c0ffec8600380bcd Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Fri, 31 Jul 2026 00:18:41 +0430 Subject: [PATCH 4/4] Document add approver params --- src/libs/actions/IOU/ReportWorkflow.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index 905211604f84..4ce049d684f6 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -1842,16 +1842,37 @@ function assignReportToMe( } 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']; };