Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Cancel Payment command (again) #33836

Merged
merged 23 commits into from
Jan 15, 2024
Merged
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
4 changes: 3 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,9 +601,11 @@ const CONST = {
},
THREAD_DISABLED: ['CREATED'],
},
CANCEL_PAYMENT_REASONS: {
ADMIN: 'CANCEL_REASON_ADMIN',
},
ACTIONABLE_MENTION_WHISPER_RESOLUTION: {
INVITE: 'invited',
NOTHING: 'nothing',
},
ARCHIVE_REASON: {
DEFAULT: 'default',
Expand Down
28 changes: 27 additions & 1 deletion src/components/MoneyReportHeader.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useMemo} from 'react';
import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
Expand All @@ -24,7 +24,9 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
import HeaderWithBackButton from './HeaderWithBackButton';
import * as Expensicons from './Icon/Expensicons';
import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar';
import participantPropTypes from './participantPropTypes';
import SettlementButton from './SettlementButton';
Expand Down Expand Up @@ -94,6 +96,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
isPolicyAdmin && (isApproved || isManager)
: isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager);
const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport);
const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);

const cancelPayment = useCallback(() => {
IOU.cancelPayment(moneyRequestReport, chatReport);
setIsConfirmModalVisible(false);
}, [moneyRequestReport, chatReport]);

const shouldShowPayButton = useMemo(
() => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport),
[isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport],
Expand All @@ -120,6 +129,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
);

