diff --git a/assets/images/plus-minus.svg b/assets/images/plus-minus.svg
new file mode 100644
index 000000000000..eff3d99355ac
--- /dev/null
+++ b/assets/images/plus-minus.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 712b03bf4590..f42a2357330c 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -157,6 +157,7 @@ import Phone from '@assets/images/phone.svg';
import Pin from '@assets/images/pin.svg';
import Plane from '@assets/images/plane.svg';
import Play from '@assets/images/play.svg';
+import PlusMinus from '@assets/images/plus-minus.svg';
import Plus from '@assets/images/plus.svg';
import Printer from '@assets/images/printer.svg';
import Profile from '@assets/images/profile.svg';
@@ -429,5 +430,6 @@ export {
GalleryNotFound,
Train,
boltSlash,
+ PlusMinus,
MagnifyingGlassSpyMouthClosed,
};
diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx
index 717659c16fd3..c2fe17951acb 100644
--- a/src/components/MoneyRequestAmountInput.tsx
+++ b/src/components/MoneyRequestAmountInput.tsx
@@ -3,10 +3,10 @@ import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} fr
import type {NativeSyntheticEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import {useMouseContext} from '@hooks/useMouseContext';
-import * as Browser from '@libs/Browser';
-import * as CurrencyUtils from '@libs/CurrencyUtils';
+import {isMobileSafari} from '@libs/Browser';
+import {convertToFrontendAmountAsString, getCurrencyDecimals} from '@libs/CurrencyUtils';
import getOperatingSystem from '@libs/getOperatingSystem';
-import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
+import {replaceAllDigits, replaceCommasWithPeriod, stripCommaFromAmount, stripDecimalsFromAmount, stripSpacesFromAmount, validateAmount} from '@libs/MoneyRequestUtils';
import shouldIgnoreSelectionWhenUpdatedManually from '@libs/shouldIgnoreSelectionWhenUpdatedManually';
import CONST from '@src/CONST';
import isTextInputFocused from './TextInput/BaseTextInput/isTextInputFocused';
@@ -92,6 +92,18 @@ type MoneyRequestAmountInputProps = {
/** The width of inner content */
contentWidth?: number;
+
+ /** Whether the amount is negative */
+ isNegative?: boolean;
+
+ /** Function to toggle the amount to negative */
+ toggleNegative?: () => void;
+
+ /** Function to clear the negative amount */
+ clearNegative?: () => void;
+
+ /** Whether to allow flipping amount */
+ allowFlippingAmount?: boolean;
} & Pick;
type Selection = {
@@ -107,7 +119,7 @@ const getNewSelection = (oldSelection: Selection, prevLength: number, newLength:
return {start: cursorPosition, end: cursorPosition};
};
-const defaultOnFormatAmount = (amount: number, currency?: string) => CurrencyUtils.convertToFrontendAmountAsString(amount, currency ?? CONST.CURRENCY.USD);
+const defaultOnFormatAmount = (amount: number, currency?: string) => convertToFrontendAmountAsString(amount, currency ?? CONST.CURRENCY.USD);
function MoneyRequestAmountInput(
{
@@ -129,6 +141,10 @@ function MoneyRequestAmountInput(
autoGrow = true,
autoGrowExtraSpace,
contentWidth,
+ isNegative = false,
+ allowFlippingAmount = false,
+ toggleNegative,
+ clearNegative,
...props
}: MoneyRequestAmountInputProps,
forwardedRef: ForwardedRef,
@@ -139,7 +155,7 @@ function MoneyRequestAmountInput(
const amountRef = useRef(undefined);
- const decimals = CurrencyUtils.getCurrencyDecimals(currency);
+ const decimals = getCurrencyDecimals(currency);
const selectedAmountAsString = amount ? onFormatAmount(amount, currency) : '';
const [currentAmount, setCurrentAmount] = useState(selectedAmountAsString);
@@ -159,15 +175,17 @@ function MoneyRequestAmountInput(
*/
const setNewAmount = useCallback(
(newAmount: string) => {
+ if (allowFlippingAmount && newAmount.startsWith('-') && toggleNegative) {
+ toggleNegative();
+ }
+
// Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value
// More info: https://github.com/Expensify/App/issues/16974
- const newAmountWithoutSpaces = MoneyRequestUtils.stripSpacesFromAmount(newAmount);
- const finalAmount = newAmountWithoutSpaces.includes('.')
- ? MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces)
- : MoneyRequestUtils.replaceCommasWithPeriod(newAmountWithoutSpaces);
+ const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount);
+ const finalAmount = newAmountWithoutSpaces.includes('.') ? stripCommaFromAmount(newAmountWithoutSpaces) : replaceCommasWithPeriod(newAmountWithoutSpaces);
// Use a shallow copy of selection to trigger setSelection
// More info: https://github.com/Expensify/App/issues/16385
- if (!MoneyRequestUtils.validateAmount(finalAmount, decimals)) {
+ if (!validateAmount(finalAmount, decimals)) {
setSelection((prevSelection) => ({...prevSelection}));
return;
}
@@ -176,7 +194,7 @@ function MoneyRequestAmountInput(
willSelectionBeUpdatedManually.current = true;
let hasSelectionBeenSet = false;
- const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(finalAmount);
+ const strippedAmount = stripCommaFromAmount(finalAmount);
amountRef.current = strippedAmount;
setCurrentAmount((prevAmount) => {
const isForwardDelete = prevAmount.length > strippedAmount.length && forwardDeletePressedRef.current;
@@ -189,7 +207,7 @@ function MoneyRequestAmountInput(
return strippedAmount;
});
},
- [decimals, onAmountChange],
+ [allowFlippingAmount, decimals, onAmountChange, toggleNegative],
);
useImperativeHandle(moneyRequestAmountInputRef, () => ({
@@ -233,12 +251,12 @@ function MoneyRequestAmountInput(
// Modifies the amount to match the decimals for changed currency.
useEffect(() => {
// If the changed currency supports decimals, we can return
- if (MoneyRequestUtils.validateAmount(currentAmount, decimals)) {
+ if (validateAmount(currentAmount, decimals)) {
return;
}
// If the changed currency doesn't support decimals, we can strip the decimals
- setNewAmount(MoneyRequestUtils.stripDecimalsFromAmount(currentAmount));
+ setNewAmount(stripDecimalsFromAmount(currentAmount));
// we want to update only when decimals change (setNewAmount also changes when decimals change).
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
@@ -249,7 +267,12 @@ function MoneyRequestAmountInput(
*/
const textInputKeyPress = ({nativeEvent}: NativeSyntheticEvent) => {
const key = nativeEvent?.key.toLowerCase();
- if (Browser.isMobileSafari() && key === CONST.PLATFORM_SPECIFIC_KEYS.CTRL.DEFAULT) {
+
+ if (!textInput.current?.value && key === 'backspace' && isNegative) {
+ clearNegative?.();
+ }
+
+ if (isMobileSafari() && key === CONST.PLATFORM_SPECIFIC_KEYS.CTRL.DEFAULT) {
// Optimistically anticipate forward-delete on iOS Safari (in cases where the Mac Accessiblity keyboard is being
// used for input). If the Control-D shortcut doesn't get sent, the ref will still be reset on the next key press.
forwardDeletePressedRef.current = true;
@@ -276,7 +299,7 @@ function MoneyRequestAmountInput(
});
}, [amount, currency, onFormatAmount, formatAmountOnBlur, maxLength]);
- const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit);
+ const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit);
const {setMouseDown, setMouseUp} = useMouseContext();
const handleMouseDown = (e: React.MouseEvent) => {
@@ -340,6 +363,7 @@ function MoneyRequestAmountInput(
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
contentWidth={contentWidth}
+ isNegative={isNegative}
/>
);
}
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index 26cdbac4d890..4194452ddbc0 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -136,7 +136,7 @@ function MoneyRequestPreviewContent({
merchant,
tag,
category,
- } = useMemo>(() => getTransactionDetails(transaction) ?? {}, [transaction]);
+ } = useMemo>(() => getTransactionDetails(transaction, undefined, true) ?? {}, [transaction]);
const description = truncate(StringUtils.lineBreaksToSpaces(requestComment), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH});
const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH});
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index acb6b05e077d..bbe1de323f9b 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -150,7 +150,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
originalAmount: transactionOriginalAmount,
originalCurrency: transactionOriginalCurrency,
postedDate: transactionPostedDate,
- } = useMemo>(() => getTransactionDetails(transaction) ?? {}, [transaction]);
+ } = useMemo>(() => getTransactionDetails(transaction, undefined, true) ?? {}, [transaction]);
const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
const isDistanceRequest = isDistanceRequestTransactionUtils(transaction);
const isPerDiemRequest = isPerDiemRequestTransactionUtils(transaction);
diff --git a/src/components/TextInputWithCurrencySymbol/BaseTextInputWithCurrencySymbol.tsx b/src/components/TextInputWithCurrencySymbol/BaseTextInputWithCurrencySymbol.tsx
index 4c30b048c1af..343d116cbf9a 100644
--- a/src/components/TextInputWithCurrencySymbol/BaseTextInputWithCurrencySymbol.tsx
+++ b/src/components/TextInputWithCurrencySymbol/BaseTextInputWithCurrencySymbol.tsx
@@ -2,10 +2,11 @@ import React from 'react';
import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native';
import AmountTextInput from '@components/AmountTextInput';
import CurrencySymbolButton from '@components/CurrencySymbolButton';
+import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as CurrencyUtils from '@libs/CurrencyUtils';
-import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
+import {getLocalizedCurrencySymbol, isCurrencySymbolLTR} from '@libs/CurrencyUtils';
+import {addLeadingZero, replaceAllDigits} from '@libs/MoneyRequestUtils';
import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/types';
import type BaseTextInputWithCurrencySymbolProps from './types';
@@ -22,14 +23,15 @@ function BaseTextInputWithCurrencySymbol(
isCurrencyPressable = true,
hideCurrencySymbol = false,
extraSymbol,
+ isNegative = false,
style,
...rest
}: BaseTextInputWithCurrencySymbolProps,
ref: React.ForwardedRef,
) {
const {fromLocaleDigit} = useLocalize();
- const currencySymbol = CurrencyUtils.getLocalizedCurrencySymbol(selectedCurrencyCode);
- const isCurrencySymbolLTR = CurrencyUtils.isCurrencySymbolLTR(selectedCurrencyCode);
+ const currencySymbol = getLocalizedCurrencySymbol(selectedCurrencyCode);
+ const shouldShowCurrencySymbolLTR = isCurrencySymbolLTR(selectedCurrencyCode);
const styles = useThemeStyles();
const currencySymbolButton = !hideCurrencySymbol && (
@@ -46,7 +48,7 @@ function BaseTextInputWithCurrencySymbol(
* @param text - Changed text from user input
*/
const setFormattedAmount = (text: string) => {
- const newAmount = MoneyRequestUtils.addLeadingZero(MoneyRequestUtils.replaceAllDigits(text, fromLocaleDigit));
+ const newAmount = addLeadingZero(replaceAllDigits(text, fromLocaleDigit));
onChangeAmount(newAmount);
};
@@ -67,9 +69,12 @@ function BaseTextInputWithCurrencySymbol(
/>
);
- if (isCurrencySymbolLTR) {
+ const negativeSymbol = -;
+
+ if (shouldShowCurrencySymbolLTR) {
return (
<>
+ {isNegative && negativeSymbol}
{currencySymbolButton}
{amountTextInput}
{extraSymbol}
@@ -79,6 +84,7 @@ function BaseTextInputWithCurrencySymbol(
return (
<>
+ {isNegative && negativeSymbol}
{amountTextInput}
{currencySymbolButton}
{extraSymbol}
diff --git a/src/components/TextInputWithCurrencySymbol/types.ts b/src/components/TextInputWithCurrencySymbol/types.ts
index 3988654584d0..eea33cadd4db 100644
--- a/src/components/TextInputWithCurrencySymbol/types.ts
+++ b/src/components/TextInputWithCurrencySymbol/types.ts
@@ -77,6 +77,9 @@ type BaseTextInputWithCurrencySymbolProps = {
/** Hide the focus styles on TextInput */
hideFocusedState?: boolean;
+
+ /** Whether the amount is negative */
+ isNegative?: boolean;
} & Pick;
type TextInputWithCurrencySymbolProps = Omit & {
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 127de853ce29..97935b90c39e 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -975,6 +975,7 @@ const translations = {
payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} elsewhere` : `Pay elsewhere`),
nextStep: 'Next steps',
finished: 'Finished',
+ flip: 'Flip',
sendInvoice: ({amount}: RequestAmountParams) => `Send ${amount} invoice`,
submitAmount: ({amount}: RequestAmountParams) => `submit ${amount}`,
submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `submitted ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
diff --git a/src/languages/es.ts b/src/languages/es.ts
index ba838537a2a7..9a9444465dfc 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -969,6 +969,7 @@ const translations = {
payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} de otra forma` : `Pagar de otra forma`),
nextStep: 'Pasos siguientes',
finished: 'Finalizado',
+ flip: 'Cambiar',
sendInvoice: ({amount}: RequestAmountParams) => `Enviar factura de ${amount}`,
submitAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`,
submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicitó ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 465067a045c4..4620a43a6896 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -205,7 +205,7 @@ function getForReportAction({
if (hasModifiedAmount) {
const oldCurrency = reportActionOriginalMessage?.oldCurrency;
const oldAmountValue = reportActionOriginalMessage?.oldAmount ?? 0;
- const oldAmount = oldAmountValue > 0 ? convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency) : '';
+ const oldAmount = oldAmountValue ? convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency) : '';
const currency = reportActionOriginalMessage?.currency;
const amount = convertToDisplayString(reportActionOriginalMessage?.amount ?? 0, currency);
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 454c25a0b6d1..c13ca1cf7457 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -3213,8 +3213,8 @@ function getMoneyRequestSpendBreakdown(report: OnyxInputOrEntry, searchR
// There is a possibility that if the Expense report has a negative total.
// This is because there are instances where you can get a credit back on your card,
// or you enter a negative expense to “offset” future expenses
- nonReimbursableSpend = isExpenseReport(moneyRequestReport) ? nonReimbursableSpend * -1 : Math.abs(nonReimbursableSpend);
- totalSpend = isExpenseReport(moneyRequestReport) ? totalSpend * -1 : Math.abs(totalSpend);
+ nonReimbursableSpend = isExpenseReport(moneyRequestReport) ? nonReimbursableSpend * -1 : nonReimbursableSpend;
+ totalSpend = isExpenseReport(moneyRequestReport) ? totalSpend * -1 : totalSpend;
const totalDisplaySpend = totalSpend;
const reimbursableSpend = totalDisplaySpend - nonReimbursableSpend;
@@ -3466,14 +3466,19 @@ function getMoneyRequestReportName({
* into a flat object. Used for displaying transactions and sending them in API commands
*/
-function getTransactionDetails(transaction: OnyxInputOrEntry, createdDateFormat: string = CONST.DATE.FNS_FORMAT_STRING): TransactionDetails | undefined {
+function getTransactionDetails(
+ transaction: OnyxInputOrEntry,
+ createdDateFormat: string = CONST.DATE.FNS_FORMAT_STRING,
+ allowNegativeAmount = false,
+): TransactionDetails | undefined {
if (!transaction) {
return;
}
const report = getReportOrDraftReport(transaction?.reportID);
+
return {
created: getFormattedCreated(transaction, createdDateFormat),
- amount: getTransactionAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
+ amount: getTransactionAmount(transaction, !isEmptyObject(report) && isExpenseReport(report), undefined, allowNegativeAmount),
attendees: getAttendees(transaction),
taxAmount: getTaxAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
taxCode: getTaxCode(transaction),
@@ -4090,7 +4095,7 @@ function getModifiedExpenseOriginalMessage(
// to match how we handle the modified expense action in oldDot
const didAmountOrCurrencyChange = 'amount' in transactionChanges || 'currency' in transactionChanges;
if (didAmountOrCurrencyChange) {
- originalMessage.oldAmount = getTransactionAmount(oldTransaction, isFromExpenseReport);
+ originalMessage.oldAmount = getTransactionAmount(oldTransaction, isFromExpenseReport, false, true);
originalMessage.amount = transactionChanges?.amount ?? transactionChanges.oldAmount;
originalMessage.oldCurrency = getCurrency(oldTransaction);
originalMessage.currency = transactionChanges?.currency ?? transactionChanges.oldCurrency;
@@ -4129,7 +4134,7 @@ function getModifiedExpenseOriginalMessage(
}
if ('customUnitRateID' in transactionChanges && updatedTransaction?.comment?.customUnit?.customUnitRateID) {
- originalMessage.oldAmount = getTransactionAmount(oldTransaction, isFromExpenseReport);
+ originalMessage.oldAmount = getTransactionAmount(oldTransaction, isFromExpenseReport, false, true);
originalMessage.oldCurrency = getCurrency(oldTransaction);
originalMessage.oldMerchant = getMerchant(oldTransaction);
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 0f0ee9b635e5..9a4f349c57ae 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -127,7 +127,7 @@ function getTransactionItemCommonFormattedProperties(
const formattedFrom = from?.displayName ?? from?.login ?? '';
const formattedTo = to?.displayName ?? to?.login ?? '';
- const formattedTotal = getTransactionAmount(transactionItem, isExpenseReport);
+ const formattedTotal = getTransactionAmount(transactionItem, isExpenseReport, undefined, true);
const date = transactionItem?.modifiedCreated ? transactionItem.modifiedCreated : transactionItem?.created;
const merchant = getTransactionMerchant(transactionItem);
const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ? '' : merchant;
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index f8374bfa358e..a8f3343ad0d1 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -326,12 +326,14 @@ function getUpdatedTransaction({
isFromExpenseReport,
shouldUpdateReceiptState = true,
policy = undefined,
+ allowNegative = false,
}: {
transaction: Transaction;
transactionChanges: TransactionChanges;
isFromExpenseReport: boolean;
shouldUpdateReceiptState?: boolean;
policy?: OnyxEntry;
+ allowNegative?: boolean;
}): Transaction {
// Only changing the first level fields so no need for deep clone now
const updatedTransaction = lodashDeepClone(transaction);
@@ -349,7 +351,7 @@ function getUpdatedTransaction({
shouldStopSmartscan = true;
}
if (Object.hasOwn(transactionChanges, 'amount') && typeof transactionChanges.amount === 'number') {
- updatedTransaction.modifiedAmount = isFromExpenseReport ? -transactionChanges.amount : transactionChanges.amount;
+ updatedTransaction.modifiedAmount = isFromExpenseReport && !allowNegative ? -transactionChanges.amount : transactionChanges.amount;
shouldStopSmartscan = true;
}
if (Object.hasOwn(transactionChanges, 'currency')) {
@@ -417,7 +419,7 @@ function getUpdatedTransaction({
const distanceInMeters = getDistanceInMeters(transaction, oldMileageRate?.unit);
const amount = DistanceRequestUtils.getDistanceRequestAmount(distanceInMeters, unit, rate ?? 0);
- const updatedAmount = isFromExpenseReport ? -amount : amount;
+ const updatedAmount = isFromExpenseReport && !allowNegative ? -amount : amount;
const updatedCurrency = updatedMileageRate.currency ?? CONST.CURRENCY.USD;
const updatedMerchant = DistanceRequestUtils.getDistanceMerchant(true, distanceInMeters, unit, rate, updatedCurrency, Localize.translateLocal, (digit) =>
toLocaleDigit(preferredLocale, digit),
@@ -501,9 +503,9 @@ function getDescription(transaction: OnyxInputOrEntry): string {
/**
* Return the amount field from the transaction, return the modifiedAmount if present.
*/
-function getAmount(transaction: OnyxInputOrEntry, isFromExpenseReport = false, isFromTrackedExpense = false): number {
+function getAmount(transaction: OnyxInputOrEntry, isFromExpenseReport = false, isFromTrackedExpense = false, allowNegative = false): number {
// IOU requests cannot have negative values, but they can be stored as negative values, let's return absolute value
- if (!isFromExpenseReport || isFromTrackedExpense) {
+ if ((!isFromExpenseReport || isFromTrackedExpense) && !allowNegative) {
const amount = transaction?.modifiedAmount ?? 0;
if (amount) {
return Math.abs(amount);
@@ -515,12 +517,21 @@ function getAmount(transaction: OnyxInputOrEntry, isFromExpenseRepo
// The amounts are stored using an opposite sign and negative values can be set,
// we need to return an opposite sign than is saved in the transaction object
let amount = transaction?.modifiedAmount ?? 0;
- if (amount) {
+ if (amount && !allowNegative) {
return -amount;
}
- // To avoid -0 being shown, lets only change the sign if the value is other than 0.
+ if (amount) {
+ return amount;
+ }
+
amount = transaction?.amount ?? 0;
+
+ if (allowNegative) {
+ return amount;
+ }
+
+ // To avoid -0 being shown, lets only change the sign if the value is other than 0.
return amount ? -amount : 0;
}
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 1a2686488abe..e5d0edd001ed 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -3397,6 +3397,7 @@ function getUpdateMoneyRequestParams(
policyCategories: OnyxTypes.OnyxInputOrEntry,
violations?: OnyxEntry,
hash?: number,
+ allowNegative?: boolean,
): UpdateMoneyRequestData {
const optimisticData: OnyxUpdate[] = [];
const successData: OnyxUpdate[] = [];
@@ -3420,9 +3421,10 @@ function getUpdateMoneyRequestParams(
transactionChanges,
isFromExpenseReport,
policy,
+ allowNegative,
})
: undefined;
- const transactionDetails = getTransactionDetails(updatedTransaction);
+ const transactionDetails = getTransactionDetails(updatedTransaction, undefined, true);
if (transactionDetails?.waypoints) {
// This needs to be a JSON string since we're sending this to the MapBox API
@@ -6491,6 +6493,7 @@ type UpdateMoneyRequestAmountAndCurrencyParams = {
policyTagList?: OnyxEntry;
policyCategories?: OnyxEntry;
taxCode: string;
+ allowNegative?: boolean;
};
/** Updates the amount and currency fields of an expense */
@@ -6504,6 +6507,7 @@ function updateMoneyRequestAmountAndCurrency({
policyTagList,
policyCategories,
taxCode,
+ allowNegative = false,
}: UpdateMoneyRequestAmountAndCurrencyParams) {
const transactionChanges = {
amount,
@@ -6517,7 +6521,7 @@ function updateMoneyRequestAmountAndCurrency({
if (isTrackExpenseReport(transactionThreadReport) && isSelfDM(parentReport)) {
data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, policy);
} else {
- data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList ?? null, policyCategories ?? null);
+ data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList ?? null, policyCategories ?? null, undefined, undefined, allowNegative);
}
const {params, onyxData} = data;
API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY, params, onyxData);
diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx
index a948e43d8656..083d79bf80da 100644
--- a/src/pages/iou/MoneyRequestAmountForm.tsx
+++ b/src/pages/iou/MoneyRequestAmountForm.tsx
@@ -6,6 +6,7 @@ import type {ValueOf} from 'type-fest';
import BigNumberPad from '@components/BigNumberPad';
import Button from '@components/Button';
import FormHelpMessage from '@components/FormHelpMessage';
+import * as Expensicons from '@components/Icon/Expensicons';
import MoneyRequestAmountInput from '@components/MoneyRequestAmountInput';
import type {MoneyRequestAmountInputRef} from '@components/MoneyRequestAmountInput';
import ScrollView from '@components/ScrollView';
@@ -68,6 +69,9 @@ type MoneyRequestAmountFormProps = {
/** Whether the user input should be kept or not */
shouldKeepUserInput?: boolean;
+
+ /** Whether to allow flipping the amount */
+ allowFlippingAmount?: boolean;
};
const isAmountInvalid = (amount: string) => !amount.length || parseFloat(amount) < 0.01;
@@ -93,6 +97,7 @@ function MoneyRequestAmountForm(
onSubmitButtonPress,
selectedTab = CONST.TAB_REQUEST.MANUAL,
shouldKeepUserInput = false,
+ allowFlippingAmount = false,
}: MoneyRequestAmountFormProps,
forwardedRef: ForwardedRef,
) {
@@ -103,6 +108,8 @@ function MoneyRequestAmountForm(
const textInput = useRef(null);
const moneyRequestAmountInput = useRef(null);
+ const [isNegative, setIsNegative] = useState(false);
+
const [formError, setFormError] = useState('');
const [shouldUpdateSelection, setShouldUpdateSelection] = useState(true);
@@ -111,6 +118,8 @@ function MoneyRequestAmountForm(
const formattedTaxAmount = convertToDisplayString(Math.abs(taxAmount), currency);
+ const absoluteAmount = Math.abs(amount);
+
/**
* Event occurs when a user presses a mouse button over an DOM element.
*/
@@ -162,10 +171,18 @@ function MoneyRequestAmountForm(
);
useEffect(() => {
- if (!currency || typeof amount !== 'number') {
+ if (amount >= 0) {
+ return;
+ }
+
+ setIsNegative(true);
+ }, [amount]);
+
+ useEffect(() => {
+ if (!currency || typeof absoluteAmount !== 'number') {
return;
}
- initializeAmount(amount);
+ initializeAmount(absoluteAmount);
// we want to re-initialize the state only when the selected tab
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [selectedTab]);
@@ -227,9 +244,11 @@ function MoneyRequestAmountForm(
return;
}
- onSubmitButtonPress({amount: currentAmount, currency, paymentMethod: iouPaymentType});
+ const newAmount = isNegative ? `-${currentAmount}` : currentAmount;
+
+ onSubmitButtonPress({amount: newAmount, currency, paymentMethod: iouPaymentType});
},
- [taxAmount, onSubmitButtonPress, currency, translate, formattedTaxAmount],
+ [taxAmount, currency, isNegative, onSubmitButtonPress, translate, formattedTaxAmount],
);
const buttonText: string = useMemo(() => {
@@ -248,48 +267,99 @@ function MoneyRequestAmountForm(
setFormError('');
}, [selectedTab]);
+ const toggleNegative = useCallback(() => {
+ setIsNegative((prevIsNegative) => !prevIsNegative);
+ }, []);
+
+ const clearNegative = useCallback(() => {
+ setIsNegative(false);
+ }, []);
+
return (
- onMouseDown(event, [AMOUNT_VIEW_ID])}
- style={[styles.moneyRequestAmountContainer, styles.flex1, styles.flexRow, styles.w100, styles.alignItemsCenter, styles.justifyContentCenter]}
- >
- {
- if (!formError) {
- return;
- }
- setFormError('');
- }}
- shouldUpdateSelection={shouldUpdateSelection}
- ref={(ref) => {
- if (typeof forwardedRef === 'function') {
- forwardedRef(ref);
- } else if (forwardedRef?.current) {
- // eslint-disable-next-line no-param-reassign
- forwardedRef.current = ref;
- }
- textInput.current = ref;
- }}
- shouldKeepUserInput={shouldKeepUserInput}
- moneyRequestAmountInputRef={moneyRequestAmountInput}
- inputStyle={[styles.iouAmountTextInput]}
- containerStyle={[styles.iouAmountTextInputContainer]}
- />
- {!!formError && (
-
+ onMouseDown(event, [AMOUNT_VIEW_ID])}
+ style={[styles.moneyRequestAmountContainer, styles.flexRow, styles.w100, styles.alignItemsCenter, styles.justifyContentCenter]}
+ >
+ {
+ if (!formError) {
+ return;
+ }
+ setFormError('');
+ }}
+ shouldUpdateSelection={shouldUpdateSelection}
+ ref={(ref) => {
+ if (typeof forwardedRef === 'function') {
+ forwardedRef(ref);
+ } else if (forwardedRef?.current) {
+ // eslint-disable-next-line no-param-reassign
+ forwardedRef.current = ref;
+ }
+ textInput.current = ref;
+ }}
+ shouldKeepUserInput={shouldKeepUserInput}
+ moneyRequestAmountInputRef={moneyRequestAmountInput}
+ inputStyle={[styles.iouAmountTextInput]}
+ containerStyle={[styles.iouAmountTextInputContainer]}
+ toggleNegative={toggleNegative}
+ clearNegative={clearNegative}
+ isNegative={isNegative}
+ allowFlippingAmount={allowFlippingAmount}
+ />
+ {!!formError && (
+
+ )}
+
+ {isCurrencyPressable && !canUseTouchScreen && (
+
)}
+
+
+ {isCurrencyPressable && canUseTouchScreen && (
+
+ )}
+ {allowFlippingAmount && canUseTouchScreen && (
+
+ )}
+
+
onMouseDown(event, [NUM_PAD_CONTAINER_VIEW_ID, NUM_PAD_VIEW_ID])}
style={[styles.w100, styles.justifyContentEnd, styles.pageWrapper, styles.pt0]}
diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx
index 97f872d3a0f9..cc0a745db3ea 100644
--- a/src/pages/iou/request/step/IOURequestStepAmount.tsx
+++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx
@@ -11,7 +11,8 @@ import {convertToBackendAmount, isValidCurrencyCode} from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils';
import {isPaidGroupPolicy} from '@libs/PolicyUtils';
-import {getBankAccountRoute, getPolicyExpenseChat, getTransactionDetails, isArchivedReport, isPolicyExpenseChat} from '@libs/ReportUtils';
+import {getBankAccountRoute, getPolicyExpenseChat, getTransactionDetails, isArchivedReport, isPolicyExpenseChat, doesReportBelongToWorkspace} from '@libs/ReportUtils';
+import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils';
import playSound, {SOUNDS} from '@libs/Sound';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import {calculateTaxAmount, getAmount, getCurrency, getDefaultTaxCode, getRequestType, getTaxValue} from '@libs/TransactionUtils';
@@ -85,7 +86,7 @@ function IOURequestStepAmount({
const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT;
const isEditingSplitBill = isEditing && isSplitBill;
const currentTransaction = isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction;
- const {amount: transactionAmount} = getTransactionDetails(currentTransaction) ?? {amount: 0};
+ const {amount: transactionAmount} = getTransactionDetails(currentTransaction, undefined, true) ?? {amount: 0};
const {currency: originalCurrency} = getTransactionDetails(isEditing && !isEmptyObject(draftTransaction) ? draftTransaction : transaction) ?? {currency: CONST.CURRENCY.USD};
const currency = isValidCurrencyCode(selectedCurrency) ? selectedCurrency : originalCurrency;
@@ -284,7 +285,7 @@ function IOURequestStepAmount({
// If the value hasn't changed, don't request to save changes on the server and just close the modal
const transactionCurrency = getCurrency(currentTransaction);
- if (newAmount === getAmount(currentTransaction) && currency === transactionCurrency) {
+ if (newAmount === getAmount(currentTransaction, false, false, true) && currency === transactionCurrency) {
navigateBack();
return;
}
@@ -302,7 +303,10 @@ function IOURequestStepAmount({
return;
}
- updateMoneyRequestAmountAndCurrency({transactionID, transactionThreadReportID: reportID, currency, amount: newAmount, taxAmount, policy, taxCode, policyCategories});
+ const policyMemberAccountIDs = getPolicyEmployeeAccountIDs(policyID);
+ const isWorkspace = doesReportBelongToWorkspace(report, policyMemberAccountIDs, policyID);
+
+ updateMoneyRequestAmountAndCurrency({transactionID, transactionThreadReportID: reportID, currency, amount: newAmount, taxAmount, policy, taxCode, policyCategories, allowNegative: isWorkspace});
navigateBack();
};
@@ -317,7 +321,7 @@ function IOURequestStepAmount({
);
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
index c133e833fc6d..b7d7e936d9ce 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
@@ -791,7 +791,7 @@ function IOURequestStepConfirmation({
;
diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts
index 7635c8e0fce5..b8bc9e14a5fd 100644
--- a/tests/unit/Search/SearchUIUtilsTest.ts
+++ b/tests/unit/Search/SearchUIUtilsTest.ts
@@ -282,7 +282,7 @@ const transactionsListItems = [
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: 'Admin',
- formattedTotal: 5000,
+ formattedTotal: -5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
@@ -335,7 +335,7 @@ const transactionsListItems = [
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: 'Admin',
- formattedTotal: 5000,
+ formattedTotal: -5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
@@ -423,7 +423,7 @@ const reportsListItems = [
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: 'Admin',
- formattedTotal: 5000,
+ formattedTotal: -5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
@@ -512,7 +512,7 @@ const reportsListItems = [
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: 'Admin',
- formattedTotal: 5000,
+ formattedTotal: -5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
@@ -674,7 +674,7 @@ describe('SearchUIUtils', () => {
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: 'Admin',
- formattedTotal: 5000,
+ formattedTotal: -5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
@@ -729,7 +729,7 @@ describe('SearchUIUtils', () => {
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: 'Admin',
- formattedTotal: 5000,
+ formattedTotal: -5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
@@ -821,7 +821,7 @@ describe('SearchUIUtils', () => {
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: 'Admin',
- formattedTotal: 5000,
+ formattedTotal: -5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
@@ -912,7 +912,7 @@ describe('SearchUIUtils', () => {
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: 'Admin',
- formattedTotal: 5000,
+ formattedTotal: -5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
@@ -1008,7 +1008,7 @@ describe('SearchUIUtils', () => {
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: 'Admin',
- formattedTotal: 5000,
+ formattedTotal: -5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
@@ -1099,7 +1099,7 @@ describe('SearchUIUtils', () => {
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: 'Admin',
- formattedTotal: 5000,
+ formattedTotal: -5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
@@ -1195,7 +1195,7 @@ describe('SearchUIUtils', () => {
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: 'Admin',
- formattedTotal: 5000,
+ formattedTotal: -5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
@@ -1286,7 +1286,7 @@ describe('SearchUIUtils', () => {
formattedFrom: 'Admin',
formattedMerchant: 'Expense',
formattedTo: 'Admin',
- formattedTotal: 5000,
+ formattedTotal: -5000,
from: {
accountID: 18439984,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',