Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/hooks/useCreateNewReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {accountIDSelector, emailSelector} from '@selectors/Session';
import {useCallback} from 'react';

import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
import useLocalize from './useLocalize';
import useOnyx from './useOnyx';
import usePermissions from './usePermissions';

Expand All @@ -18,6 +19,7 @@ import usePermissions from './usePermissions';
*/
function useCreateNewReport() {
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const {translate} = useLocalize();
const {isBetaEnabled} = usePermissions();
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector});
Expand All @@ -32,9 +34,11 @@ function useCreateNewReport() {
return useCallback(
(policyID: string, shouldDismissEmptyReportsConfirmation = false) => {
const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
return createNewReport(currentUserPersonalDetails, hasViolations, isASAPSubmitBetaEnabled, policy, betas, isTrackIntentUser, false, shouldDismissEmptyReportsConfirmation);
return createNewReport(currentUserPersonalDetails, hasViolations, isASAPSubmitBetaEnabled, policy, betas, isTrackIntentUser, false, shouldDismissEmptyReportsConfirmation, {
translate,
});
},
[betas, currentUserPersonalDetails, hasViolations, isASAPSubmitBetaEnabled, policies, isTrackIntentUser],
[betas, currentUserPersonalDetails, hasViolations, isASAPSubmitBetaEnabled, policies, isTrackIntentUser, translate],
);
}