const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)];
if (isPayer && isSettled && ReportUtils.isExpenseReport(moneyRequestReport)) {
threeDotsMenuItems.push({
icon: Expensicons.Trashcan,
text: translate('iou.cancelPayment'),
onSelected: () => setIsConfirmModalVisible(true),
});
}
if (!ReportUtils.isArchivedRoom(chatReport)) {
threeDotsMenuItems.push({
icon: ZoomIcon,
Expand Down Expand Up @@ -217,6 +233,16 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
</View>
)}
</View>
<ConfirmModal
title={translate('iou.cancelPayment')}
isVisible={isConfirmModalVisible}
onConfirm={cancelPayment}
onCancel={() => setIsConfirmModalVisible(false)}
prompt={translate('iou.cancelPaymentConfirmation')}
confirmText={translate('iou.cancelPayment')}
cancelText={translate('common.dismiss')}
danger
/>
</View>
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
import CONST from '@src/CONST';
import type {
AddressLineParams,
AdminCanceledRequestParams,
AlreadySignedInParams,
AmountEachParams,
ApprovedAmountParams,
Expand Down Expand Up @@ -111,6 +112,7 @@ type AllCountries = Record<keyof typeof CONST.ALL_COUNTRIES, string>;
export default {
common: {
cancel: 'Cancel',
dismiss: 'Dismiss',
yes: 'Yes',
no: 'No',
ok: 'OK',
Expand Down Expand Up @@ -573,6 +575,8 @@ export default {
requestMoney: 'Request money',
sendMoney: 'Send money',
pay: 'Pay',
cancelPayment: 'Cancel payment',
cancelPaymentConfirmation: 'Are you sure that you want to cancel this payment?',
viewDetails: 'View details',
pending: 'Pending',
canceled: 'Canceled',
Expand Down Expand Up @@ -609,6 +613,7 @@ export default {
payerSettled: ({amount}: PayerSettledParams) => `paid ${amount}`,
approvedAmount: ({amount}: ApprovedAmountParams) => `approved ${amount}`,
waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`,
adminCanceledRequest: ({amount}: AdminCanceledRequestParams) => `The ${amount} payment has been cancelled by the admin.`,
canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) =>
`Canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
Expand Down
5 changes: 5 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CONST from '@src/CONST';
import type {
AddressLineParams,
AdminCanceledRequestParams,
AlreadySignedInParams,
AmountEachParams,
ApprovedAmountParams,
Expand Down Expand Up @@ -101,6 +102,7 @@ import type {
export default {
common: {
cancel: 'Cancelar',
dismiss: 'Descartar',
yes: 'Sí',
no: 'No',
ok: 'OK',
Expand Down Expand Up @@ -566,6 +568,8 @@ export default {
requestMoney: 'Pedir dinero',
sendMoney: 'Enviar dinero',
pay: 'Pagar',
cancelPayment: 'Cancelar el pago',
cancelPaymentConfirmation: '¿Estás seguro de que quieres cancelar este pago?',
viewDetails: 'Ver detalles',
pending: 'Pendiente',
canceled: 'Canceló',
Expand Down Expand Up @@ -602,6 +606,7 @@ export default {
payerSettled: ({amount}: PayerSettledParams) => `pagó ${amount}`,
approvedAmount: ({amount}: ApprovedAmountParams) => `aprobó ${amount}`,
waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`,
adminCanceledRequest: ({amount}: AdminCanceledRequestParams) => `El pago de ${amount} ha sido cancelado por el administrador.`,
canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) =>
`Canceló el pago ${amount}, porque ${submitterDisplayName} no habilitó su billetera Expensify en un plazo de 30 días.`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
Expand Down
3 changes: 3 additions & 0 deletions src/languages/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ type WaitingOnBankAccountParams = {submitterDisplayName: string};

type CanceledRequestParams = {amount: string; submitterDisplayName: string};

type AdminCanceledRequestParams = {amount: string};

type SettledAfterAddedBankAccountParams = {submitterDisplayName: string; amount: string};

type PaidElsewhereWithAmountParams = {payer?: string; amount: string};
Expand Down Expand Up @@ -288,6 +290,7 @@ type TranslationFlatObject = {
};

export type {
AdminCanceledRequestParams,
ApprovedAmountParams,
AddressLineParams,
AlreadySignedInParams,
Expand Down
2 changes: 1 addition & 1 deletion src/libs/OptionsListUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ function getLastMessageTextForReport(report) {
} else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) {
lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report);
} else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) {
lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(report);
lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, report);
} else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) {
lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction);
} else if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) {
Expand Down
77 changes: 63 additions & 14 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, Report, ReportAction, ReportMetadata, Session, Transaction} from '@src/types/onyx';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage';
import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated, ReimbursementDeQueuedMessage} from '@src/types/onyx/OriginalMessage';
import type {Status} from '@src/types/onyx/PersonalDetails';
import type {NotificationPreference} from '@src/types/onyx/Report';
import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction';
Expand Down Expand Up @@ -180,6 +180,11 @@ type OptimisticSubmittedReportAction = Pick<
'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
>;

type OptimisticCancelPaymentReportAction = Pick<
ReportAction,
'actionName' | 'actorAccountID' | 'message' | 'originalMessage' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
>;

type OptimisticEditedTaskReportAction = Pick<
ReportAction,
'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'shouldShow' | 'message' | 'person'
Expand Down Expand Up @@ -1592,9 +1597,13 @@ function getReimbursementQueuedActionMessage(reportAction: OnyxEntry<ReportActio
/**
* Returns the preview message for `REIMBURSEMENTDEQUEUED` action
*/
function getReimbursementDeQueuedActionMessage(report: OnyxEntry<Report>): string {
function getReimbursementDeQueuedActionMessage(reportAction: OnyxEntry<ReportAction>, report: OnyxEntry<Report>): string {
const amount = CurrencyUtils.convertToDisplayString(Math.abs(report?.total ?? 0), report?.currency);
const originalMessage = reportAction?.originalMessage as ReimbursementDeQueuedMessage | undefined;
if (originalMessage?.cancellationReason === CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN) {
return Localize.translateLocal('iou.adminCanceledRequest', {amount});
}
const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, true) ?? '';
const amount = CurrencyUtils.convertToDisplayString(report?.total ?? 0, report?.currency);

return Localize.translateLocal('iou.canceledRequest', {submitterDisplayName, amount});
}
Expand Down Expand Up @@ -2877,6 +2886,40 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string,
};
}

/**
* Builds an optimistic REIMBURSEMENTDEQUEUED report action with a randomly generated reportActionID.
*
*/
function buildOptimisticCancelPaymentReportAction(expenseReportID: string): OptimisticCancelPaymentReportAction {
return {
actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED,
actorAccountID: currentUserAccountID,
message: [
{
cancellationReason: CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN,
expenseReportID,
type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
text: '',
},
],
originalMessage: {
cancellationReason: CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN,
expenseReportID,
},
person: [
{
style: 'strong',
text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
type: 'TEXT',
},
],
reportActionID: NumberUtils.rand64(),
shouldShow: true,
created: DateUtils.getDBTime(),
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
};
}

/**
* Builds an optimistic report preview action with a randomly generated reportActionID.
*
Expand Down Expand Up @@ -4199,17 +4242,22 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry<ReportAction>)
const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency) ?? '';
const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport?.managerID, true);

switch (originalMessage.paymentType) {
case CONST.IOU.PAYMENT_TYPE.ELSEWHERE:
translationKey = 'iou.paidElsewhereWithAmount';
break;
case CONST.IOU.PAYMENT_TYPE.EXPENSIFY:
case CONST.IOU.PAYMENT_TYPE.VBBA:
translationKey = 'iou.paidWithExpensifyWithAmount';
break;
default:
translationKey = 'iou.payerPaidAmount';
break;
// If the payment was cancelled, show the "Owes" message
if (!isSettled(IOUReportID)) {
translationKey = 'iou.payerOwesAmount';
} else {
switch (originalMessage.paymentType) {
case CONST.IOU.PAYMENT_TYPE.ELSEWHERE:
translationKey = 'iou.paidElsewhereWithAmount';
break;
case CONST.IOU.PAYMENT_TYPE.EXPENSIFY:
case CONST.IOU.PAYMENT_TYPE.VBBA:
translationKey = 'iou.paidWithExpensifyWithAmount';
break;
default:
translationKey = 'iou.payerPaidAmount';
break;
}
}
return Localize.translateLocal(translationKey, {amount: formattedAmount, payer: payerName ?? ''});
}
Expand Down Expand Up @@ -4436,6 +4484,7 @@ export {
buildOptimisticIOUReportAction,
buildOptimisticReportPreview,
buildOptimisticModifiedExpenseReportAction,
buildOptimisticCancelPaymentReportAction,
updateReportPreview,
buildOptimisticTaskReportAction,
buildOptimisticAddCommentReportAction,
Expand Down
Loading
Loading