Expand Down
19 changes: 11 additions & 8 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5927,7 +5927,7 @@ function getReportPreviewMessage(translate: LocalizedTranslate, params: GetRepor
* IMPORTANT: keep the English strings here in sync with the `iou.*` entries in `en.ts` and with the branching
* in {@link getReportPreviewMessage}.
*/
function getReportPreviewReportActionMessage(params: GetReportPreviewMessageBaseParams): string {
function getReportPreviewReportActionMessage(params: GetReportPreviewMessageBaseParams, translate?: LocalizedTranslate): string {
const {reportOrID, iouReportAction = null, shouldConsiderScanningReceiptOrPendingRoute = false, isPreviewMessageForParentChatReport = false, policy, isForListPreview = false} = params;
const originalReportAction = params.originalReportAction ?? iouReportAction;
const report = typeof reportOrID === 'string' ? getReport(reportOrID, deprecatedAllReports) : reportOrID;
Expand Down Expand Up @@ -6007,7 +6007,7 @@ function getReportPreviewReportActionMessage(params: GetReportPreviewMessageBase
const policyName = getPolicyName({report: parentReport ?? report, policy});
const payerName = isExpenseReport(report)
? policyName
: getDisplayNameForParticipant({accountID: report.managerID, shouldUseShortForm: !isPreviewMessageForParentChatReport, formatPhoneNumber: formatPhoneNumberPhoneUtils});
: getDisplayNameForParticipant({accountID: report.managerID, shouldUseShortForm: !isPreviewMessageForParentChatReport, formatPhoneNumber: formatPhoneNumberPhoneUtils, translate});

const formattedAmount = convertToDisplayString(totalAmount, report.currency);

Expand Down Expand Up @@ -6064,7 +6064,7 @@ function getReportPreviewReportActionMessage(params: GetReportPreviewMessageBase
let actualPayerName =
report.managerID === deprecatedCurrentUserAccountID && !isForListPreview
? ''
: getDisplayNameForParticipant({accountID: payerAccountID, shouldUseShortForm: true, formatPhoneNumber: formatPhoneNumberPhoneUtils});
: getDisplayNameForParticipant({accountID: payerAccountID, shouldUseShortForm: true, formatPhoneNumber: formatPhoneNumberPhoneUtils, translate});

actualPayerName = actualPayerName && isForListPreview && !isPreviewMessageForParentChatReport ? `${actualPayerName}:` : actualPayerName;
const payerDisplayName = isPreviewMessageForParentChatReport ? payerName : actualPayerName;
Expand All @@ -6091,7 +6091,8 @@ function getReportPreviewReportActionMessage(params: GetReportPreviewMessageBase
}

if (report.isWaitingOnBankAccount) {
const submitterDisplayName = getDisplayNameForParticipant({accountID: report.ownerAccountID, shouldUseShortForm: true, formatPhoneNumber: formatPhoneNumberPhoneUtils}) ?? '';
const submitterDisplayName =
getDisplayNameForParticipant({accountID: report.ownerAccountID, shouldUseShortForm: true, formatPhoneNumber: formatPhoneNumberPhoneUtils, translate}) ?? '';
return `started payment, but is waiting for ${submitterDisplayName} to add a bank account.`;
}

Expand Down Expand Up @@ -6120,13 +6121,13 @@ function getReportPreviewReportActionMessage(params: GetReportPreviewMessageBase
// 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 !== deprecatedCurrentUserAccountID
? getDisplayNameForParticipant({accountID: lastActorID, shouldUseShortForm: !isPreviewMessageForParentChatReport, formatPhoneNumber: formatPhoneNumberPhoneUtils})
? getDisplayNameForParticipant({accountID: lastActorID, shouldUseShortForm: !isPreviewMessageForParentChatReport, formatPhoneNumber: formatPhoneNumberPhoneUtils, translate})
: '';
return `${requestorName ? `${requestorName}: ` : ''}${amountToDisplay}${comment ? ` for ${comment}` : ''}`;
}

if (containsNonReimbursable) {
const ownerName = getDisplayNameForParticipant({accountID: report.ownerAccountID, formatPhoneNumber: formatPhoneNumberPhoneUtils}) ?? '';
const ownerName = getDisplayNameForParticipant({accountID: report.ownerAccountID, formatPhoneNumber: formatPhoneNumberPhoneUtils, translate}) ?? '';
return `${ownerName} spent ${formattedAmount}`;
}
return `${payerName ?? ''} owes ${formattedAmount}${comment ? ` for ${comment}` : ''}`;
Expand Down Expand Up @@ -7846,9 +7847,10 @@ function buildOptimisticReportPreview(
childReportID?: string,
reportActionID?: string,
delegateAccountIDParam: number | undefined = undefined,
translate?: LocalizedTranslate,
): ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW> {
const hasReceipt = hasReceiptTransactionUtils(transaction);
const message = getReportPreviewReportActionMessage({reportOrID: iouReport});
const message = getReportPreviewReportActionMessage({reportOrID: iouReport}, translate);
const created = DateUtils.getDBTime();
const reportActorAccountID = (isInvoiceReport(iouReport) || isExpenseReport(iouReport) ? iouReport?.ownerAccountID : iouReport?.managerID) ?? -1;
// Falls back to module-level delegateEmail (from Onyx.connect) for callers not yet migrated; will be removed in https://github.com/Expensify/App/issues/66425
Expand Down Expand Up @@ -8024,6 +8026,7 @@ function updateReportPreview(
isPayRequest = false,
comment = '',
transaction?: OnyxEntry<Transaction>,
translate?: LocalizedTranslate,
): ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW> {
const hasReceipt = hasReceiptTransactionUtils(transaction);
const recentReceiptTransactions = reportPreviewAction?.childRecentReceiptTransactionIDs ?? {};
Expand All @@ -8039,7 +8042,7 @@ function updateReportPreview(
}
}

const message = getReportPreviewReportActionMessage({reportOrID: iouReport, iouReportAction: reportPreviewAction});
const message = getReportPreviewReportActionMessage({reportOrID: iouReport, iouReportAction: reportPreviewAction}, translate);
const originalMessage = getOriginalMessage(reportPreviewAction);
return {
...reportPreviewAction,
Expand Down
1 change: 1 addition & 0 deletions src/libs/actions/IOU/Duplicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,7 @@ function duplicateReport({
const newReportName = translate('common.copyOfReportName', sourceReportName);
const {reportPreviewReportActionID, ...newReport} = createNewReport(ownerPersonalDetails, false, isASAPSubmitBetaEnabled, targetPolicy, betas, isTrackIntentUser, false, undefined, {
reportName: newReportName,
translate,
});

const isCrossWorkspace = !!sourceReport && sourceReport.policyID !== targetPolicy.id;
Expand Down
2 changes: 1 addition & 1 deletion src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4650,7 +4650,7 @@ function createWorkspaceFromIOUPayment({
message: [
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
text: ReportUtils.getReportPreviewReportActionMessage({reportOrID: expenseReport, policy: newWorkspace}),
text: ReportUtils.getReportPreviewReportActionMessage({reportOrID: expenseReport, policy: newWorkspace}, localeTranslate),
},
],
created: DateUtils.getDBTime(),
Expand Down
62 changes: 43 additions & 19 deletions src/libs/actions/Report/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4093,18 +4093,33 @@ function navigateToConciergeChat(
}
}

function buildNewReportOptimisticData(
policy: OnyxEntry<Policy>,
reportID: string,
reportActionID: string,
ownerPersonalDetails: CurrentUserPersonalDetails,
reportPreviewReportActionID: string,
hasViolationsParam: boolean,
isASAPSubmitBetaEnabled: boolean,
betas: OnyxEntry<Beta[]>,
isTrackIntentUser: boolean | undefined,
reportName?: string,
) {
type BuildNewReportOptimisticDataParams = {
policy: OnyxEntry<Policy>;
reportID: string;
reportActionID: string;
ownerPersonalDetails: CurrentUserPersonalDetails;
reportPreviewReportActionID: string;
hasViolationsParam: boolean;
isASAPSubmitBetaEnabled: boolean;
betas: OnyxEntry<Beta[]>;
isTrackIntentUser: boolean | undefined;
reportName?: string;
translate?: LocalizedTranslate;
};

function buildNewReportOptimisticData({
policy,
reportID,
reportActionID,
ownerPersonalDetails,
reportPreviewReportActionID,
hasViolationsParam,
isASAPSubmitBetaEnabled,
betas,
isTrackIntentUser,
reportName,
translate,
}: BuildNewReportOptimisticDataParams) {
const {accountID, login, email} = ownerPersonalDetails;
const timeOfCreation = DateUtils.getDBTime();
const parentReport = getPolicyExpenseChat(accountID, policy?.id);
Expand Down Expand Up @@ -4143,7 +4158,7 @@ function buildNewReportOptimisticData(
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
};

const message = getReportPreviewReportActionMessage({reportOrID: optimisticReportData});
const message = getReportPreviewReportActionMessage({reportOrID: optimisticReportData}, translate);
const createReportActionMessage = [
{
html: message,
Expand Down Expand Up @@ -4351,16 +4366,16 @@ function createNewReport(
isTrackIntentUser: boolean | undefined,
shouldNotifyNewAction = false,
shouldDismissEmptyReportsConfirmation?: boolean,
options: {managedCardTransactionID?: string; reportName?: string} = {},
options: {managedCardTransactionID?: string; reportName?: string; translate?: LocalizedTranslate} = {},
) {
const {managedCardTransactionID, reportName} = options;
const {managedCardTransactionID, reportName, translate} = options;
const optimisticReportID = generateReportID();
const reportActionID = rand64();
const reportPreviewReportActionID = rand64();

const {parentReportID, reportPreviewAction, optimisticData, successData, failureData, optimisticReportData} = buildNewReportOptimisticData(
const {parentReportID, reportPreviewAction, optimisticData, successData, failureData, optimisticReportData} = buildNewReportOptimisticData({
policy,
optimisticReportID,
reportID: optimisticReportID,
reportActionID,
ownerPersonalDetails,
reportPreviewReportActionID,
Expand All @@ -4369,7 +4384,8 @@ function createNewReport(
betas,
isTrackIntentUser,
reportName,
);
translate,
});

if (shouldDismissEmptyReportsConfirmation) {
Onyx.merge(ONYXKEYS.NVP_EMPTY_REPORTS_CONFIRMATION_DISMISSED, true);
Expand Down Expand Up @@ -7360,6 +7376,7 @@ function buildOptimisticChangePolicyData({
optimisticPolicyExpenseChatReport,
reportPreviewAction,
isTrackIntentUser,
translate,
}: {
report: Report;
parentReport: OnyxEntry<Report>;
Expand All @@ -7375,6 +7392,7 @@ function buildOptimisticChangePolicyData({
optimisticPolicyExpenseChatReport?: Report;
reportPreviewAction: OnyxEntry<ReportAction>;
isTrackIntentUser: boolean | undefined;
translate: LocalizedTranslate;
}) {
const optimisticData: Array<
OnyxUpdate<
Expand Down Expand Up @@ -7633,7 +7651,7 @@ function buildOptimisticChangePolicyData({
// and set it as a parent of the moved report
const policyExpenseChat = optimisticPolicyExpenseChatReport ?? getPolicyExpenseChat(report.ownerAccountID, policy.id);
// TODO: delegateAccountIDParam will be threaded in PR 15 (https://github.com/Expensify/App/issues/66425)
const optimisticReportPreviewAction = buildOptimisticReportPreview(policyExpenseChat, report, '', null, undefined, undefined, undefined);
const optimisticReportPreviewAction = buildOptimisticReportPreview(policyExpenseChat, report, '', null, undefined, undefined, undefined, translate);

const newPolicyExpenseChatReportID = policyExpenseChat?.reportID;

Expand Down Expand Up @@ -7876,6 +7894,7 @@ function changeReportPolicy({
isReportLastVisibleArchived = false,
reportPreviewAction,
isTrackIntentUser,
translate,
}: {
report: Report;
parentReport: OnyxEntry<Report>;
Expand All @@ -7891,6 +7910,7 @@ function changeReportPolicy({
isReportLastVisibleArchived?: boolean;
reportPreviewAction: OnyxEntry<ReportAction>;
isTrackIntentUser: boolean | undefined;
translate: LocalizedTranslate;
}) {
if (!report || !policy || report.policyID === policy.id || !isExpenseReport(report)) {
return;
Expand All @@ -7910,6 +7930,7 @@ function changeReportPolicy({
reportNextStep,
reportPreviewAction,
isTrackIntentUser,
translate,
});

const params = {
Expand Down Expand Up @@ -7944,6 +7965,7 @@ function changeReportPolicyAndInviteSubmitter({
reportActionsList,
reportPreviewAction,
isTrackIntentUser,
translate,
}: {
report: Report;
parentReport: OnyxEntry<Report>;
Expand All @@ -7960,6 +7982,7 @@ function changeReportPolicyAndInviteSubmitter({
reportActionsList: OnyxCollection<ReportActions>;
reportPreviewAction: OnyxEntry<ReportAction>;
isTrackIntentUser: boolean | undefined;
translate: LocalizedTranslate;
}) {
if (!report.reportID || !policy?.id || report.policyID === policy.id || !isExpenseReport(report) || !report.ownerAccountID || !submitterLogin) {
return;
Expand Down Expand Up @@ -8014,6 +8037,7 @@ function changeReportPolicyAndInviteSubmitter({
optimisticPolicyExpenseChatReport: membersChats.reportCreationData[submitterLogin],
reportPreviewAction,
isTrackIntentUser,
translate,
});

const optimisticData = [...optimisticAddMembersData, ...optimisticChangePolicyData];
Expand Down
2 changes: 2 additions & 0 deletions src/pages/DynamicReportChangeWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ function DynamicReportChangeWorkspacePage({report}: DynamicReportChangeWorkspace
reportActionsList: filteredReportActions,
reportPreviewAction,
isTrackIntentUser,
translate,
});
return;
}
Expand All @@ -172,6 +173,7 @@ function DynamicReportChangeWorkspacePage({report}: DynamicReportChangeWorkspace
isReportLastVisibleArchived,
reportPreviewAction,
isTrackIntentUser,
translate,
});
};

Expand Down
72 changes: 72 additions & 0 deletions tests/actions/PolicyTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7362,6 +7362,78 @@ describe('actions/Policy', () => {
isIOUReportUsingReportSpy.mockRestore();
});

it('updates the old chat report preview action with a message built from the provided localeTranslate', async () => {
await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID});
await waitForBatchedUpdates();

const employeeAccountID = 210;
const iouReport: Report = {
...createRandomReport(2, undefined),
reportID: '510',
type: CONST.REPORT.TYPE.IOU,
ownerAccountID: employeeAccountID,
chatReportID: '511',
policyID: 'oldPolicyID',
currency: CONST.CURRENCY.USD,
total: 1000,
isWaitingOnBankAccount: false,
};
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport);
await waitForBatchedUpdates();

const previewActionID = 'preview-action-510';
const reportPreviewAction = createMock<ReportAction>({
reportActionID: previewActionID,
actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW,
created: '2026-01-01 00:00:00',
});

const apiWriteSpy = jest.spyOn(APIModule, 'write').mockImplementation(() => Promise.resolve());
const isIOUReportUsingReportSpy = jest.spyOn(ReportUtils, 'isIOUReportUsingReport').mockReturnValue(true);

Policy.createWorkspaceFromIOUPayment({
iouReport,
reportPreviewAction,
currentUserAccountID: 999,
currentUserEmail: 'custom@example.com',
iouReportOwnerEmail: 'owner@example.com',
currentUserLocalCurrency: CONST.CURRENCY.USD,
lastWorkspaceNumber: undefined,
localeTranslate: TestHelper.translateLocal,
reportActionsList: {},
doesEmployeePersonalDetailExist: false,
});
await waitForBatchedUpdates();

const workspaceFromIOUWriteCall = apiWriteSpy.mock.calls.find((call) => call.at(0) === WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT);
const writeOptions = requireRecord(requireCallArgument(workspaceFromIOUWriteCall, 2));

// The old chat's report preview action gets an optimistic message rebuilt via getReportPreviewReportActionMessage with the provided localeTranslate.
// The move also nulls the action under the old chat, so only match the update that carries the rebuilt action object.
const previewUpdate = requireRecordArrayProperty(writeOptions, 'optimisticData').find((update) => {
const actionValue = readProperty(readProperty(update, 'value'), previewActionID);
return typeof actionValue === 'object' && actionValue !== null;
});
expect(previewUpdate).toBeDefined();

const updatedAction = requireRecord(readProperty(readProperty(previewUpdate, 'value'), previewActionID));
const messages: unknown = readProperty(updatedAction, 'message');
const firstMessage: unknown = Array.isArray(messages) ? messages.at(0) : undefined;

// The rebuilt preview keeps the hardcoded-English copy for the moved expense report
expect(requireStringProperty(firstMessage, 'text')).toContain('owes');

// And the failure data clears the optimistic preview action from the new workspace chat again
const failureUpdate = requireRecordArrayProperty(writeOptions, 'failureData').find((update) => {
const value = readProperty(update, 'value');
return value !== null && typeof value === 'object' && previewActionID in value && readProperty(value, previewActionID) === null;
});
expect(failureUpdate).toBeDefined();

apiWriteSpy.mockRestore();
isIOUReportUsingReportSpy.mockRestore();
});

it('should return undefined for non-IOU reports', () => {
const nonIOUReport: Report = {
...createRandomReport(1, undefined),
Expand Down
Loading
Loading